创建Linux下可运行的超小型ELF可执行文件(4)

p { margin-bottom: 0.21cm; }

如果你现在就停止阅读 ELF 规范,那么你本可以发现另一些规则的: 1)ELF 文件的不同块可以位于文件中的任何位置,除了 ELF header 必须位于文件最开始部分,所以可以把一些部分进行重叠; 2)header 里的一些字段并没有真正被使用。

 

具体地说,我正在想 header 中的 16 字节长的标识符字段尾部的一串零。它们只是纯粹的填充,旨在为 ELF 规范将来的扩展预留空间。所以操作系统并不关心那个地方有什么信息。我们已经把任何东西都加载到内存中,我们的程序代码只有 7 字节。。。

 

那么我们能把程序代码放到 ELF header 里去吗?

 

; tiny.asm

 

BITS 32

 

org 0x08048000

 

ehdr: ; Elf32_Ehdr

db 0x7F, "ELF" ; e_ident

db 1, 1, 1, 0, 0

_start: mov bl, 42

xor eax, eax

inc eax

int 0x80

dw 2 ; e_type

dw 3 ; e_machine

dd 1 ; e_version

dd _start ; e_entry

dd phdr - $$ ; e_phoff

dd 0 ; e_shoff

dd 0 ; e_flags

dw ehdrsize ; e_ehsize

dw phdrsize ; e_phentsize

dw 1 ; e_phnum

dw 0 ; e_shentsize

dw 0 ; e_shnum

dw 0 ; e_shstrndx

 

ehdrsize equ $ - ehdr

 

phdr: ; Elf32_Phdr

dd 1 ; p_type

dd 0 ; p_offset

dd $$ ; p_vaddr

dd $$ ; p_paddr

dd filesize ; p_filesz

dd filesize ; p_memsz

dd 5 ; p_flags

dd 0x1000 ; p_align

 

phdrsize equ $ - phdr

 

filesize equ $ - $$

毕竟字节就是字节:

$ nasm -f bin -o a.out tiny.asm

$ chmod +x a.out

$ ./a.out ; echo $?

42

$ wc -c a.out

84 a.out

 

我们是否可以对 program header table 做同样的事情呢?它和 ELF header 重叠可能吗?

ELF header 的最后 8 字节和 program header table 的最开始的 8 字节完全是一样的。

那么:

; tiny.asm

 

BITS 32

 

org 0x08048000

 

ehdr:

db 0x7F, "ELF" ; e_ident

db 1, 1, 1, 0, 0

_start: mov bl, 42

xor eax, eax

inc eax

int 0x80

dw 2 ; e_type

dw 3 ; e_machine

dd 1 ; e_version

dd _start ; e_entry

dd phdr - $$ ; e_phoff

dd 0 ; e_shoff

dd 0 ; e_flags

dw ehdrsize ; e_ehsize

dw phdrsize ; e_phentsize

phdr: dd 1 ; e_phnum ; p_type

; e_shentsize

dd 0 ; e_shnum ; p_offset

; e_shstrndx

ehdrsize equ $ - ehdr

dd $$ ; p_vaddr

dd $$ ; p_paddr

dd filesize ; p_filesz

dd filesize ; p_memsz

dd 5 ; p_flags

dd 0x1000 ; p_align

phdrsize equ $ - phdr

 

filesize equ $ - $$

And sure enough, Linux doesn't mind our parsimony one bit:

$ nasm -f bin -o a.out tiny.asm

$ chmod +x a.out

$ ./a.out ; echo $?

42

$ wc -c a.out

76 a.out

 

重叠这个方法已经用到头了,除非我门能够改变结构的内容来匹配更多的字段。。。

 

到底 Linux 会检查多少这里面的字段呢?比如, Linux 真会去看字段 e_machine 是否包含 3( 代表 intel 386) ,或者它仅仅是假设就是这样?

 

