在512字节中拟合第一个

2021-06-12 04:45:49

如果你足够深,软件充满了循环依赖关系。他们编译的语言编译是最明显的例子,而不是一个人。要编译内核,需要运行内核。链接器,构建系统,shell。即使是文本编辑器,如果要编写代码而不是下载它。你如何打破这个循环? 1自引导问题首先引起我的注意,我已经被绘制了Tothis独特的软件工程领域。并非害怕有人会潜入信任的信任攻击,但只是作为一个有趣的一片。

11年前,vanjos72在Reddit上描述了什么HECALL一个思想实验:如果你被锁定在带有IBM PC的房间里,没有操作系统?什么是软件&#39的最小量; D需要从举起恢复舒适?

正如它所发生的那样,我最近发现自己有丰富的空闲时间,所以我决定使这个不仅仅是一个思想的实验。唉,我的计算机没有配备前面板开关,所以一些软件需要在电脑上存在......

绝对最小的选项将是一个接受键盘输入的简单程序,然后跳转到它。由于TheBIOS中的键盘输入例程实现ALT + NUMPAD转义代码,因此您甚至需要编写任何基础译码代码。 2此外,循环甚至需要一个endcondition - 刚刚向后写入缓冲区,直到您运行到现有代码并覆盖跳转目标。这种方法仅占14个字节:

6A00按下WORD 0 07 POP ES FD STD BF1E7C MOV DI,缓冲+ 16;调整味道。谨防围栏。 INPUT_LOOP:B400 MOV AH,0 CD16 INT 0x16 AA STOSB EBF9 JMP SHORT INPUT_LOOP缓冲区:

但是,我在任何地方都没有找到进入代码的前景。我决定,由于BIOS无论如何,BIOS加载整个扇区,适合Bootsector的AnyBootstrap种子是公平的游戏。 3显然,人们希望最大化所选程序的效用。我们可以适用于510字节的大多数动力是什么?

许多有趣的部门大小的计划是由OSCAR TOLEDO编写的。这是许多游戏,例如厄运射线游戏或国际象棋AI,以及基本的基本解释器,但最多可能是我们的usecase最相关的游戏是bootos:

BOOTOS是一个单片操作系统,适合一个引导扇区。它'加载,执行和保存程序。还保留文件系统。

它通过中断接口公开其文件系统例程,并包括Abuiltin命令,允许通过在其hexdump中键入创建文件。非常整洁,但显然主要是在其他扇区规模的中编程之间作为多路复用器。我想寻求的是一种最小化in手中的机器代码的解决方案。理想情况下,它将是一个编程语言,武器,与基本不同,可以在运行时扩展。如果你' ve阅读了这个帖子的标题,你已经知道我解决了什么 - 事实证明,它' s possibleto在一个靴子中赤裸上身。您可以在GitHub上看到MiniforthRepository中的代码,但我将在此处包含大部分内容。

整个迫切需要,此时,504个字节。正如您所希望的那样,发育过程涉及到百分比储蓄零件的永久监视。但是,当我发布了我的想法是非常密切的代码时,伊利亚库尔德鲁克夫夫人来到并设法找到了24个Bytesto!我迅速将这一保存的空间迅速重新投资于新功能。

是基于堆栈的语言。例如,数字将推动其值Ontothe堆栈,而+ Word将弹出两个数字并推动其总和。 Acommon调试实用程序,但其中不包含在Miniforth中的是.s Word,它打印堆栈的内容。

这定义了双倍的单词,它与DUP +相同。顺便说一句,是堆栈的操纵词。它重复堆栈上的TopElement:

这基本上是整个语言。有一些标准的设施,但我们不需要与那些正常的人关心自己,因为它们可以在Miniforth之后建造。

要谈谈效果,请在堆栈的状态下,我们使用符号:

在-S中的列表 - 是输入,其中包含堆栈的顶部last.After-,我们列出了同一堆栈深度的输出。这让我们翻译了一个单词的共同方面。

虽然一些第四种系统确实包括完全吹,优化编译器类似弄脏一个' D以典型的编程语言看,有一个简单的策略。毕竟,一切顺义单词可以做的是执行其他单词,所以呼叫指令的序列非常接近:

