0 起因
计算机系统导论课上老师说可以内联汇编来尽可能减小 Hello,World!的大小。
我觉得既然要做,不如就做到极致。何不试试直接写汇编呢!
1 汇编的尝试
直接用 as
有点不理想,因为没有 db
用来往文件里写字符串。
不妨试试 nasm
,我们只需要一次 write
和一次 exit
,很容易编出这样的 asm:
section.text:
global _start
data db "Hello, World!", 0
_start:
xor eax, eax
add eax, 1
mov edi, eax
mov rdx, 13
mov esi, data
syscall
mov eax, 0x3C
mov rdi, 0
syscall
编译一下:
nasm -f ELF64 hello.s -o hello
编出来的文件没有可执行权限,+x
后运行一下。然后报错了:
zsh: exec format error: ./hello
发现文件并没有被 link 过!让 ld 来链接一下:
ld -g ./hello -o ./hello
然后程序成功运行了,我们来看看多大。
-rwxr-xr-x 1 woshiluo woshiluo 4.7K Apr 24 13:38 hello
2 链接的问题
怎么这么大!这很奇怪,我们只有几行汇编指令,这么大的文件是从哪里来的呢?
readelf
可以看到,ld
贴心的为我们添加了 bss 段等 padding,但是我们这里并不需要。
怎么办呢?其实我们可以考虑自己构建一个合法 ELF 结构。
而我们自然不是第一个这么想的,所以这里有一个前人的轮子:https://github.com/tchajed/minimal-elf
那么我们很成功的得到了一个 210B 大小的可执行文件!
3 压缩,再压缩
x86 是变长指令集。我们可以换换 asm 实现来进一步压缩。
那么首先是如何置 1,先 xor 后 inc 是比直接 mov 要短的。
mov 也可以砍一刀,我们可以向 al mov,这样会断不少。
syscall 是比较长的,我们可以 jmp 到一个 syscall,节省空间。
那么我们可以得到这样的代码:
let mut a = CodeAssembler::new(64)?;
let mut b = a.create_label();
let mut c = a.create_label();
a.xor(rax, rax)?;
a.inc(al)?;
a.xor(rdi, rdi)?;
a.inc(edi)?;
a.lea(rsi, ptr(b))?;
a.xor(rdx, rdx)?;
a.mov(dl, 13)?;
a.set_label(&mut c)?;
a.syscall()?;
a.mov(al, 0x3C)?;
a.xor(rdi, rdi)?;
a.jmp(c)?;
a.set_label(&mut b)?;
a.db(b"Hello, World!")?;
这样一来,我们就有 164B 大的 Hello,World!
-rwxr-xr-x 1 woshiluo woshiluo 164 Apr 24 13:48 tiny
4 附
上述文件的 strace:
$ strace.py ./tiny
execve(/tmp/minimal-elf/tiny, ['/tmp/minimal-elf/tiny'], [/* 40 vars */]) = 710265
Hello, World!write(1, 'Hello, World!', 13) = 13 (0x000000000000000d)
exit(0)
*** Process 710265 exited normally ***
似乎存在一些优化的可能性,ELFHeader 里有一些 padding 也许可以活用。而如果考虑直接输出文件名的话,也许可以做到更优。
不过 ELFHeader + ProgrmaHeader 都有 128B 了,能做的优化空间也不大了。
还是汇编大佬!