; 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退出调用期间没有其他东西出现,这可能是好的。
;; 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.
;; 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
; 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:
剩下的一个重要技巧是将代码完全嵌入dos.exe头中,看起来代码很好地适合连续的10个字节的.exe头字段minalloc,maxalloc,ss,sp,checksum,和实际使用的代码字节不会造成问题,当DOS解释这些头域时,当加载程序。查看源代码中的注解以获得更多细节。一些更多信息:我们不需要一个大的minalloc这将增加程序的内存需求。maxalloc无关紧要(但是为了礼貌起见,至少要让它和minalloc一样大)我们需要足够大的ss和sp,这样堆栈就不会覆盖代码和消息(例如初始值应该指向end之上至少32个字节),并且minalloc必须足够大,以便堆栈也适合。checksum不重要,旧的和新的DOS版本(以PC DOS 2.00为最老版本测试)都忽略它。
3条答案
按热度按时间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, 108h
或107h
(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/上更适合很好。所以它实际上不会对机器做任何致命的事情(取决于BX所指向的位置)。在给出我们想要的BP和SI值的DOS版本中,BX = 0会屏蔽掉
[ds: 4f]
中的一些位,这是在reserved part of the PSP(程序段前缀)中。如果在我们退出之前或在DOS退出调用期间没有其他东西出现,这可能是好的。但请注意
and al, 0x90
的结尾:字符串本身以24h
结束,也就是'$'
,作为一条指令的开始,这是and al, imm8
的操作码,所以它会消耗1字节的下一条指令。因此,在放置一条有用指令的开始之前,你需要在它后面填充一个字节,这将扼杀1字节的节省。
ret
**。我们需要int 20h
退出,除非你可以用CC int3
或其他东西来退出。不确定DOS会对这个异常做什么。其他的大写是一个问题,包括
'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指令。3C ib cmp al, imm8
,也就是ASCII码<
。我们需要它不要弄乱SP,如果它递减,我们需要递增或弹出到虚拟寄存器,所以它再次指向返回地址。
18字节版本,打印相同长度的修改字符串
拆卸为
这个函数执行
inc bp
,修改了一个寄存器的初始值。但是除非低字节是FF
,否则它不会在高半部分 Package 和改变09
。特别是在FreeDOS 1.0上,初始BP值是091Eh
。在Win9x的MS-DOS版本上,它是0912h
。在Windows NT派生版本的DOS上,它是09xxh
,但不排除09FFh
为了平衡堆栈,我不得不非常认真地修改字符串,使用偶数个
dec sp
指令和pop来平衡堆栈,1字节的58+ rw
pop reg
包含了一些晚期的大写字母。还必须避免
add [si], sp
或类似的东西,因为初始的SI指向我们的字符串(初始的BX通常不指向)。HELLO_WOLD<
有一个太多的压入,但是'LD'
部分抵消了,dec sp
/inc sp
。按照这个顺序,这样它就不会暂时把返回地址的一部分留在SP下面,在那里中断或调试器可能会将它清除。如果你真的想让字符串看起来更像
HELLO WORLD
,那么你需要制作一个ASCII字符和对应指令的表格,很多大写的ASCII字符都是单字节指令的操作码,比如inc
/dec
或push
/pop
。你可以使用一个好的汇编器,比如NASM,它有一个
%rep
/%assign i i+1
/db i
/%endrep
块,然后通过一个反汇编器运行它,或者编写一个程序输出一个二进制文件并反汇编它。或者查看http://ref.x86asm.net/coder32.html并将其与https://asciitable.com/匹配
我们可以使用文本作为机器代码,至少退出程序?不太可能;
ret
是c3
,int 20h
是CD 20
,因此这两个操作码都不会出现在ASCII文本中。AFAIK,你不可能尾调用DOS例程,让它打印然后退出,而不需要一个
ret
或你自己代码中的等价物。或者如果你可以,它将是一个3字节的jmp rel16
,或者更可能是一个远jmp,如果我们谈论的是jmp ptr16:16
,它将占用比mov ah, 9
/int 21h
多的2 + 2字节。wnrlj8wa2#
如果不介意提供文本作为参数,可以将其缩短为8个字节:
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头。
在https://github.com/pts/mininasm/blob/master/demo/hello/helloe.nasm查看更多评论
以下是其中涉及的技巧:
mov ax, @data
后跟mov ds, ax
来初始化 ds。(我们需要一个正确的 ds 来打印一条带有 ah == 9和int 0x21
的消息。)这条消息相当长,而且它还为 @data 使用了一个4字节的重定位槽。避免这种情况的一种方法是使.exe头中的ss
为0,然后push ss
和pop ds
也可以工作,并且不使用任何重定位。不要初始化 ds,利用DOS将其初始化为PSP段的事实(PSP段的长度为0x 100字节,并且正好位于内存中程序映像的前面),因此在打印时只需将0x 100添加到 dx。cs
设置为0xfff 0,这将把 cs 初始化为PSP段。如果 cs 的值是PSP段,则可以使用int 0x20
退出到DOS,退出代码为0,而不是较长的mov ax, 0x4c00
后跟int 0x21
。这将节省3个字节。xchg bp, ax
将 ah 设置为9的技巧在MS-DOS 4. 00之前不起作用,所以我的解决方案没有使用它。通常的mov ah, 9
比mov ah, 9
长1个字节。我的解决方案使用mov ax, 0x903
,它甚至比mov ax, 0x903
长1个字节,看起来很浪费(因为没有使用 al 中的3),但是重叠的DOS .exe头文件中有空间容纳额外的字节,并且较小的值3减少了程序的内存使用,因为mov ax, 0x903
指令与.exe标头中的 minalloc 重叠。relocpos
和noverlay
.exe头字段在.exe头的末尾被DOS忽略,所以我的解决方案用消息.relocpos
将它们重叠,仅当.exe程序包含重定位时才使用(我的不包含),并且noverlay
在加载(和执行).exe文件时被DOS完全忽略。minalloc
,maxalloc
,ss
,sp
,checksum
,和实际使用的代码字节不会造成问题,当DOS解释这些头域时,当加载程序。查看源代码中的注解以获得更多细节。一些更多信息:我们不需要一个大的minalloc
这将增加程序的内存需求。maxalloc
无关紧要(但是为了礼貌起见,至少要让它和minalloc
一样大)我们需要足够大的ss
和sp
,这样堆栈就不会覆盖代码和消息(例如初始值应该指向end
之上至少32个字节),并且minalloc
必须足够大,以便堆栈也适合。checksum
不重要,旧的和新的DOS版本(以PC DOS 2.00为最老版本测试)都忽略它。ip
和cs
头字段,我们需要非常具体的值。因此,如果将代码完全嵌入到消息中,而不是完全嵌入到.exe标头中,我们无法保存更多字节。为了简单起见,也为了使消息更灵活,我决定使用后者。$
)。少于4个字节可能不起作用,因为某些DOS版本要求.exe程序文件至少为28个字节长,因为这是DOS .exe头文件的最小长度。