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

来源:互联网 发布:java 18.3 编辑:程序博客网 时间:2024/05/16 19:02

p { margin-bottom: 0.21cm; }

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

 

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

 

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

 

;tiny.asm

 

BITS32

 

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

84a.out

 

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

ELFheader的最后8字节和programheader table的最开始的8字节完全是一样的。

那么:

;tiny.asm

 

BITS32

 

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 $ - $$

Andsure 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

76a.out

 

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

 

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

 

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

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

 

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

 

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

;tiny.asm

 

BITS32

 

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 $ - $$

正如你现在看到的,programheader table的头20字节和ELFheader的最后20字节重叠了。在ELFheader中有两部分需要注意,第一个是e_phnum字段,它刚好和p_paddr字段相同,然而在programheadertable中它被明确忽略了。另一个是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

64a.out

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

 

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

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

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

;tiny.asm

 

BITS32

 

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_flags5变到4,与e_phoff的值相同,他标示了programheader table在文件中的偏移值。程序已经被移到ELFheader的更低的部分,开始与e_shoff字段,结束于e_flags字段。

加载地址也被改变到一个更低的值,这能保证e_entry字段中的值足够小,这很好因为它也是p_memszp_filesz的改变需要解释下。因为我们没有设置p_flags字段中的write位,Linux不允许我们定义p_memszp_filesz大,因为它不能零初始化那些额外的字节因为它们不可写。既然我们在保证programheadertable对齐的条件下不能改变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

52a.out

 

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

;tiny.asm

 

BITS32

 

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

45a.out

 

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

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

 

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

原创粉丝点击