但是,这与返回堆栈的硬件x86堆栈连接起来,使Uhandroll用于实际用户级堆栈的单独堆栈(称为参数堆栈)。访问参数堆栈更常见,我们' dlike使用推送和流行指令,而是类似于呼叫的载体活动。首先,让' s只是存储一张指针拖车列表:

这方面的生活方式是每个原始词从内存中获取下一个单词的地址,并跳转到它。对此序列的指针保持在SI中,因此LODSW指令允许易于处理此列表:

DUP:POP斧推动斧推动斧头LODSW JMP AX PLUS:POP AX POP BX添加AX,BX推轴LODSW JMP AX

顺便问,这种机制被称为线程代码。与康乃馨不相关。

但是,如果一个编译的单词呼叫另一个词,会发生什么?这是它堆栈进来的地方。使用本机指针的BP寄存器可能会自然。但是,在16位x86中,实际上存在一个[bp]寻址码。您最接近的是[BP + IMM8],这意味着在BP中访问POMMEMORY浪费一个字节以指定您不想要偏移量。这是为什么我使用返回堆栈的DI寄存器。总的来说,这个选择4个字节。

无论如何,这里是返回堆栈如何用于处理调用彼此的编译单词。推动返回堆栈是很好的,因为它只是stosw教学。

双倍:呼叫Docol DW DUP DW Plus DW ExitDocol:; &#34短暂;冒号字和#34; XCHG AX,SI;在这里用作“Mov Ax,Si`”,但互换;斧头只有一个字节,而`mov`s是两个字节的stosw pop si;抓住由`call` nextexit推出的指针:dec di dec di mov si,[di]下一个

这几乎是Miniforth使用的执行策略,具有一个简单但重要的改进 - 堆栈顶部的值存储在BX寄存器中。这允许在许多原语中跳过推动和流行:

但是,一个案例仍然是未解决的。如果一个单词包含一个数字,例如:double 2 *;?这是通过点亮处理的,这将取出引出指针流的Littleal:

双倍:呼叫DOCOL DW LIT,2 DW Mult DW ExitLit:推送BX Lodsw Xchg Bx,AX接下来

第四,需要一种方法来定位用户类型的单词的实现。这是字典的作用。我使用类似于许多其他人的结构 - SMACE - 单独链接的单词标题列表,直接预先预先预期每个单词的代码。出于传统,列表的头部是keptin一个最新的变量。

如果单词标记为立即,它将立即执行,即使是我们' RE目前编译一个定义。例如,这用于实施;。如果一个单词被标记为隐藏,则在搜索目的时被忽略它。除了被用作基本的封装机制外,可以使用该方法来实现传统的语义,其中单词定义扫描是指当您希望当前正在编译的定义时使用相同名称(和重复使用的前一词)。但是,在终止的开发结束时,我删除了实际从DefaultImpling的代码:和;。

当解压缩器和其斑载荷必须仅适用于512字节时,通常不值得使用压缩。但是,在第四种实施情况下,' s重复的常见是下一个的实现。

我们可以通过替换这些与共享副本来替换这些字节来保存一些字节。但是,短跳仍然需要两个字节 - 而不是显着的保存。正如ITTURNS OUT,一个专用压缩方案只能处理这个重复型Pattern是值得的,只要您将其与以下观察结合起来,可预测的。

我选择实现一个压缩方案,其中每个0xFF字节是替换的,然后是基于0xFF字节的概要计算的链接字段。当我介绍时,此策略保存了19个字节。 4.

起初,我用了一个0x90字节 - 毕竟,它的'是nop的操作码,我' m绝对不会使用。但是,字节仍然可以发生指令的Intehiate字节。它起初并不是一个问题,但代码在内存中转移,各种地址和偏移量变得0x90,通常足以是一种滋扰。 0xFF似乎有这个问题。

要创建一个链接,我们将最新的值复制到解压缩器输出,Andupdate最新点指向WORD WE'刚才写的。这可以以Avery Comply的指令序列完成,但它仍然需要足够的字节,即ITIS值得将其作为子程序因子传输 - 它也被以下内部用于:,它在运行时创建字典条目。

;在di创建字典链接列表链接。 Makelink:MOV AX,DI XCHG [最新],AX;斧头现在在旧的入口点,而不是;新的最新和di点。 Stosw Ret.