事实上, Linux 确实会去检测字段 e_machine 。但是,有相当数量的其他字段被默默地忽略了。

接下来就是 ELF header 中关键的和非关键的字段。第一个四字节必须包含魔数,否则 Linux 不会去识别这个文件。在字段 e_ident 里的其他三字节不会被检测,那意味着我们有不少于 12 字节的连续空间可以填充任何信息。 e_type 必须被设置成 2 ,来标示其为可执行文件, e_machine 必须为 3 e_version 就像是 e_ident 中的版本号,完全忽略掉了。这也可以理解,目前只有唯一一个 ELF 标准版本。 e_entry 也必须是有效的,因为它指向程序的开始。很显然, e_phoff 必须包含 program header table 在文件中的正确的偏移, e_phnum 必须包含 program header table 中的正确的项数, e_flags 据记载现在没有为 intel 使用,所以我们可以自由地使用它。 e_ehsize 用于验证 ELF header 有期待的长度,但是 Linux 并没有用它。 e_phentsize 用于验证 program header table 中表项的长度。这个字段在比较老的内核中没有用,但是现在它必须被正确地设置。 ELF header 里的其他信息是关于 section header table ,它们对于可执行文件不起作用。

 

现在让我们来考虑下 program header table 表项。字段 p_type 必须包含数字 1 ,来标示它为一个可加载段, p_offset 也需要有正确的文件偏移来开始加载。同样地, p_vaddr 需要包含合适的加载地址。注意我们没有被要求必须加载到内存映像 0x08048000 处。任何位于 0x00000000 0x80000000 之间的页对齐的地址都可以。 p_paddr 被忽略,所以是肯定可以自由使用的。 p_filesz 标示文件中的多少字节要被加载到内存中, p_memsz 标示内存段需要有多大, p_flags 标示内存段的权限设置,它必须是 readable(4) ,否则,它就没有用,它也必须是 executable(1) ,否则就不能被执行。其他位也可以被设置,但是必须至少包含这两位。最后, p_align 给出了内存段的位对齐需求。这个字段主要用于重定位包含 pic 的段时,所以对于可执行文件 Linux 会忽略我们存储在这里的任何垃圾。

 

总结起来,有点余地发挥。特别是,仔细观察后发现 ELF header 中的大部分必须字段在前半部,后半部可以自由挥霍。基于这点考虑,我们能重叠那两个结构再多一点点:

; tiny.asm

 

BITS 32

 

org 0x00200000

 

db 0x7F, "ELF" ; e_ident

db 1, 1, 1, 0, 0

_start:

mov bl, 42

xor eax, eax

inc eax

int 0x80

dw 2 ; e_type

dw 3 ; e_machine

dd 1 ; e_version

dd _start ; e_entry

dd phdr - $$ ; e_phoff

phdr: dd 1 ; e_shoff ; p_type

dd 0 ; e_flags ; p_offset

dd $$ ; e_ehsize ; p_vaddr

; e_phentsize

dw 1 ; e_phnum ; p_paddr

dw 0 ; e_shentsize

dd filesize ; e_shnum ; p_filesz

; e_shstrndx

dd filesize ; p_memsz

dd 5 ; p_flags

dd 0x1000 ; p_align

 

filesize equ $ - $$

正如你现在看到的, program header table 的头 20 字节和 ELF header 的最后 20 字节重叠了。在 ELF header 中有两部分需要注意,第一个是 e_phnum 字段,它刚好和 p_paddr 字段相同,然而在 program header table 中它被明确忽略了。另一个是 e_phentsize 字段,它和 p_vaddr 字段的头半截相同。者可以通过选择一个非标注你的加载地址,只要其头半截为 0x0020 就可以了。

 

$ nasm -f bin -o a.out tiny.asm

$ chmod +x a.out

$ ./a.out ; echo $?

42

$ wc -c a.out

64 a.out

确实能行!正如所预期的,又少了 12 字节!

 

