在linux平台上创建超小的ELF可执行文件

/ns/wz/comp/data/20010909025736.htm

作者:breadbox <breadbox@muppetlabs.com>
原文<A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux >
整理翻译:alert7 <alert7@21cn.com>
来源: http://www.xfocus.org/
时间:2001-9-4
------------------------------------------------------------------

前言:
有些时候,文件的大小是很重要的,从这片文章中,也探讨了ELF文件格式内部的工作
情况与LINUX的操作系统。该片文章向我们展示了如何构造一个超小的ELF可执行文件。

文章中给出的这些example都是运行在intel 386体系的LINUX上。其他系统体系上或许也有同样的
效果,但我不感肯定。

我们的汇编代码使用的是Nasm写的,它的风格类似于X86汇编风格。
NASM软件是免费的,可以从下面得到
http://www.web-sites.co.uk/nasm/



--------------------------------------------------------------------------------

看看下面一个很小的程序例子,它唯一做的事情就是返回一个数值到操作系统中。
UNIX系统通常返回0和1,这里我们使用42作为返回值。

[alert7@redhat]# set -o noclobber && cat > tiny.c << EOF

/* tiny.c */
int main(void) { return 42; }
EOF

[alert7@redhat]# gcc -Wall tiny.c
[alert7@redhat]# ./a.out ;echo $?
42

再用gdb看看,这个程序实在很简单吧
[alert7@redhat]# gdb a.out -q
(gdb) disass main
Dump of assembler code for function main:
0x80483a0 <main>: push %ebp
0x80483a1 <main+1>: mov %esp,%ebp
0x80483a3 <main+3>: mov $0x2a,%eax
0x80483a8 <main+8>: jmp 0x80483b0 <main+16>
0x80483aa <main+10>: lea 0x0(%esi),%esi
0x80483b0 <main+16>: leave
0x80483b1 <main+17>: ret

看看有多大
[alert7@redhat]# wc -c a.out
11648 a.out

在原作者的机子上3998,在我的rh 2.2.14-5.0上就变成11648,好大啊,我们需要
使它变的更小。

[alert7@redhat]# gcc -Wall -s tiny.c
[alert7@redhat]# ./a.out ;echo $?
42
[alert7@redhat]# wc -c a.out
2960 a.out
现在变成2960,小多了.
gcc -Wall -s tiny.c实际上等价于
gcc -Wall tiny.c
strip a.out 抛弃所有的标号

[alert7@redhat]# wc -c a.out
11648 a.out
[alert7@redhat]# strip a.out
[alert7@redhat]# wc -c a.out
2960 a.out


下一步,我们来进行优化。


[alert7@redhat]# gcc -Wall -s -O3 tiny.c
[alert7@redhat]# wc -c a.out
2944 a.out

我们看到,只比上面的小16个字节,所以以优化指令来减小大小是比较困难的。

很不幸,C程序在编译的时候编译器会增加一些额外的代码,所以接下来我们使用汇编来写程序。

如上一个程序,我们需要返回代码为42,我们只需要把eax设置为42就可以了。程序的
返回状态就是存放在eax中的,从上面一段disass main出来的汇编代码我们也应该知道。

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
; tiny.asm
BITS 32
GLOBAL main
SECTION .text
main:
mov eax, 42
ret
EOF