Decompressor用于利用一个有趣的技巧,而不是ashort前进跳转,opcode被放置,所以它需要跳过的指令。也就是说,而不是

3c db 0x3c;通过将其Opcode与Al .write进行比较:AA STOSB来跳过下面的STOSB

因此,如果某些其他代码跳转到.write,stosb执行,但是该软脚下只是cmp al,0xaa。起初,我想到了CMP alintruction,而是将一个转变为一次性寄存器。这是由于我无法实际选择可以安全地覆盖的寄存器的回收。

伊利亚·库尔德鲁科夫然后展示了相同的经历,可以实现这种"魔法"允许类似的修改,以消除此技巧的另一个发生。这本质是尝试跳过STOSB,我们在ThecodePaths分支机构之前无条件地执行它,然后在必要时基本上撤消Dec DiD:

special_byte equ 0xff mov si,compressdata mov di,compresstbegin mov cx,compress_size.decompress:lodsb stosb cmp al,special_byte jnz short .not_special dec di mov ax,0xffad; Lodsw / JMP AX STOSW MOV AL,0xE0 STOSB调用makelink.not_special:循环.decompress

实际上产生压缩流更涉及。因为我想要将压缩和未压缩的部分跳转到工作,所以汇编程序需要拍摄它正在将其实际运行的位置写入代码。我首次尝试通过调整每个特殊特性后的组织,九个幸福,Yasm ndn' t那样。

显然,需要单独的后处理步骤。我写了一个宏向垫片字节的解压缩器将插入:

这具有允许简单的自动化方法来验证事故中没有滑动的特殊方法。

我仍然不得不为压缩数据分配空间。我选择关注的布局:

在此之后立即分配解压缩缓冲区,该缓冲缓冲区是YASM输出目标内容的位置。

为实现这一目标,我需要确切地知道需要为压缩数据分配多少空间。首先,我计算在Compression_sentinel宏中介入计数器保存的确切字节数:

special_byte = B' \ xff' sentinel = special_byte + b' \ xef \ xbe \ xad \ xde'打开(' raw.bin'' rb')作为f:data = f.read()output_offset = data.index(b' \ xcc' * 20)Chunks = data [output_offset:]。lstrip(b' \ xcc').split(sentinel)ssuert special_byte不在块[0]压缩= bytearray(块[0])块块[1:]:assert special_byte不在compress.extend(special_byte)comprettle.extend(chunk)#.确保准确地为压缩数据分配了正确的空间量#。断言B' \ xcc' * len(压缩)在数据中断言B' \ xcc' *(len(压缩)+ 1)不在dataOutput = data [:output_offset] +压缩打印(len(输出),'字节使用')输出+ = b' \ x00' *(510 - Len(输出))输出+ = B' \ x55 \ xaa'打开(' boot.bin',' wb')作为f:f.write(产出)

相同的脚本还生成扩展磁盘映像,其中包含块1中的SomesMoke-Testing代码:

输出+ = B' \ x00' * 512输出+ =打开(' test.fth',' rb').read()。替换(b' \ n',b&#39 ;')输出+ = B' ' *(2048 - Len(输出))与开放(' test.img'' wb')作为f:f.write(输出)

Compression_sentinel最常被Defcode宏用于原始词的Createsthe字典条目。它需要一个标签(然后可以跳转到某些单词的实现),单词作为字符串的名称,以及可选的,要在长度字段中or的一些标志:

; defcode plus," +&#34 ;; defcode semi,&#34 ;;",f_immediate%宏defcode 2 - 3 0 compression_sentinel%strlen nameLength%2 db%3 | NameLength,%2%1:%EndMacro

defcode plus," +" POP AX添加BX,AX DEFCODE减去," - " POP轴SUB AX,BX XCHG BX,AX DEFCODE PEEK," @" ; ......

但是,DOCOL,退出和点亮也适用于其下一步的压缩机制。由于链接字段仍然写出,因此基本上创建了CreatesBogus字典条目。幸运的是,退出的第一个操作码和点亮有f_橡钩设置,所以这不是问题:

CompressageBegin:Docol:Xchg Ax,Si Stosw Pop Si;抓住由`call` compression_sentinellit推动的指针:推送bx lodsw xchg bx,ax compression_sentinelexit:dec di di mov si,[di] defcode plus," +" ; ......

这就是为什么Miniforth将其大部分变量存储在Ifstructions的直接字段中。当然,这意味着这些变量的地址将在代码的每一个编辑中都会加速,这是有问题的,因为我们将希望在第四代码中访问这些变量。公开变量的典型方式创建推动其地址的单词。然而,这与我们的制约件有用过于昂贵。我解决了什么是将地址推到Stackat启动上。通过将堆栈的初始内容简单地确定为数据,可以为每个地址仅使用2个字节来完成2个字节:

ORG 0x7C00 JMP 0:StartStack:DW在此DW Base DW状态DW最新版本:; ... mov sp,堆栈

即使在堆栈中需要被推到堆栈中,如果需要初始化变量 - 初始化变量的最佳方式,即使需要初始化变量,那么初始化变量的最佳方式并将其分配在BootSector和DW中的最佳方式在那里的价值,它恰好介于堆栈数据,并保持更短的指令编码的优势。

启动后完成的第一件事正在设置段寄存器和击字机。方向标志也被清除,使字符串指令在正确的方向上工作。

JMP 0:开始; ......开始:推动CS推送CS推送CS POP DS POP ES POP SS MOV SP,堆栈CLD

这个代码有两个值得注意的事情。首先,段寄存器通过堆栈拍摄。这是一个字节保存的技巧i'从bootbasic拾取 - 它允许必须将通用寄存器初始化为零:

31C0 XOR AX,AX;通过AX-8 BYTES8ED8 MOV DS,AX 8EC0 MOVE ES,AX 8ED0 MOV SS,AX 0E推动CS;通过堆栈 - 6 Bytes0e推送CS 0E推送CS 1F POP DS 07 POP ES 17 POP SS

其次,有人会认为,虽然堆栈正在被牢固,但发生一个小的raceCondition窗口 - 如果Pop SS和MOV SP之间发生中断,则可以随之而来,如果SP的先前值在内存中的不幸位置,则可以随之而来。 ,我可以越过我的手指,希望这不发生这种情况,如果在CLI / STI对中包装的2Bytes太多了。但是,由于X86架构的晦涩角落,这是不需要这种权衡。引用X86软件开发人员的第2B卷' S手册:

使用POP指令5加载SS寄存器抑制或禁止某些调试异常,并禁止以下指令边界上的中断。 (禁止在交付异常或执行下一个指令后结束。)此行为允许将堆栈指针加载到ESP寄存器中,并在可以传递事件之前将其加载到ESP寄存器中。

在分段后,设置堆栈和方向标志,解压缩器ISRAN。至关重要的是,它不使用DL寄存器,其中包含我们启动的BIOS DiskNumber。然后将其戳进入负载(其在压缩段中),并按用户代码被推到堆叠上,以供Loxuse:

此时,我们到达外部解释器 - 第四个系统的一部分处理用户输入。姓名"外翻员"将其与内部解释器区分开,这是协调所定义的单词的executionWithIn的组件,并且由下一个,docol,退出和点燃组成。

通常,第一个将公开其外解释器ASWORD在字典中的构建块,例如

在Miniforth,根本没有注意这种做法。字典标题字节,因此只通过堆栈通信。事实上,Wordand>数字被融合到一个例程中,这两个例程都是 - 该方法,可以共享循环,保存字节。

这种单片架构也让我们决定为堆栈顶部和返回堆栈指针的顶部并决定BX和DI,而外部解释器正在执行。这显着有助于在这些相对复杂的系统中的注册过程中的透析性。在跳转到一个单词之前,同学设置并在返回后保存。

初始化完成后,代码通过读取线路,以便在从键盘中读取输入行。我们稍后还会跳回回路,当前输入线耗尽时。输入缓冲区在BDA之后直接在0x500。虽然惯用字符串格式为一个单独的长度字段,但此缓冲区为null终止,因为解析时是Easierto句柄。指向InputPTR中输入的输入的未降级片段,这是唯一不使用撰写撰写的变量,因为它不需要明确初始化 - 自然地写入读取之前。

INPUTBUF EQU 0x500 INPUTPTR EQU 0xA02; DW Readline:MOV DI,INPUTBUF MOV [INPUTPTR],DI。循环:MOV AH,0 INT 0x16 CMP al,0x0d je short。输入stosb cmp a

......