我们注意到 p_memsz 标示为内存段分配的内存量,显然它至少要跟 p_filesz 一样大,但是如果它更大些也无妨。毕竟我们申请多少并不意味着我们必须使用多少。

另外,与我们的期望相反, executable 位能够从 p_flags 字段中去掉。看起来 readable executable 是冗余的:两者都可以代表另一个。

想到这两点,我们可以重新组织文件:

; tiny.asm

 

BITS 32

 

org 0x00001000

 

db 0x7F, "ELF" ; e_ident

dd 1 ; p_type

dd 0 ; p_offset

dd $$ ; p_vaddr

dw 2 ; e_type ; p_paddr

dw 3 ; e_machine

dd _start ; e_version ; p_filesz

dd _start ; e_entry ; p_memsz

dd 4 ; e_phoff ; p_flags

_start:

mov bl, 42 ; e_shoff ; p_align

xor eax, eax

inc eax ; e_flags

int 0x80

db 0

dw 0x34 ; e_ehsize

dw 0x20 ; e_phentsize

dw 1 ; e_phnum

dw 0 ; e_shentsize

dw 0 ; e_shnum

dw 0 ; e_shstrndx

 

filesize equ $ - $$

p_flags 5 变到 4 ,与 e_phoff 的值相同,他标示了 program header table 在文件中的偏移值。程序已经被移到 ELF header 的更低的部分,开始与 e_shoff 字段,结束于 e_flags 字段。

加载地址也被改变到一个更低的值,这能保证 e_entry 字段中的值足够小,这很好因为它也是 p_memsz p_filesz 的改变需要解释下。因为我们没有设置 p_flags 字段中的 write 位, Linux 不允许我们定义 p_memsz p_filesz 大,因为它不能零初始化那些额外的字节因为它们不可写。既然我们在保证 program header table 对齐的条件下不能改变 p_flags 字段的值,你可能会想唯一的解决方案就是把 p_memsz 下移至与 p_filesz 相等。尽管如此,还有另一种解决方案,增加 p_filesz 使之等于 p_memsz 。那意味这它俩都比实际文件大的多,但是它能让加载器避免写只读内存,这是它所关注的。

$ nasm -f bin -o a.out tiny.asm

$ chmod +x a.out

$ ./a.out ; echo $?

42

$ wc -c a.out

52 a.out

 

看起来就算文件的长度并不是整个 ELF header 的长度, Linux 仍然能玩得转,并用零填充缺失的字节。我们有不少于 7 个零在文件末尾,所以我们可以把它们从文件映像中去除。

; tiny.asm

 

BITS 32

 

org 0x00001000

 

db 0x7F, "ELF" ; e_ident

dd 1 ; p_type

dd 0 ; p_offset

dd $$ ; p_vaddr

dw 2 ; e_type ; p_paddr

dw 3 ; e_machine

dd _start ; e_version ; p_filesz

dd _start ; e_entry ; p_memsz

dd 4 ; e_phoff ; p_flags

_start:

mov bl, 42 ; e_shoff ; p_align

xor eax, eax

inc eax ; e_flags

int 0x80

db 0

dw 0x34 ; e_ehsize

dw 0x20 ; e_phentsize

db 1 ; e_phnum

; e_shentsize

; e_shnum

; e_shstrndx

 

filesize equ $ - $$

... we can, incredibly enough, still produce a working executable:

$ nasm -f bin -o a.out tiny.asm

$ chmod +x a.out

$ ./a.out ; echo $?

42

$ wc -c a.out

45 a.out

 

最后 45 字节的文件比最小的用标准工具创建的 ELF 可执行文件大小的八分之一还要小,比最小的用纯 C 代码编写的文件的十五分之一还要小。当然,文件里一半的值都违反了 ELF 标准。

另一方面,这个可执行文件里的每一个字节都是有意义的并且是正当的。有多少你最近创建的可执行文件能够这么说?

 

http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值