assembly 汇编“Hello world”执行文件小于20字节

olqngx59  于 2022-12-19  发布在  其他
关注(0)|答案(3)|浏览(114)

下面的代码占用了20个字节。然而,有一种方法可以通过中断使它更小。怎么做?

A
MOV AH,9
MOV DX,108
INT 21
RET
DB 'HELLO WORLD$'

R CX
14
N MYHELLO.COM
W
cx6n0qe3

cx6n0qe31#

打印较短的消息,如db 'hi$':P
或者像Vitsoft建议的那样,将字符串作为一个arg,就像Unix echo命令一样,这样它就不会占用程序的空间。
或者,如果你不关心可移植性或者只依赖于文档中的保证,那么就依赖于某些DOS版本在程序启动时在寄存器中留下的一些值(例如,使用xchg ax, bp而不是mov ah, 9来节省一个字节)。
我不知道你说的"* 有一种方法可以通过中断使它更小 *"是什么意思。我不认为这是真的,但你是在陈述事实,而不是在问。如果你不知道怎么做,你的来源是什么,它可以 * 通过中断 * 使它更小,同时仍然打印相同的输出字符串?
几乎可以肯定,int 21h/ah=9是打印多个字节文本的最紧凑的方式。您需要以某种方式获得AH = 9和DX =指针。如果不依赖于寄存器或内存中方便位置的现有字节(某些DOS版本可能碰巧会将这些字节留在附近),则需要2字节的mov ah,9和3字节的mov dx, imm16
你可以用xor dx,dx设置DX = 0,但在. com程序中,即使文件的开头也是偏移量100h(这意味着让ASCII文本作为机器码执行,而不加jmp!)
call label/db "text"/label: pop dx将是总共4个字节,以使指针进入DX。

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