编译并测试
[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s tiny.o
[alert7@redhat]# ./a.out ; echo $?
42

现在看看汇编代码有什么不同,看看它的大小
[alert7@redhat]# wc -c a.out
2892 a.out

这样又减小了(2944-2892)52个字节. 但是,只要我们使用main()接口,就还会有许多额外的代码。
linker还会为我们加一个到OS的接口。事实上就是调用main().所以我们如何来去掉我们不需要的
代码呢。

linker默认使用的实际入口是标号_start.gcc联接时,它会自动包括一个_start的例程,设置argc和argv,
....,最后调用main().

所以让我们来看看,是否可以跳过这个,自己定义_start例程。

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
; tiny.asm
BITS 32
GLOBAL _start
SECTION .text
_start:
mov eax, 42
ret
EOF

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s tiny.o
tiny.o: In function `_start':
tiny.o(.text+0x0): multiple definition of `_start'
/usr/lib/crt1.o(.text+0x0): first defined here
/usr/lib/crt1.o: In function `_start':
/usr/lib/crt1.o(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status


如何做才可以编译过去呢?
GCC有一个编译选项--nostartfiles

-nostartfiles
当linking时,不使用标准的启动文件。但是通常是使用的。

我们要的就是这个,再来:

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s -nostartfiles tiny.o
[alert7@redhat]# ./a.out ; echo $?
Segmentation fault (core dumped)
139

gcc没有报错,但是程序core dump了,到底发生了什么?

错就错在我们把_start看成了一个C的函数,然后试着从它返回。事实上它根本不是一个函数。
它仅仅是一个标号,它是被linker使用的一个程序入口点。当程序运行,它也就直接被调用。
假如我们来看,将看到在堆栈顶部的变量值为1,它的确非常的不象一个地址。事实上,在
堆栈那位置是我们程序的argc变量,之后是argv数组,包含NULL元素,接下来是envp环境变量。
所以,那个根本就不是返回地址。

因此,_start要退出,就要调用exit()函数。

事实上,我们实际调用的_exit()函数,因为exit()函数所要做的额外事情太多了,因为我们跳过了
lib库的启动代码,所以我们也可以跳过LIB库的shutdown代码。

好了,再让我们试试。调用_exit()函数,它唯一的参数就是一个整形。所以我们需要push一个数到
堆栈里,然后调用_exit().
(应该这样定义:EXTERN _exit)

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
; tiny.asm
BITS 32
EXTERN _exit
GLOBAL _start
SECTION .text
_start:
push dword 42
call _exit
EOF

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s -nostartfiles tiny.o
[alert7@redhat]# ./a.out ; echo $?
42

yeah~~,成功了,来看看多大

[alert7@redhat]# wc -c a.out
1312 a.out

不错不错,又减少了将近一半,:),有没有其他所我们感兴趣的gcc选项呢?

在-nostartfiles就有一个很另人感兴趣的选项:

-nostdlib
在linking的时候,不使用标准的LIB和启动文件。那些东西都需要自己指定传给
linker.
这个值得研究一下:

[alert7@redhat]# gcc -Wall -s -nostdlib tiny.o
tiny.o: In function `_start':
tiny.o(.text+0x6): undefined reference to `_exit'
collect2: ld returned 1 exit status


_exit()是一个库函数,但是加了-nostdlib 就不能使用了,所以我们必须自己处理,
首先,必须知道在linux下如何制造一个系统调用。


--------------------------------------------------------------------------------

象其他操作系统一样,linux通过系统调用来向程序提供基本的服务。
这包括打开文件,读写文件句柄,等等......

LINUX系统调用接口只有一个指令:int 0x80.所有的系统调用都是通过该接口。
为了制造一个系统调用,eax应该包含一个数字(该数字表明了哪个系统调用),其他寄存器
保存着参数。
假如系统调用使用一个参数,那么参数在ebx中;
假如使用两个参数,那么在ebx,ecx中
假如使用三个,四个,五个参数,那么使用ebx,ecx,esi

从系统调用返回时, eax 将包含了一个返回值。
假如错误发生,eax将是一个负值,它的绝对值表示错误的类型。

在/usr/include/asm/unistd.h中列出了不同的系统调用。
快速看一下将看到exit的系统调用号为1。它只有一个参数,该值会返回给父进程,该值会
被放到ebx中。

好了,现在又可以开工了:)

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
; tiny.asm
BITS 32
GLOBAL _start
SECTION .text
_start:
mov eax, 1
mov ebx, 42
int 0x80
EOF

[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# gcc -Wall -s -nostdlib tiny.o
[alert7@redhat]# ./a.out ; echo $?
42

看看大小

[alert7@redhat]# wc -c a.out
416 a.out


现在可真是tiny,呵呵,那么还能不能更小呢?
如何使用更短的指令呢?

看看下面两段汇编代码:

00000000 B801000000 mov eax, 1
00000005 BB2A000000 mov ebx, 42
0000000A CD80 int 0x80


00000000 31C0 xor eax, eax
00000002 40 inc eax
00000003 B32A mov bl, 42
00000005 CD80 int 0x80

很明显从功能上讲是等价的,但是下面一个比上面一个节约了5个字节。


使用gcc大概已经不能减少大小了,下面我们就使用linker--ld

[alert7@redhat]# set -o noclobber && cat > tiny.asm << EOF
; tiny.asm
BITS 32
GLOBAL _start
SECTION .text
_start:
xor eax,eax
inc eax
mov bl,42
int 0x80

EOF
[alert7@redhat]# nasm -f elf tiny.asm
[alert7@redhat]# ld -s tiny.o
[alert7@redhat]# wc -c a.out
412 a.out

小了4个字节,应该是5个字节的,但是另外的一个字节被用来考虑对齐去了。

是否到达了极限了呢,能否更小?

hm.我们的程序代码现在只有7个字节长。是否ELF文件还有405字节的额外的负载呢 ?他们都是
些什么?

使用objdump来看看文件的内容:

[alert7@redhat]# objdump -x a.out | less
a.out: no symbols

a.out: file format elf32-i386
a.out
architecture: i386, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x08048080

Program Header:
LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
filesz 0x00000087 memsz 0x00000087 flags r-x

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000007 08048080 08048080 00000080 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .bss 00000001 08049087 08049087 00000087 2**0
CONTENTS
2 .comment 0000001c 00000000 00000000 00000088 2**0
CONTENTS, READONLY

[译者注:在我的机子上多了个.bss节,我想可能是跟ld版本有关。所以在我系统上
演示的一直比原作者上面的大:(
看来要想更小的话,还是可以考虑找个低版本的编译:)
]

如上,完整的.text节为7个字节大,刚好如我们刚才所说。

但是还有其他的节,例如".comment",谁安排它的呢?".comment"节大小为28个字节。
我们现在不知道.comment节到底是什么东西,但是可以大胆的说,它是不必须的。

.comment节在文件偏移量为00000087 (16进制)
我们来看看是什么东西

[alert7@redhat]# objdump -s a.out

a.out: file format elf32-i386

Contents of section .text:
8048080 31c040b3 2acd80 1.@.*..
Contents of section .bss:
8049087 00 .
Contents of section .comment:
0000 00546865 204e6574 77696465 20417373 .The Netwide Ass
0010 656d626c 65722030 2e393800 embler 0.98.

哦,是nasm自己的一段信息,或许我们应该使用gas.......

假如我们:

[alert7@redhat]# set -o noclobber && cat > tiny.s << EOF
.globl _start
.text
_start:
xorl %eax, %eax
incl %eax
movb $42, %bl
int $0x80
EOF

[alert7@redhat]# gcc -s -nostdlib tiny.S
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
368 a.out

[译者注:在作者机子上这里大小没有变化,但在我的系统上,这里变成了368
(跟作者的机子上一样了),比前面的所以的都要小
]


再用一下objdump,会有些不同:

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000007 08048074 08048074 00000074 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 0804907c 0804907c 0000007c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0804907c 0804907c 0000007c 2**2
ALLOC

没有了commnet节,但是多了两个无用的节,用来存储不存在的数据。而且那些节居然还是0长度。
他们使文件大小变大。

所以它们都是没有用的,我们如何来去掉它们呢?

我们需要准备一些elf文件格式的知识。虽然我也已经翻译过《ELF文件格式》 ,
在http://www.xfocus.org/上可以找到,但是翻译的很垃圾,早已招人唾骂过了,
所以还是推荐大家看英文原版文档,而且是强烈推荐。



--------------------------------------------------------------------------------
elf文件格式英文文档下载地址:
ftp://tsx.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.gz.
或者 http://www.muppetlabs.com/~breadbox/software/ELF.txt.

基本的,我们需要知道如下知识:

每一个elf文件都是以一个ELF header的结构开始的。该结构为52个字节长,并且包含了一个
信息部分,这些信息部分描述了文件的内容。例如,前16个字节包含了一个“标识符”,它
包含了ELF文件的魔术数,但字节的标记表明是32位的还是64位的,小端序还是大端序,等等。
在elf header包含的其他的信息还有,例如:目标体系;ELF文件是否是可执行的还是OBJECT
文件还是一个共享的库;程序的开始地址;program header table和section header table
在文件的偏移量。


两个表可以出先在文件的任何地方, 但是以前经常是直接跟在ELF HEADER后面,后来出现在
文件的末尾或许是靠近末尾。两个表有相试的功能,都是为了甄别文件的组成。但是,
section header table更关注的是识别在程序中不同部分在什么地方,然而,program
header table描述的是哪里和如何把那些部分转载到内存中。
简单的说,section header table 是被编译器(compiler)和连接器(linker)使用,program
header table是被程序转载器(loader)使用。对object 文件,program header talbe是
可选的,实际上从来也没有出现过。同样的,对于可执行文件来说,section header table
也是可选的,但是它却总是存在于可执行文件中。

因此,对于我们的程序来说,seciton header table是完全没有用的,那些sections也不会
影响到程序内存的映象。

那么,到底如何去掉它们呢?

我们必须自己来构造程序的ELF HEADER.
你也可以查看ELF文档和/usr/include/linux/elf.h得到相关信息,一个空的ELF可执行文件应该
象如下:


BITS 32

org 0x08048000

ehdr: ; Elf32_Ehdr
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 9 db 0
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

_start:

; your program here

filesize equ $ - $$

该映象包含了一个ELF header ,没有section header table ,一个program header table 包含了
一个入口。该入口指示程序转载器把完整的文件装载到内存(一般的是包含自己的ELF header 和
program header table)开始地址为0x08048000(这是可执行文件装载的默认地址)的地方,并且
开始执行_start处代码,_start紧跟着program header table.没有.data段,没有.bss段
没有.comment段。

好了,现在我们的程序就变成这样了:

[alert7@redhat]# cat tiny.asm
; tiny.asm
org 0x08048000

ehdr: ; Elf32_Ehdr
db 0x7F, "ELF", 1, 1, 1 ; e_ident
times 9 db 0
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
_start:
mov bl, 42
xor eax, eax
inc eax
int 0x80

filesize equ $ - $$


[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42


再看看大小:

[alert7@redhat]# wc -c a.out
93 a.out


真是奇迹,才93个字节大小了。

假如我们明白在可执行文件中的每个字节,我们或许还可以更小,也许很是极限了哦:)


--------------------------------------------------------------------------------

你可能已经注意到了:
1)ELF文件的不同部分允许被定位在任何地方(除了ELF header,它必须放在文件的开始),
并且它们可以交叠。
2)事实上一些字段到目前还没有被用到。

在鉴别文件字段最后有9个字节为0,我们的代码只有7个字节长,所以我们试图把代码放入
鉴别文件字段最后9个字节中,还有2个剩余。....


[alert7@redhat]# cat tiny.asm
; tiny.asm

BITS 32

org 0x08048000

ehdr: ; Elf32_Ehdr
db 0x7F, "ELF" ; e_ident
db 1, 1, 1, 0
_start: mov bl, 42
xor eax, eax
inc eax
int 0x80
db 0
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 $ - $$


[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
84 a.out


现在我们的程序只有一个elf header和一个program header table入口,为了装载和运行程序,
这些是我们必要的。所以现在我们不能减少了!除非....


我们使elf header和program header table一部分重合或者说是交叠,有没有可能呢?

答案当然是有的,注意我们的程序,就会注意到在elf header最后8个字节和program header table
前8个字节是一样的,所以...

[alert7@redhat]# cat tiny.asm
; tiny.asm

BITS 32

org 0x08048000

ehdr:
db 0x7F, "ELF" ; e_ident
db 1, 1, 1, 0
_start: mov bl, 42
xor eax, eax
inc eax
int 0x80
db 0
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 $ - $$


[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
76 a.out


现在已经不能够再更多的重叠那两个结构了,因为两个结构的字节没有再相同的了。

但是,我们可以再构造这两个结构,使它们有更多的相同部分。

到底linux会检查多少字段呢?例如,它会检查e_machine字段吗?

事实上很另人惊讶,一些字段居然被默默的忽略了。

因此:哪些东西才是ELF header中最重要的呢?最前的四个字节当然是的,它包含了一个
魔术数,否则linux不会继续处理它。在e_ident字段的其他3个字节不被检查,那就意味着
我们有不少于12个连续的字节我们可以设置为任意的值。e_type必须被设置为2(用来表明
是个可执行文件),e_machine必须为3。就象e_ident中的版本号一样,e_version被完全的
忽略。(这样做可以理解,因为现在只有一个版本的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入口的
大小。但是只有在2.2.17以后的2.2系列内核中这个字段才是被检查的。早于2.2的和2.4.0的
内核是忽略它的。

program header table又是如何呢?
p_type必须是1(即PT_LOAD),表明这是个可载入的段。p_offset是开始装载的文件偏移量。
同样的,p_vaddr是正确的装载地址。注意:我们没有要求把它装载到0x08048000.
可用的地址为0-0x80000000,并且要页对齐。文档上说p_paddr被忽略,因此这个字段更是可
用的。p_filesz 指示了从文件中装载到内存中有多少字节,p_memsz指示了需要多大的内存段。
因此,他们的值应该是相关的。p_flags指示了给于内存段什么权限。可设置读,写,执行,
其他位也可能被设置,但是我们只需要最小权限。最后,p_align给出了对齐需求。该字段主要
使用在当重定位段包含了与位置无关的代码时,岂今为止,可执行文件将被LINUX忽略这个字段。

根据分析,我们从中可以看出一些必要的字段,一些无用的字段,这样,我们就可以重叠更多的
字数了。

[alert7@redhat]# cat tiny.asm
; tiny.asm◆

BITS 32

org 0x00200000

db 0x7F, "ELF" ; e_ident
db 1, 1, 1, 0
_start:
mov bl, 42
xor eax, eax
inc eax
int 0x80
db 0
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的前12个字节重叠在ELF header的最后12个字节里。
相当的吻合。ELF header重复中只有两部分会有麻烦。一是e_phnum字段,相对应的是p_paddr
是会被忽略。第二个是e_phentsize字段,它和p_vaddr前两个字节相一致,为了这个相一致,
使用了非标准的加载地址0x00200000,那么前面的两个字节就是0x0020.

[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
64 a.out

well,现在大小为64字节了

如果我们使 program header table完全放在ELF header中,那么,呵呵,大小就可以更小了,
但是这样做行吗?

是的,是可能的。使program header table从第四个字节就开始,精心构造可执行的ELF文件。

我们注意到:
第一P_memsz指出了为内存段分配多少内存。明显的,它必须至少跟P_filesz一样大,
当然更大是没有关系的。

第二, 可执行位可以从p_flags字段中丢弃,linux会为我们设置它的。为什么这样会工作呢?
作者说不知道,又猜测了原因说是否因为入口指针指向了该段?

[★译者注:
但我知道,linux根本就没有为我们设置p_flags字段中的可执行位,可以工作,
只是因为Intel体系上根本就不具有执行保护功能,就是这个原因,才使得有人有
必要设计了类似堆栈不可运行的内核补丁程序。
]


[alert7@redhat]# cat tiny.asm
; 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 filesize ; 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,这个4也是e_phoff字段的值,它给出了program header table在文件中
的偏移量。代码被放在从e_shoff 开始到e_flags内部结束。

注意到:装载地址被改变了更低了。只是为了保持e_entry的值到一个比较合适的小值,它刚好
也是P_mensz的数值。

[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
52 a.out

现在,程序代码本身和program header table完全嵌入了ELF header,我们的可执行文件现在和
elf header一样大。而且可以正常运行。

最后,我们不禁还要问,是否到达了最小的极限呢?毕竟,我们需要一个完整的ELF header,否则
linux不会给我们运行的机会。

真的是这样吗 ?

错了,我们还可以运用最后一招卑鄙的哄骗技术了。

如果文件大小还没有整个ELF header大的话,linux还是会运行它的。并且把那些少的字节填充为
0。我们在文件的最后有不少于7个0,可以丢弃。


[alert7@redhat]# cat tiny.asm
; 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 filesize ; 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 $ - $$

[alert7@redhat]# nasm -f bin -o a.out tiny.asm
[alert7@redhat]# chmod +x a.out
[alert7@redhat]# ./a.out ; echo $?
42
[alert7@redhat]# wc -c a.out
45 a.out


讨论到此,一个elf可执行文件最小大小为45 bytes,我们被迫终止我们的讨论了。

--------------------------------------------------------------------------------
一个45字节大小的文件比一个用标准工具创建的最小可执行文件的1/8还要小,比用纯C代码
创建的1/50还要小。

这片文章中的一半ELF字段变量违反了标准的ELF规范,

以上程序中打上◆ 的程序,会使readelf core dump
[alert7@redhat]# readelf -a a.out
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 b3 2a 31 c0 40 cd 80 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 179
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x200008
Start of program headers: 32 (bytes into file)
Start of section headers: 1 (bytes into file)
Flags: 0x0
Size of this header: 0 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 1
Size of section headers: 0 (bytes)
Number of section headers: 64
Section header string table index: 0
readelf: Error: Unable to read in 0 bytes of section headers

Program Header:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00200000 0x00000001 0x00040 0x00040 R E 0x1000

There is no dynamic segment in this file.
Segmentation fault (core dumped)

呵呵,居然出现了可爱的core dumped

[alert7@redhat]# ls -l /usr/bin/readelf
-rwxr-xr-x 1 root root 132368 Feb 5 2000 /usr/bin/readelf

:(不是带s位的,也就懒的去看它到底哪里出问题了。

创建的这种超小的elf文件的确比较畸形,连objdump都不能dump它们了。
[alert7@redhat]# objdump -a a.out
objdump: a.out: File format not recognized