一次有趣的尝试 — 最小化 Hello,World

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 了,能做的优化空间也不大了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

message
account_circle
Please input name.
email
Please input email address.
links

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据