Tips for golfing in x86/x64 machine code on codegolf.SE链接的http://www.fysnet.net/yourhelp.htm在一系列DOS版本中找到了启动寄存器值。AFAIK,这不是标准化的,所以它只是偶然发生的。FreeDOS的后期版本变得越来越类似于MS-DOS,因为可能一些现有的软件是故意或偶然地依赖它编写的,或者因为有些人不知道"在我的机器上工作"和"保证未来证明和便携"不是一回事,而是各种其他DOS版本不同。这不是你应该依赖于生产使用的东西,只有愚蠢的计算机技巧,如代码高尔夫或"demo scene"程序。
大多数DOS版本在程序启动时都会保留SI=0100h,所以如果我们可以把字符串放在那里而不弄乱机器(或SI),我们可以用mov dx, si(2字节)代替mov dx, 108h107h(3字节),但是lea dx, [si+8]是3字节(opcode + modrm + disp8),所以除非我们让字符串执行,否则不会保存。
或者更好的是,如果栈上有一些东西可以用于pop dx,或者popa,如果你非常幸运的话,也设置了AX=09xx,但是我不知道是否有DOS版本碰巧在栈上留下了任何已知的东西,而不是指向int 20h指令或其它指令的"返回"地址。弹出这将意味着手动退出int 20h,而不是ret,多花费1个字节。
实际上,xchg ax, reg就是only 1 byte,所以如果任何寄存器以09xx开头,我们都可以使用它。**MS-DOS 4.0及更高版本、FreeDOS 1.0和IBM PC-DOS 4.0及更高版本都以BP=09xxh开头。**因此,我们可以使用xchg ax, bp在AH init中节省一个字节,与DX中的任何内容分开。(有趣的事实:这就是90h NOP编码的来源:它只是xchg ax, ax一个特例,直到x86 - 64不得不将其记录为实际的NOP,因为在64位模式下,它不会像xchg eax, eax那样将EAX零扩展到RAX。

让文本作为代码执行

为了节省更多的字节,我看到的唯一希望是使用碰巧解码的文本作为指令,让执行从另一边出来,而不弄乱SI,这样你就可以把它放在执行路径中,但最多你节省了1个字节,除非文本还包含有用的指令。
但是你的信息不适用于这个,我检查了它的3个大写字母是如何反汇编的,使用nasm生成一个平面二进制,使用ndisasm -b16反汇编结果。(我使用align 16,这样我就可以找到边界,并且给出一种nop幻灯片,这样如果字符串的最后一个字节不是指令的最后一个字节,它将消耗一些填充,而不是改变下一个字符串的解码。)我没有DOS或debug.exe,所以我对十六进制数字使用尾随的-h语法。在DOS调试中,所有数字都是隐式十六进制的,这就是为什么int 21是正确的数字。我也没有测试这些,我对过时的16位代码不感兴趣,但是x86机器码的恶作剧很有趣,虽然真正的code-golf挑战问题在Stack Overflow上是离题的,但是这种单语言优化问题在这里比在https://codegolf.stackexchange.com/上更适合

; just to look at disassembly, to see if there's any hope of letting them execute
DB 'HELLO WORLD$'
align 16
db 'Hello World$'
align 16
db 'hello world$'
;; DB 'HELLO WORLD$'
00000000  48                dec ax        ; early-alphabet upper-case 
00000001  45                inc bp        ; is all single-byte inc/dec
00000002  4C                dec sp        ; the same opcodes x86-64 repurposed as REX
00000003  4C                dec sp         ;; Modified SP breaks RET
00000004  4F                dec di
00000005  20574F            and [bx+0x4f],dl   ;; step on part of the PSP
00000008  52                push dx       ; 'R'  also modifies SP
00000009  4C                dec sp        ; 'L'
0000000A  44                inc sp        ; 'D'   cancel each other's effect on SP
0000000B  2490              and al,0x90
0000000D  90                nop
0000000E  90                nop
0000000F  90                nop

很好。所以它实际上不会对机器做任何致命的事情(取决于BX所指向的位置)。在给出我们想要的BP和SI值的DOS版本中,BX = 0会屏蔽掉[ds: 4f]中的一些位,这是在reserved part of the PSP(程序段前缀)中。如果在我们退出之前或在DOS退出调用期间没有其他东西出现,这可能是好的。

但请注意and al, 0x90的结尾:字符串本身以24h结束,也就是'$',作为一条指令的开始,这是and al, imm8的操作码,所以它会消耗1字节的下一条指令。
因此,在放置一条有用指令的开始之前,你需要在它后面填充一个字节,这将扼杀1字节的节省。

    • 而且它会扰乱SP,所以我们不能再运行ret**。我们需要int 20h退出,除非你可以用CC int3或其他东西来退出。不确定DOS会对这个异常做什么。
;; 20 bytes, cancelling out saving from  xchg ax,bp
DB 'HELLO WORLD$'  ; executes as machine code without doing anything too bad
nop                ; but this is needed.  It's actually consumed as an immediate for 24h
mov  dx, si
xchg ax, bp        ; AH=09  on some DOS versions, in 1 byte instead of 2.
int  21
int  20            ; larger than ret, making this a net loss.

其他的大写是一个问题,包括'l'作为6C insb IO指令(https://www.felixcloutier.com/x86/ins:insb:insw:insd),类似地'o'作为6F outsw
一个三个三个一个
第24h字节仍然是悬空的,作为指令的开始,如果在它后面有一个nop,ndisasm会像前面的代码块一样将它解码为fs and al, 0x90
看来ello出了问题,有IO指令。

    • 我们需要字符串的倒数第二个字节是其他的东西**,比如一个2字节指令的开头,理想情况下是3C ib cmp al, imm8,也就是ASCII码<

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

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

;; 18 bytes
DB 'HELLO_WOIY<$'  ; executes as machine code, returning SP to original position without overwriting return address

mov  dx, si    ; mov dx,0100h MS-DOS (all versions), FreeDOS 1.0, many other DOSes
xchg ax, bp    ; mov ah,9     MS-DOS 4.0 and later, and FreeDOS 1.0
int  21h
ret

拆卸为

00000000  48        dec ax     ; 'H'
00000001  45        inc bp     ; 'E' affects BP, which we want to use later
00000002  4C        dec sp     ; 'L'
00000003  4C        dec sp     ; 'L'  ; SP offset by -2
00000004  4F        dec di     ; 'O'
00000005  5F        pop di     ; '_'  ; restore SP
00000006  57        push di    ; 'W'  ; SP offset by -2
00000007  4F        dec di     ; 'O'
00000008  49        dec cx     ; 'I'
00000009  59        pop cx     ; 'Y'  ; restore SP
0000000A  3C24      cmp al,0x24   ; '<' consumes the '$' as an imm8

0000000C  89F2      mov dx,si      ; instructions from the source, as written.
0000000E  95        xchg ax,bp
0000000F  CD21      int 0x21
00000011  C3        ret

这个函数执行inc bp,修改了一个寄存器的初始值。但是除非低字节是FF,否则它不会在高半部分 Package 和改变09。特别是在FreeDOS 1.0上,初始BP值是091Eh。在Win9x的MS-DOS版本上,它是0912h。在Windows NT派生版本的DOS上,它是09xxh,但不排除09FFh
为了平衡堆栈,我不得不非常认真地修改字符串,使用偶数个dec sp指令和pop来平衡堆栈,1字节的58+ rwpop reg包含了一些晚期的大写字母。
还必须避免add [si], sp或类似的东西,因为初始的SI指向我们的字符串(初始的BX通常不指向)。
HELLO_WOLD<有一个太多的压入,但是'LD'部分抵消了,dec sp/inc sp。按照这个顺序,这样它就不会暂时把返回地址的一部分留在SP下面,在那里中断或调试器可能会将它清除。
如果你真的想让字符串看起来更像HELLO WORLD,那么你需要制作一个ASCII字符和对应指令的表格,很多大写的ASCII字符都是单字节指令的操作码,比如inc/decpush/pop
你可以使用一个好的汇编器,比如NASM,它有一个%rep/%assign i i+1/db i/%endrep块,然后通过一个反汇编器运行它,或者编写一个程序输出一个二进制文件并反汇编它。
或者查看http://ref.x86asm.net/coder32.html并将其与https://asciitable.com/匹配
我们可以使用文本作为机器代码,至少退出程序?不太可能; retc3int 20hCD 20,因此这两个操作码都不会出现在ASCII文本中。
AFAIK,你不可能尾调用DOS例程,让它打印然后退出,而不需要一个ret或你自己代码中的等价物。或者如果你可以,它将是一个3字节的jmp rel16,或者更可能是一个远jmp,如果我们谈论的是jmp ptr16:16,它将占用比mov ah, 9/int 21h多的2 + 2字节。

wnrlj8wa

wnrlj8wa2#

如果不介意提供文本作为参数,可以将其缩短为8个字节:

R:\>debug
-A
0DB2:0100 MOV AH,9
0DB2:0102 MOV DX,82
0DB2:0105 INT 21
0DB2:0107 RET
0DB2:0108
-R CX
CX 0000  :8
-N HELLO8.COM
-W
Writing 0008 bytes
-Q

R:\>HELLO8.COM HELLO WORLD$
HELLO WORLD
R:\>
fnx2tebb

fnx2tebb3#

FYI这是我尝试写一个简短的hello-world dos.exe程序。
我并不想让代码和消息重叠(就像@ PeterCordes在他们的DOS.com解决方案中那样),但是(正如我们稍后将看到的),这无助于减少程序的大小。
一个DOS hello-world.exe程序肯定比一个等价的DOS .com程序要长,因为DOS .exe头文件是28字节(而DOS .com头文件是0字节)。
我能够压缩到40字节,包括28字节的DOS .exe头和15字节的消息,总共43字节(!),通过使代码和消息都覆盖DOS .exe头。

; nasm -O0 -f bin -o prog.exe prog.nasm
exe:  ; DOS .exe header: http://justsolve.archiveteam.org/wiki/MS-DOS_EXE
.signature      db 'MZ'
.lastsize       dw end-exe  ; Number of bytes in the last 0x200-byte block in the .exe file. For us, total file size.
.nblocks        dw 1  ; Number of 0x200-byte blocks in .exe file (rounded up).
.nreloc         dw 0  ; No relocations.
.hdrsize        dw 0  ; Load .exe file to memory from the beginning.
..@code:
%if 1  ; Produces identical .exe output even if we change it to 0.
.minalloc:      mov ax, 0x903  ; AH := 9, AL := junk. The number 3 matters for minalloc, see below.
.ss_minus_1:    mov dx, message+(0x100-exe)  ; (0x100-exe) to make it work with any `org'.
.sp:            int 0x21  ; Print the message to stdout. https://stanislavs.org/helppc/int_21-9.html
.checksum:      int 0x20  ; Exit. Requires CS == PSP. https://stanislavs.org/helppc/int_20.html
%else
.minalloc       dw 0x03b8  ; To have enough room for the stack, we need minalloc >= ss+((sp+0xf)>>4)-0x20 == 0x315. kvikdos verifies it.
.maxalloc       dw 0xba09  ; Actual value doesn't matter.
.ss             dw 0x0118  ; Actual value doesn't matter as long as it matches minalloc. dw message
.sp             dw 0x21cd  ; Actual value doesn't matter. int 0x21
.checksum       dw 0x20cd  ; Actual value doesn't matter. int 0x20
%endif
.ip             dw ..@code+(0x100-exe)  ; Entry point offset. (0x100-exe) to make it work with any `org'.
.cs             dw 0xfff0  ; CS := PSP upon entry.
%if 0
.relocpos       dw ?  ; Doesn't matter, overlaps with 2 bytes of message: 'He'.
.noverlay       dw ?  ; Doesn't matter. overlaps with 2 bytes of message: 'll'.
; End of 0x1c-byte .exe header.
%endif
message         db 'Hello, World!', 13, 10, '$'
end:

https://github.com/pts/mininasm/blob/master/demo/hello/helloe.nasm查看更多评论
以下是其中涉及的技巧:

  • 大多数.exe程序都有mov ax, @data后跟mov ds, ax来初始化 ds。(我们需要一个正确的 ds 来打印一条带有 ah == 9和int 0x21的消息。)这条消息相当长,而且它还为 @data 使用了一个4字节的重定位槽。避免这种情况的一种方法是使.exe头中的ss为0,然后push sspop ds也可以工作,并且不使用任何重定位。不要初始化 ds,利用DOS将其初始化为PSP段的事实(PSP段的长度为0x 100字节,并且正好位于内存中程序映像的前面),因此在打印时只需将0x 100添加到 dx
  • 将.exe文件头中的cs设置为0xfff 0,这将把 cs 初始化为PSP段。如果 cs 的值是PSP段,则可以使用int 0x20退出到DOS,退出代码为0,而不是较长的mov ax, 0x4c00后跟int 0x21。这将节省3个字节。
  • 不幸的是,xchg bp, axah 设置为9的技巧在MS-DOS 4. 00之前不起作用,所以我的解决方案没有使用它。通常的mov ah, 9mov ah, 9长1个字节。我的解决方案使用mov ax, 0x903,它甚至比mov ax, 0x903长1个字节,看起来很浪费(因为没有使用 al 中的3),但是重叠的DOS .exe头文件中有空间容纳额外的字节,并且较小的值3减少了程序的内存使用,因为mov ax, 0x903指令与.exe标头中的 minalloc 重叠。
  • relocposnoverlay.exe头字段在.exe头的末尾被DOS忽略,所以我的解决方案用消息. relocpos将它们重叠,仅当.exe程序包含重定位时才使用(我的不包含),并且noverlay在加载(和执行).exe文件时被DOS完全忽略。
  • 剩下的一个重要技巧是将代码完全嵌入dos.exe头中,看起来代码很好地适合连续的10个字节的.exe头字段minallocmaxallocssspchecksum,和实际使用的代码字节不会造成问题,当DOS解释这些头域时,当加载程序。查看源代码中的注解以获得更多细节。一些更多信息:我们不需要一个大的minalloc这将增加程序的内存需求。maxalloc无关紧要(但是为了礼貌起见,至少要让它和minalloc一样大)我们需要足够大的sssp,这样堆栈就不会覆盖代码和消息(例如初始值应该指向end之上至少32个字节),并且minalloc必须足够大,以便堆栈也适合。checksum不重要,旧的和新的DOS版本(以PC DOS 2.00为最老版本测试)都忽略它。
  • 不可能将消息完整地放入DOS.exe头中,因为消息本身太长。也不可能将其重叠更多,因为这样就必须重叠ipcs头字段,我们需要非常具体的值。因此,如果将代码完全嵌入到消息中,而不是完全嵌入到.exe标头中,我们无法保存更多字节。为了简单起见,也为了使消息更灵活,我决定使用后者。
  • 要缩短.exe文件,请缩短消息(最少4个字节,包括$)。少于4个字节可能不起作用,因为某些DOS版本要求.exe程序文件至少为28个字节长,因为这是DOS .exe头文件的最小长度。

相关问题