p { margin-bottom: 0.21cm; }
我们的程序本身只有 7 字节,难道 ELF 真的需要 361 字节的额外空间吗?
我们用 objdump 来看一下文件内容:
$ objdump -x a.out | less
让我们看看块列表:
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000007 08048080 08048080 00000080 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .comment 0000001c 00000000 00000000 00000087 2**0
CONTENTS, READONLY
完整的 .text 节显示是 7 字节,这说明完全控制我们程序的机器码很安全。
但是 .comment 节是做什么用的呢?它有 28 字节!我们并不确定 .comment 节是做什么用的,但是看起来它并不是必要的代码。。。
我们可以看看 .comment 节到底存储了什么内容,在文件偏移 0x00000087 处,使用 hexdump 查看:
00000080: 31C0 40B3 2ACD 8000 5468 6520 4E65 7477 1.@.*...The Netw
00000090: 6964 6520 4173 7365 6D62 6C65 7220 302E ide Assembler 0.
000000A0: 3938 0000 2E73 796D 7461 6200 2E73 7472 98...symtab..str
谁能想到 nasm 会暗中做这些事?也许我们该换用 gas , AT&T :
; tiny.s
.globl _start
.text
_start:
xorl %eax, %eax
incl %eax
movb $42, %bl
int $0x80
$ gcc -s -nostdlib tiny.s
$ ./a.out ; echo $?
42
$ wc -c a.out
368 a.out
一样的结果!
但是,通过 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
又多了两节,虽然他们的长度为零,但是仍然导致了额外的空间。
对于这些额外消耗,我们怎样避免呢?
要回答这个问题,我们需要理解 ELF 文件格式。
关于 intel-386 架构下 ELF 文件格式的正式描述可以参考 http://refspecs.freestandards.org/elf/elf.pdf 。你也可以参考普通文本格式的 1.0 版 http://www.muppetlabs.com/~breadbox/software/ELF.txt 。这些规范覆盖了很多方面,所以如果你不想自己读完全部的文档,我也可以理解。基本上,我们需要懂得如下内容就可以了:
每一个 ELF 文件都是以一个叫做 ELF header 的结构体开始的。这个结构体有 52 字节长,它包含一些描述文件内容的信息。比如,第一个 16 字节包含文件的魔数签名 (7F 45 4C 46) ,还有 1 字节的标志位表明是 32 位还是 64 位,大尾还是小尾编码,等等。 ELF header 里的其他字段包含信息比如目标处理器架构,文件类型喂可执行,目标文件还是共享目标文件,程序的起始地址, program header table 和 section header table 的位置。
Program header table 和 section header table 能够出现在文件的任何位置,但是前者一般紧跟在 ELF header 之后,后者一般出现在文件末尾或者接近末尾的地方。这两个表完成相似的目的,都是为了标示文件中的组件块。尽管如此, Section header table 倾向于标示程序中各种块在文件中的位置,而 program header table 描述这些块怎样被加载到内存中的什么位置。简单地讲, section header table 对编译器和链接器有用,而 program header table 对程序加载器有用。 Program header table 对于目标文件来说是可选的,在实际应用中从来不出现。同样地, section header table 对于可执行文件是可选的,但是在实际应用中总是出现。
那么,这就是我们第一个问题的答案。程序中的一些过度的空间消耗用于完全没必要的 section header table ,和一些同样对程序的内存映像毫无帮助的没有用的块。
接下来,我们转向第二个问题:怎样除掉这些无用块呢?
没有标准的工具用于制作不包含 section header table 之类的可执行文件。如果想做这样的事,那只能靠我们自己了。
但是那并不意味着我们必须打开二进制编辑器手写十六进制代码。 nasm 有一种普通二进制文本输出格式,那正好适合我们用。我们所需要的全部信息就是一个空 ELF 可执行文件的内存映像,然后把我们的程序填进去。
我们能够看 ELF 规范,和 /usr/include/linux/elf.h ,并参考标准工具创建的可执行文件,来推断一个空的 ELF 可执行文件应该是什么样。但是,如果你不是很有耐心,那么可以用下面提供的:
BITS 32
org 0x08048000
ehdr: ; Elf32_Ehdr
db 0x7F, "ELF", 1, 1, 1, 0 ; e_ident
times 8 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 ,标示文件是 intel 386 可执行文件,没有 section header , program header table 包含一个元素。前文中讲过 program header 引导程序加载器把整个文件加载到内存映像中地址 0x08048000 处,这是默认的可执行文件的加载地址。接着就开始执行 _start 处的代码,它紧随 program header table 之后。没有 .data 段,没有 .bss 段,没有注释,除了真正必须的信息。
我们的小程序现在变成:
; tiny.asm
org 0x08048000
;
; (as above)
;
_start:
mov bl, 42
xor eax, eax
inc eax
int 0x80
filesize equ $ - $$
试一下:
$ nasm -f bin -o a.out tiny.asm
$ chmod +x a.out
$ ./a.out ; echo $?
42
$ wc -c a.out
91 a.out