程序集“Hello world”执行文件小于 20 字节

Phi*_*rev -1 assembly dos code-size x86-16

下面的代码占用 20 个字节。然而,有一种方法可以通过中断使其变得更小。如何?

\n
A\nMOV AH,9\nMOV DX,108\nINT 21\nRET\nDB 'HELLO WORLD$'\n\nR CX\n14\nN MYHELLO.COM\nW \n
Run Code Online (Sandbox Code Playgroud)\n

Pet*_*des 8

打印一条较短的消息,例如db \'hi$\':P
\n或者按照 Vitsoft 的建议,将字符串作为 arg,就像 Unix 一样echo命令的参数,这样它就不会占用程序中的空间。

\n

或者,如果您不关心可移植性或仅依赖文档化的保证,则依赖于程序启动时某些 DOS 版本留在寄存器中的某些值。xchg ax, bp(例如,通过使用而不是保存一个字节mov ah, 9来保存一个字节。)

\n

我不知道你所说的“有一种方法可以通过中断使其变得更小”是什么意思。我不认为这是真的,但你将其陈述为事实,不问。如果您不知道如何实现,那么您的来源是什么,可以通过中断使其变得更小使其更小,同时仍然打印相同的输出字符串?

\n
\n

几乎可以肯定int 21h/ah=9是打印多个字节文本的最紧凑的方式。您需要以某种方式获得 AH=9 和 DX=指针。不依赖寄存器或内存中方便位置的现有字节(某些 DOS 版本可能会碰巧留下这些字节),这需要 2 字节mov ah,9和 3 字节mov dx, imm16

\n

您可以使用 来设置 DX=0 xor dx,dx,但即使文件的开头位置也是100h.com 程序中的偏移量。(这意味着让 ASCII 文本作为机器代码执行,而不需要jmp覆盖它!)

\n

call label//db "text"label: pop dx指针放入 DX 总共需要 4 个字节。

\n
\n

使用某些已知 DOS 版本留下的未初始化寄存器值。

\n

http://www.fysnet.net/yourhelp.htm链接自Codegolf.SE 上 x86/x64 机器代码打高尔夫球的提示,发现了一系列 DOS 版本的启动寄存器值。AFAIK,这不是标准化的,所以这只是一个偶然的工作。后来的 FreeDOS 版本变得越来越类似于 MS-DOS,因为大概是一些现有的软件是为了依赖它而编写的,有意的、偶然的,或者因为有些人不知道“在我的机器上运行”是\与“保证面向未来且可移植”不是一回事,但其他各种 DOS 版本有所不同。 这不是您在生产用途中应该依赖的东西,只有愚蠢的计算机技巧,例如代码高尔夫或“演示场景””程序。

\n

大多数 DOS 版本都会SI=0100h在程序启动时退出。因此,如果我们可以将字符串放在那里而不弄乱机器(或 SI),我们可以mov dx, si(2 个字节)而不是mov dx, 108h107h(3 个字节)。但是lea dx, [si+8]是 3 个字节(opcode + modrm + disp8),所以除非我们让字符串执行,否则不会保存。

\n

或者甚至更好,如果堆栈上有一些东西可以用来做什么pop dx?或者popa,如果您非常幸运也可以设置AX=09xx. 但我不知道是否有任何 DOS 版本碰巧在堆栈上留下了任何已知的东西,除了指向指令int 20h或其他东西的“返回”地址。弹出该命令意味着使用int 20h而不是手动退出ret,这会多花费 1 个字节。

\n

实际上,xchg ax, reg只有1 个字节,所以如果有任何寄存器以 开头09xx,我们就可以使用它。 MS-DOS 4.0 及更高版本、FreeDOS 1.0 以及 IBM PC-DOS 4.0 及更高版本均以BP=09xxh. 因此,我们可以使用xchg ax, bp, 与 DX 的任何内容分开,在 AH init 中保存一个字节。(有趣的事实:这就是90hNOP 编码的来源:它只是 , 的一个特例xchg ax, ax,直到 x86-64 必须将其记录为实际的 NOP,因为在 64 位模式下它不会对 EAX 进行零扩展像 RAX 一样xchg eax, eax)。

\n
\n

让文本作为代码执行

\n

为了节省更多字节,我认为缩短这个时间的唯一希望是使用恰好解码为指令的文本,让执行从另一侧出来,而不会弄乱 SI,因此您可以将其放在执行路径中。但最多只能节省 1 个字节,除非文本还包含执行任何有用操作的指令。

