创建Linux下可运行的超小型ELF可执行文件(4)
2010-10-03 17:01
288 查看
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
如果你现在就停止阅读
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
相关文章推荐
- 创建Linux下可运行的超小型ELF可执行文件(2)
- 创建Linux下可运行的超小型ELF可执行文件(3)
- 创建Linux下可运行的超小型ELF可执行文件(1)
- 在Linux平台上创建超小的ELF可执行文件
- 在linux平台上创建超小的ELF可执行文件
- 在linux平台上创建超小的ELF可执行文件
- 在linux平台上创建超小的ELF可执行文件
- 怎样创建真正很小的Linux下的ELF可执行文件————X86-64 Ubuntu实践
- Linux下ELF可执行文件装载与运行
- Linux 可执行文件 ELF结构 及程序加载运行
- 在linux平台上创建超小的ELF可执行文件
- 在linux平台上创建超小的ELF可执行文件
- 怎样创建真正很小的Linux下的ELF可执行文件
- 怎样创建真正很小的Linux下的ELF可执行文件
- Linux下的ELF可执行文件的格式解析
- linux可执行文件的加载和运行之一(4)
- Linux启动ELF可执行文件的过程
- Python远程登录Linux操作系统,执行命令、创建目录、上传及下载文件
- Linux下ELF格式可执行文件及动态链接相关部分的解析
- linux可执行文件的加载和运行之一(5)