\n

但你的消息对此不起作用。我检查了它的 3 个大写字母如何反汇编,用于nasm制作平面二进制文件并ndisasm -b16反汇编结果。(我使用align 16这样我可以找到边界,并给出一种 nop 幻灯片,因此如果字符串的最后一个字节不是指令的最后一个字节,它将消耗一些填充而不是更改下一个字符串的解码.) 我没有 DOS 或 debug.exe,因此我h在十六进制数字上使用尾随语法。在 DOS 调试中,所有数字都是隐式的十六进制,这就是为什么int 21数字是正确的。我也没有测试过这些,我对过时的 16 位东西不感兴趣,但是 x86 机器代码恶作剧很有趣。尽管真正的挑战问题在 Stack Overflow 上是偏离主题的,但这种单语言优化问题比https://codegolf.stackexchange.com/更适合这里。

\n
; just to look at disassembly, to see if there\'s any hope of letting them execute\nDB \'HELLO WORLD$\'\nalign 16\ndb \'Hello World$\'\nalign 16\ndb \'hello world$\'\n
Run Code Online (Sandbox Code Playgroud)\n
;; DB \'HELLO WORLD$\'\n00000000  48                dec ax        ; early-alphabet upper-case \n00000001  45                inc bp        ; is all single-byte inc/dec\n00000002  4C                dec sp        ; the same opcodes x86-64 repurposed as REX\n00000003  4C                dec sp         ;; Modified SP breaks RET\n00000004  4F                dec di\n00000005  20574F            and [bx+0x4f],dl   ;; step on part of the PSP\n00000008  52                push dx       ; \'R\'  also modifies SP\n00000009  4C                dec sp        ; \'L\'\n0000000A  44                inc sp        ; \'D\'   cancel each other\'s effect on SP\n0000000B  2490              and al,0x90\n0000000D  90                nop\n0000000E  90                nop\n0000000F  90                nop\n
Run Code Online (Sandbox Code Playgroud)\n

漂亮。所以它实际上不会对机器做任何致命的事情(取决于 BX 指向的位置)。DOS 版本中的 BX=0 给出了我们想要的 BP 和 SI 值,这会屏蔽 中的一些位[ds: 4f],这些位位于PSP(程序段前缀)的保留部分中。如果在我们退出之前或在 DOS 退出调用期间没有其他东西查看那里,这可能没问题。

\n

但请注意and al, 0x90结尾:字符串本身以24h(又名 )结尾\'$\',作为指令的开头。这是 的操作码and al, imm8,因此它会消耗下一个指令的 1 个字节作为该指令的一部分。

\n

因此,在放置有用指令的开头之前,您需要在其后添加一个字节的填充。这会破坏 1 字节大小的节省。

\n

它扰乱了 SP,所以我们不能ret再这么做了。我们需要退出,除非你能用什么东西int 20h来保释。CC int3不确定 DOS 对该异常执行什么操作。

\n
;; 20 bytes, cancelling out saving from  xchg ax,bp\nDB \'HELLO WORLD$\'  ; executes as machine code without doing anything too bad\nnop                ; but this is needed.  It\'s actually consumed as an immediate for 24h\nmov  dx, si\nxchg ax, bp        ; AH=09  on some DOS versions, in 1 byte instead of 2.\nint  21\nint  20            ; larger than ret, making this a net loss.\n
Run Code Online (Sandbox Code Playgroud)\n

其他大写也是一个问题,涉及\'l\'as 6C insbIO 指令(https://www.felixcloutier.com/x86/ins:insb:insw:insd),以及类似的\'o\'as 6F outsw

\n
;; db \'Hello World$\'\n00000010  48                dec ax\n00000011  656C              gs insb       ; big problem, IO could crash the machine\n00000013  6C                insb          ; using port=DX, data from [DS:SI]\n00000014  6F                outsw\n00000015  20576F            and [bx+0x6f],dl\n00000018  726C              jc 0x86       ; conditional branch, but AND always clears CF so this will be not-taken\n0000001A  642490            fs and al,0x90    ; FS prefix was new with 386\n0000001D  90                nop               ; the align 16 padding, including previous immediate\n0000001E  90                nop\n0000001F  90                nop\n
Run Code Online (Sandbox Code Playgroud)\n
;; db \'hello world$\'\n00000020  68656C            push word 0x6c65\n00000023  6C                insb\n00000024  6F                outsw\n00000025  20776F            and [bx+0x6f],dh\n00000028  726C              jc 0x96\n0000002A  64                fs\n0000002B  24                db 0x24\n
Run Code Online (Sandbox Code Playgroud)\n

24h 字节再次悬空,作为指令的开始。如果后面有一个nop,ndisasm 会像fs and al, 0x90前一个块一样对其进行解码。

\n

看来ello是IO指令有问题。

\n
\n

我们需要字符串的倒数第二个字节是其他内容,例如 2 字节指令的开头,最好是类似3C ib cmp al, imm8. 那是 ASCII <

\n

我们需要它不要搞乱 SP。如果它递减,我们需要递增或弹出到虚拟寄存器中,因此它再次指向返回地址。

\n

18字节版本,打印相同长度的修改字符串

\n
;; 18 bytes\nDB \'HELLO_WOIY<$\'  ; executes as machine code, returning SP to original position without overwriting return address\n\nmov  dx, si    ; mov dx,0100h MS-DOS (all versions), FreeDOS 1.0, many other DOSes\nxchg ax, bp    ; mov ah,9     MS-DOS 4.0 and later, and FreeDOS 1.0\nint  21h\nret\n
Run Code Online (Sandbox Code Playgroud)\n

反汇编为

\n
00000000  48        dec ax     ; \'H\'\n00000001  45        inc bp     ; \'E\' affects BP, which we want to use later\n00000002  4C        dec sp     ; \'L\'\n00000003  4C        dec sp     ; \'L\'  ; SP offset by -2\n00000004  4F        dec di     ; \'O\'\n00000005  5F        pop di     ; \'_\'  ; restore SP\n00000006  57        push di    ; \'W\'  ; SP offset by -2\n00000007  4F        dec di     ; \'O\'\n00000008  49        dec cx     ; \'I\'\n00000009  59        pop cx     ; \'Y\'  ; restore SP\n0000000A  3C24      cmp al,0x24   ; \'<\' consumes the \'$\' as an imm8\n\n0000000C  89F2      mov dx,si      ; instructions from the source, as written.\n0000000E  95        xchg ax,bp\n0000000F  CD21      int 0x21\n00000011  C3        ret\n
Run Code Online (Sandbox Code Playgroud)\n

这会inc bp修改我们所依赖的初始值的寄存器之一。但除非从低字节FF开始,否则它不会换行并更改09高半部分。具体来说,在 FreeDOS 1.0 上,初始 BP 值为091Eh。在 Win9x 的 MS-DOS 版本上,它是0912h. 在来自 Win-NT 派生版本的 DOS 上,它是09xxh,这并不排除09FFh

\n

我必须非常认真地破坏字符串来平衡堆栈,并使用偶数个dec sp指令和弹出来平衡它。1 字节58+ rw pop reg包括一些后期的大写字母。

\n

还必须避免add [si], sp类似的事情,因为初始 SI 指向我们的字符串。(最初的 BX 通常不会。)

\n
\n

HELLO_WOLD<有太多次推动,但该\'LD\'部分抵消了,dec sp/ inc sp。按照这个顺序,它不会暂时将部分返回地址留在 SP 以下,因为中断或调试器可能会破坏它。

\n

如果您真的想认真想出一个在视觉上看起来更像 的字符串HELLO WORLD,您需要制作一个 ASCII 字符和相应指令的表。许多大写 ASCII 字符是单字节指令的操作码,即inc/decpush/ pop

\n

您可以使用像 NASM 这样带有%rep// %assign i i+1/块db i的良好汇编器%endrep,并通过反汇编器运行它。或者编写一个程序来输出二进制文件并反汇编它。

\n

或者查看http://ref.x86asm.net/coder32.html并将其与https://asciitable.com/进行匹配

\n
\n

我们可以使用文本作为机器代码来至少退出程序吗?不太可能;retis c3int 20his CD 20,因此这两个操作码都不会出现在 ASCII 文本中。

\n

AFAIK,您无法尾部调用 DOS 例程以使其打印然后退出,而无需ret在您自己的代码中使用 或等效项。或者,如果可以的话,它将是一个 3 字节jmp rel16,或者更可能是一个远 jmp,如果我们谈论的是形式,那么mov ah, 9/会占用比 2+2 更多的字节。int 21hjmp ptr16:16

\n