原文地址:
http://www.muppetlabs.com/~breadbox/...ny/teensy.html 如果你是一個(gè)對(duì)軟件體積腫脹受夠了的程序員,那么可能你能在這里找到合適的解決方法。
本文討論把額外的字節(jié)榨出簡(jiǎn)單程序的方法。(當(dāng)然,本文更實(shí)際的目的是描述ELF文件格式和Linux操作系統(tǒng)的一些內(nèi)部工作。但是希望你也能在這個(gè)過(guò)程中學(xué)到一些如何創(chuàng)建真正很小的ELF可執(zhí)行文件的知識(shí)。)
請(qǐng)注意這里給出的信息和示例,絕大部分,都是針對(duì)運(yùn)行于Intel-386體系結(jié)構(gòu)下的Linux平臺(tái)上的ELF可執(zhí)行文件的。我猜測(cè)這些信息中很有一部分也適用于其它基于ELF的Unix系統(tǒng),但是我在這方面的經(jīng)驗(yàn)太有限了所以不能肯定。
本文中出現(xiàn)的匯編代碼是要使用Nasm匯編的。(除了更適合于我們的需要之外,Nasm的語(yǔ)法對(duì)于那些在學(xué)會(huì)使用Gas之前學(xué)習(xí)x86匯編語(yǔ)言的人來(lái)說(shuō)比AT&T語(yǔ)法更好。)Nasm可以免費(fèi)獲得并且非常容易移植;參見(jiàn)
http://www.web-sites.co.uk/nasm/。也請(qǐng)注意如果你對(duì)匯編代碼不是特別熟,你會(huì)發(fā)現(xiàn)本文的一些部分很難懂。
――――――――――――――――――――――――――――――――――――――
為了開(kāi)始工作,我們需要一個(gè)程序。幾乎任何程序都可以,但是程序越簡(jiǎn)單越好,因?yàn)?br>
我們更感興趣的是我們能把可執(zhí)行文件做成多么小而不是能把程序作多么小。
讓我們拿一個(gè)非常非常簡(jiǎn)單的程序,它什么也不作只是向操作系統(tǒng)返回一個(gè)數(shù)。為什么
不呢?畢竟,Unix已經(jīng)帶了至少兩個(gè)這種程序:true和false。既然0和1已經(jīng)被用過(guò)了,
我們就使用數(shù)42吧。
所以,這就是我們的第一版:
/* tiny.c */
int main(void) { return 42; }
我們可以進(jìn)行如下的編譯和測(cè)試:
$ gcc -Wall tiny.c
$ ./a.out ; echo $?
42
好了。它有多大呢?在我的機(jī)器上,有:
$ wc -c a.out
3998 a.out
(在你的機(jī)器上可能會(huì)稍有不同。)應(yīng)該承認(rèn),按當(dāng)今的標(biāo)準(zhǔn)來(lái)說(shuō)這已經(jīng)是非常小了。
但是它幾乎肯定比它需要的要大。
很顯然的第一步是strip可執(zhí)行文件:
$ gcc -Wall -s tiny.c
$ ./a.out ; echo $?
42
$ wc -c a.out
2632 a.out
這的確有改善。下一步,優(yōu)化會(huì)怎么樣?
$ gcc -Wall -s -O3 tiny.c
$ wc -c a.out
2616 a.out
這也有所幫助,但是不多。這是合理的:程序中幾乎沒(méi)有什么可以優(yōu)化的。
看起來(lái)好像我們?cè)僖膊荒芸s減一個(gè)只有一條語(yǔ)句的C程序了。我們將拋棄C,轉(zhuǎn)而使用匯
編。希望這將把C程序自動(dòng)帶來(lái)的額外開(kāi)銷砍掉。
現(xiàn)在,向我們的第二版進(jìn)軍。我們需要做的就是從main()中返回42。在匯編語(yǔ)言中,這
意味著該函數(shù)應(yīng)該把累加器,eax,設(shè)置為42,然后返回:
; tiny.asm
BITS 32
GLOBAL main
SECTION .text
main:
mov eax, 42
ret
然后我們可以build并測(cè)試如下:
$ nasm -f elf tiny.asm
$ gcc -Wall -s tiny.o
$ ./a.out ; echo $?
42
(嘿,誰(shuí)說(shuō)匯編代碼很難呀?)現(xiàn)在有多大?
$ wc -c a.out
2604 a.out
看起來(lái)我們只去掉了區(qū)區(qū)的12字節(jié)。難道C只自動(dòng)引進(jìn)這么多的額外開(kāi)銷嗎?
問(wèn)題在于,通過(guò)使用main()接口我們?nèi)匀灰M(jìn)了很大開(kāi)銷。鏈接器仍然為我們添加一個(gè)
到OS的接口,真正調(diào)用main()的是那個(gè)接口。那如果我們不需要它的話該怎么做?
鏈接器真正使用的入口點(diǎn)缺省是名字為_(kāi)start的符號(hào)。當(dāng)我們使用gcc鏈接時(shí),它自動(dòng)
包含一個(gè)_start例程,該例程將設(shè)置argc和argv,以及其他事情,然后調(diào)用main()。
那么,看看我們能否繞過(guò)這一點(diǎn)。定義我們自己的_start例程:
; tiny.asm
BITS 32
GLOBAL _start
SECTION .text
_start:
mov eax, 42
ret
gcc會(huì)按我們想要的去做嗎?
$ nasm -f elf tiny.asm
$ gcc -Wall -s tiny.o
tiny.o(.text+0x0): multiple definition of `_start'
/usr/lib/crt1.o(.text+0x0): first defined here
/usr/lib/crt1.o(.text+0x36): undefined reference to `main'
不會(huì)。嗯,實(shí)際上,它會(huì)的,但是首先我們得知道怎樣才能得到我們想要的東西。
原來(lái)gcc能識(shí)別一個(gè)選項(xiàng)-nostartfiles。從gcc的info頁(yè)上可以看到:
-nostartfiles
Do not use the standard system startup files when linking. The
standard libraries are used normally.
耶!現(xiàn)在看看我們能做些什么:
$ nasm -f elf tiny.asm
$ gcc -Wall -s -nostartfiles tiny.o
$ ./a.out ; echo $?
Segmentation fault
139
gcc不抱怨了,但是程序不能工作。錯(cuò)在哪里?
錯(cuò)誤在于我們把_start當(dāng)作它好像是一個(gè)C函數(shù),并且試圖從它返回。實(shí)際上,它根本
不是一個(gè)函數(shù)。它只是目標(biāo)文件中鏈接器用來(lái)定位程序入口點(diǎn)的一個(gè)符號(hào)。當(dāng)我們的程
序被激活時(shí),它被直接激活。如果我們?nèi)ゲ榭匆幌?,將?huì)發(fā)現(xiàn)棧頂上是數(shù)1,這顯然不
像是一個(gè)地址。事實(shí)上,棧頂上是我們程序的argc值。在這之后是argv數(shù)組的元素,包
括結(jié)束時(shí)的NULL元素,接著是envp的元素。這就是全部。在棧頂上沒(méi)有返回地址。
那,_start是如何退出的?它調(diào)用了exit()函數(shù)!畢竟,這就是它出現(xiàn)的作用。
實(shí)際上,我說(shuō)謊了。它真正做的是調(diào)用_exit()函數(shù)。[譯注:原文如此。標(biāo)準(zhǔn)啟動(dòng)中
_start還是調(diào)用exit()。](注意前面的下劃線。)exit()要為進(jìn)程進(jìn)行某些任務(wù)的結(jié)
束處理,但是這些任務(wù)將不會(huì)被啟動(dòng),因?yàn)槲覀兝@過(guò)了庫(kù)的啟動(dòng)代碼。所以我們也需要
繞過(guò)庫(kù)的結(jié)束代碼,直接到達(dá)操作系統(tǒng)的結(jié)束處理。
好,讓我們?cè)僭囈幌?。我們將要調(diào)用_exit(),這是一個(gè)需要一個(gè)整數(shù)參數(shù)的函數(shù)。所
以我們需要做的就是把那個(gè)數(shù)壓到棧上并調(diào)用該函數(shù)。(我們還需要聲明_exit()為外
部)下面是我們的匯編:
; tiny.asm
BITS 32
EXTERN _exit
GLOBAL _start
SECTION .text
_start:
push dword 42
call _exit
然后我們像前面那樣build和測(cè)試:
$ nasm -f elf tiny.asm
$ gcc -Wall -s -nostartfiles tiny.o
$ ./a.out ; echo $?
42
終于成功了!現(xiàn)在看它有多大:
$ wc -c a.out
1340 a.out
幾乎只有一半的大??!不錯(cuò)。真得不錯(cuò)。Hmmm...那gcc還有什么別的有意思的選項(xiàng)嗎?
這一個(gè),在文檔中緊接著-nostartfiles的,很是顯眼:
-nostdlib
Don't use the standard system libraries and startup files when
linking. Only the files you specify will be passed to the linker.
這值得研究一下:
$ gcc -Wall -s -nostdlib tiny.o
tiny.o(.text+0x6): undefined reference to `_exit'
Oops。是的..._exit()畢竟是一個(gè)庫(kù)函數(shù)。它必須要被填充。
好吧。但是肯定,我們并不需要libc的幫助來(lái)結(jié)束一個(gè)程序,不是嗎?
是的,我們不需要。如果我們?cè)敢鈷仐壦锌梢浦残砸?,我們可以退出程序而不需?br>
和任何其他東西鏈接。然而,首先我們需要了解如何在Linux下進(jìn)行系統(tǒng)調(diào)用。
――――――――――――――――――――――――――――――――――――――
Linux,像大多數(shù)操作系統(tǒng)一樣,通過(guò)系統(tǒng)調(diào)用對(duì)它支持的程序提供基本的必需功能。這
包括打開(kāi)文件,讀寫(xiě)文件句柄――當(dāng)然,也包括結(jié)束一個(gè)進(jìn)程。
Linux系統(tǒng)調(diào)用接口是一條指令:int 0x80。所有的系統(tǒng)調(diào)用都通過(guò)這個(gè)中斷進(jìn)行。要進(jìn)
行一個(gè)系統(tǒng)調(diào)用,eax應(yīng)當(dāng)包含一個(gè)數(shù)來(lái)指明那個(gè)系統(tǒng)調(diào)用被調(diào)用,并且其他寄存器用于
傳遞參數(shù),如果有的話。如果系統(tǒng)調(diào)用需要一個(gè)參數(shù),它將在ebx里;兩個(gè)參數(shù)的系統(tǒng)調(diào)
將使用ebx和ecx。類似的,edx,esi,和edi將分別被使用,如果需要第三、第四、第五
個(gè)參數(shù)的話。當(dāng)從一個(gè)系統(tǒng)調(diào)用返回后,eax將包含返回值。如果發(fā)生錯(cuò)誤,eax將包含
一個(gè)負(fù)值,其絕對(duì)值指出錯(cuò)誤。
不同系統(tǒng)調(diào)用的號(hào)碼在/usr/include/asm/unistd.h中列出。查看一下就知道exit系統(tǒng)調(diào)
用被分配的號(hào)碼是1。類似于C函數(shù),它需要一個(gè)參數(shù),即返回給父進(jìn)程的值,所以這將
通過(guò)ebx傳遞。
現(xiàn)在我們知道了如何創(chuàng)建我們程序的下一個(gè)版本,這個(gè)版本不需要任何外部函數(shù)的輔助
就可以工作:
; tiny.asm
BITS 32
GLOBAL _start
SECTION .text
_start:
mov eax, 1
mov ebx, 42
int 0x80
接下來(lái):
$ nasm -f elf tiny.asm
$ gcc -Wall -s -nostdlib tiny.o
$ ./a.out ; echo $?
42
哈哈!大小是?
$ wc -c a.out
372 a.out
現(xiàn)在已經(jīng)是非常小了。幾乎是上一個(gè)版本大小的四分之一。
那...我們還能做些什么把它變得更小嗎?
使用更短的指令怎么樣?
如果我們?yōu)閰R編代碼生成一個(gè)list文件,就會(huì)發(fā)現(xiàn)如下:
00000000 B801000000 mov eax, 1
00000005 BB2A000000 mov ebx, 42
0000000A CD80 int 0x80
嗯,我們不需要初始化ebx的全部,因?yàn)椴僮飨到y(tǒng)只使用最低字節(jié)。只設(shè)置bl就足夠了,
這將占用兩個(gè)字節(jié)而不是五個(gè)。
我們還可以通過(guò)把eax xor成0然后使用一個(gè)字節(jié)的增量指令來(lái)把eax設(shè)成1。這將又節(jié)省
兩個(gè)字節(jié)。
00000000 31C0 xor eax, eax
00000002 40 inc eax
00000003 B32A mov bl, 42
00000005 CD80 int 0x80
我想現(xiàn)在說(shuō)我們?cè)僖膊荒馨堰@個(gè)程序變得更小已經(jīng)很安全了。
另外,我們可以不再用gcc來(lái)鏈接我們的可執(zhí)行文件,因?yàn)槲覀儧](méi)有使用它的任何附加
功能,我們可以自己調(diào)用鏈接器,ld:
$ nasm -f elf tiny.asm
$ ld -s tiny.o
$ ./a.out ; echo $?
42
$ wc -c a.out
368 a.out
又小了4個(gè)字節(jié)。(嘿!我們不是砍掉了5個(gè)字節(jié)嗎?是的,我們是砍了5個(gè)字節(jié),但是
ELF文件的對(duì)齊考慮導(dǎo)致它需要一個(gè)額外字節(jié)的填充。)
那么...我們到頭了么?這就是我們所能達(dá)到的最小么?
姆。我們的程序現(xiàn)在是7個(gè)字節(jié)長(zhǎng)。ELF文件真的需要361字節(jié)的開(kāi)銷?文件里面到底是
什么?
我們可以使用objdump察看文件的內(nèi)容:
$ objdump -x a.out | less
輸出可能看起來(lái)有點(diǎn)混亂,但現(xiàn)在讓我們集中看一下節(jié)(section)列表:
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節(jié)在列表中是7字節(jié)長(zhǎng),正如我們所指出的。所以好像可以下結(jié)論說(shuō)我們現(xiàn)
在可以完全控制程序的機(jī)器語(yǔ)言內(nèi)容了。
但是還有另一個(gè)名為“.comment”的節(jié)。為什么會(huì)有它?并且它有28字節(jié)長(zhǎng),竟然!我
們不能確定這個(gè).comment節(jié)是什么,但是好像它并不是必需的...
.comment節(jié)在列表中顯示位于文件偏移00000087(十六進(jìn)制)。如果我們使用一個(gè)
hexdump程序來(lái)查看一下文件該區(qū)域的內(nèi)容,會(huì)發(fā)現(xiàn):
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
噢,噢,噢。誰(shuí)會(huì)想到Nasm會(huì)這樣破壞我們所追求的東西呢?或許我們需要轉(zhuǎn)而使用gas
,盡管要用AT&T語(yǔ)法...
哎,如果我們這樣做:
; tiny.s
.globl _start
.text
_start:
xorl %eax, %eax
incl %eax
movb $42, %bl
int $0x80
...我們發(fā)現(xiàn):
$ gcc -s -nostdlib tiny.s
$ ./a.out ; echo $?
42
$ wc -c a.out
368 a.out
...沒(méi)有區(qū)別!
實(shí)際上,是有一點(diǎn)區(qū)別的。再次使用objdump,我們會(huì)發(fā)現(xiàn):
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
沒(méi)有了comment節(jié),但是現(xiàn)在有兩個(gè)沒(méi)有用處的節(jié)來(lái)存儲(chǔ)并不存在的數(shù)據(jù)。盡管這些節(jié)
是0字節(jié)長(zhǎng),它們確實(shí)有開(kāi)銷,毫無(wú)道理的使我們的文件體積變大。
那么,這些開(kāi)銷是什么呢?我們?cè)鯓尤サ羲?br>
為了回答這些問(wèn)題,我們必須深入一些。我們需要理解ELF格式。
――――――――――――――――――――――――――――――――――――――
描述Intel-386體系結(jié)構(gòu)ELF格式的規(guī)范文檔可以在
ftp://tsx.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.gz找到。如果你不喜歡Postscript文檔,你可以在
http://http://www.muppetlabs.com/~breadbox/...??規(guī)范覆蓋了很多領(lǐng)域,所以如果你不想讀完整個(gè)文檔,我可以理解?;旧?,下面的東西是我們
需要知道的:
每個(gè)ELF文件都以一個(gè)稱為ELF頭的結(jié)構(gòu)開(kāi)始。這個(gè)結(jié)構(gòu)是52字節(jié)長(zhǎng),包含一些描述文件
內(nèi)容的信息。例如,最開(kāi)始的16個(gè)字節(jié)包含一個(gè)“標(biāo)識(shí)”,包括文件的幻數(shù)(magic-
number)簽名(7F 45 4C 46),及一些一字節(jié)標(biāo)志,用來(lái)指示文件內(nèi)容是32位還是64
位,little-endian還是big-endian,等等。ELF頭中的其他域包括:目標(biāo)體系結(jié)構(gòu);
ELF文件是一個(gè)可執(zhí)行文件,一個(gè)目標(biāo)文件,還是一個(gè)共享庫(kù)(shared-object library
);程序的起始地址;以及程序頭表(program header table)和節(jié)頭表(section
header table)在文件中的位置。
這兩個(gè)表可以位于文件中任何地方,但是通常前者緊接著ELF頭,后者位于或接近于文件
尾。這兩個(gè)表的目的類似,他們標(biāo)識(shí)文件的組成部分。但是,節(jié)頭表更側(cè)重于標(biāo)識(shí)程序
的各部分位于文件中什么地方,而程序頭表描述這些部分如何以及被裝載的內(nèi)存中的何
處。簡(jiǎn)單的說(shuō),節(jié)頭表是給編譯器和鏈接器用的,而程序頭表是給程序裝載器用的。程
序頭表對(duì)目標(biāo)文件是可選的,并且在實(shí)際中目標(biāo)文件從來(lái)沒(méi)有它。類似,節(jié)頭表對(duì)可執(zhí)
行文件是可選的――但是可執(zhí)行文件幾乎都有它。
好了,這就是我們第一個(gè)問(wèn)題的答案。我們程序中的相當(dāng)一部分開(kāi)銷是完全不必要的節(jié)
頭表,以及可能是同樣沒(méi)有用處的節(jié),這些節(jié)不參與程序的內(nèi)存映像。
來(lái)看我們的第二個(gè)問(wèn)題:我們?nèi)绾稳サ暨@些東西?
哎,我們只能靠自己。沒(méi)有任何標(biāo)準(zhǔn)工具會(huì)被設(shè)計(jì)成可以產(chǎn)生沒(méi)有節(jié)頭表的可執(zhí)行文件
。
如果我們想這么做,我們必須自己動(dòng)手。
這并不意味著我們必須找一個(gè)二進(jìn)制編輯器并且手工寫(xiě)下十六進(jìn)制值。老Nasm有一種平
板二進(jìn)制輸出格式,這剛好可以為我們所用?,F(xiàn)在我們所需要的就是一個(gè)空ELF可執(zhí)行文
件的映像,從而我們可以填充進(jìn)我們自己的程序。我們程序,沒(méi)有什么別的了。
我們可以查看ELF規(guī)范,和/usr/include/linux/elf.h,以及標(biāo)準(zhǔn)工具創(chuàng)建的可執(zhí)行文
件,來(lái)推測(cè)出空的ELF可執(zhí)行文件應(yīng)該是什么樣子。但是,如果你是那種沒(méi)有耐心的人,
你可以直接使用下面我提供的這個(gè):
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 $ - $$
這個(gè)映像包含一個(gè)ELF頭,指出文件是一個(gè)Intel-386可執(zhí)行文件,沒(méi)有節(jié)頭表,有一個(gè)
包含一項(xiàng)的程序頭表。那一項(xiàng)指示程序裝載器把整個(gè)文件裝載到內(nèi)存(程序在其內(nèi)存映
像中包含ELF頭和程序頭表是正常行為)中內(nèi)存地址0x08048000處(這是可執(zhí)行文件裝
載的缺省地址),然后從_start開(kāi)始執(zhí)行代碼,_start出現(xiàn)在緊接著程序頭表處。沒(méi)有
.data段,沒(méi)有.bss段,沒(méi)有注釋――除了必需的東西外什么也沒(méi)有。
那,讓我們加入我們自己的小程序:
; 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
我們完全通過(guò)拼湊(from scratch)創(chuàng)建了一個(gè)可執(zhí)行文件。如何?現(xiàn)在,看看它的大
?。?br>
$ wc -c a.out
91 a.out
91個(gè)字節(jié)。是我們上一次的四分之一大小還不到,是我們最初版本大小的四十分之一還
不到!
而且,這一次我們可以算計(jì)每一個(gè)字節(jié)。我們確切的知道可執(zhí)行文件中是什么東西,以
及為什么需要它。終于,到達(dá)了極限。我們?cè)僖膊荒茏龅酶×恕?br>
是么?
――――――――――――――――――――――――――――――――――――――
如果你真地讀了ELF規(guī)范,你可能會(huì)發(fā)現(xiàn)幾個(gè)事實(shí)。1)ELF文件的不同部分可以位于任何
位置(除了ELF頭,它必須位于文件頭部),并且它們可以相互重疊。2)頭中的某些域
并沒(méi)有真正被使用。
具體地說(shuō),我在考慮那16字節(jié)的標(biāo)識(shí)域尾部的9個(gè)字節(jié)的0。他們純是填充,為ELF標(biāo)準(zhǔn)將
來(lái)的擴(kuò)展留下空間。所以O(shè)S不應(yīng)該關(guān)心那里面是什么東西。并且我們已經(jīng)把所有的東西
都裝載到內(nèi)存了,而我們的程序只有7字節(jié)長(zhǎng)...
我們可以把自己的代碼放進(jìn)ELF頭里面么?
為什么不呢?
; 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 $ - $$
畢竟,一個(gè)字節(jié)也是字節(jié)?。?br>
$ nasm -f bin -o a.out tiny.asm
$ chmod +x a.out
$ ./a.out ; echo $?
42
$ wc -c a.out
84 a.out
還不錯(cuò)吧?
現(xiàn)在我們真的走到頭了。我們的文件只有一個(gè)ELF頭和一個(gè)程序頭表項(xiàng),要把程序裝進(jìn)
內(nèi)存并運(yùn)行這兩者都是絕對(duì)需要的。所以現(xiàn)在沒(méi)有什么可以縮減了。
除了...
如果我們能對(duì)程序頭表做一下剛才對(duì)程序所做的事情會(huì)怎么樣?也就是說(shuō),把它和ELF
頭重疊。這可能么?
這真的可能。看一下我們的程序。注意ELF頭中的最后8個(gè)字節(jié)和程序頭表的開(kāi)始8個(gè)字
節(jié)有某種相像。這種相像可以被描述為“相同”。
所以...
; 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 $ - $$
并且肯定,Linux一點(diǎn)也不在意我們的吝嗇:
$ nasm -f bin -o a.out tiny.asm
$ chmod +x a.out
$ ./a.out ; echo $?
42
$ wc -c a.out
76 a.out
現(xiàn)在我們真的到了最低了。再?zèng)]有辦法把兩個(gè)結(jié)構(gòu)重疊了。它們的字節(jié)不匹配。這是底
限了!
除非,我們能夠修改結(jié)構(gòu)的內(nèi)容使它們匹配的更多。
到底Linux實(shí)際查看了這些域中的多少呢?例如,Linux真的檢查e_machine域包含3(指
示為Intel-386目標(biāo)),還是僅僅假定它就是?
實(shí)際上,在上面例子中Linux真的檢查。但是很多的其他域都被悄悄的忽略了。
下面是ELF頭中的必要部分。首四個(gè)字節(jié)必須包含幻數(shù),否則Linux不會(huì)執(zhí)行它。然而,
e_ident域中的其它3個(gè)字節(jié)不被檢查,這意味著我們有不少于12個(gè)連續(xù)的字節(jié)可以設(shè)置
成任何東西。e_type必須被置成2,來(lái)指示是可執(zhí)行文件,e_machine必須是3,正如剛
才所說(shuō)的。e_version,像e_ident中的版本號(hào)一樣,完全被忽略。(這是可以理解的,
因?yàn)槟壳癊LF標(biāo)準(zhǔn)只有一個(gè)版本。)e_entry自然必須有效,因?yàn)樗赶虺绦虻拈_(kāi)始。并
且顯然,e_phoff需要包含程序頭表在文件中正確的偏移,e_phnum需要包含這個(gè)表中正
確的項(xiàng)數(shù)。然而,e_flag在文檔中指出現(xiàn)在對(duì)Intel來(lái)說(shuō)沒(méi)有使用,所以它可以被我們
利用。e_ehsize應(yīng)該被用于驗(yàn)證ELF頭有期望的大小,但是Linux沒(méi)有管它。e_phentsize
類似,用于驗(yàn)證程序頭表項(xiàng)的大小。它被檢查了,但是只是在2.2內(nèi)核的版本2.2.17之
后。2.2內(nèi)核的早期版本忽略了它,2.4.0也忽略了。ELF頭中的其它東西是關(guān)于節(jié)頭表
的,這在可執(zhí)行文件中沒(méi)有作用。
程序頭表項(xiàng)又如何呢?p_type必須包含1,標(biāo)志它是一個(gè)可裝載段。p_offset也真的需
要包含開(kāi)始裝載的正確的文件偏移。類似,p_vaddr需要包含適當(dāng)?shù)难b載地址。注意,
我們并沒(méi)有被要求裝載到0x08048000。幾乎可以使用任何地址,只要它位于0x0000000
之上,0x80000000之下,并且是頁(yè)對(duì)齊的。p_paddr域在文檔中指出是被忽略的,所以
它是可用的。p_filesz指示從文件中裝載多少到內(nèi)存,p_memsz指示內(nèi)存段需要有多大,
所以這些數(shù)應(yīng)該是健康的。p_flags只是要給予內(nèi)存段什么權(quán)限。它需要是可讀的(4)
,否則根本不可用,并且需要是可執(zhí)行的(1),否則我們不能執(zhí)行其中的代碼。其他
位也可以被設(shè)置,但是我們至少需要這些。最后,p_align給出內(nèi)存段的對(duì)齊要求。這
個(gè)域主要在重定位包含位置無(wú)關(guān)代碼(position-independent code)的段(如對(duì)于共
享庫(kù))時(shí)使用,所以對(duì)于可執(zhí)行文件Linux將忽略我們存儲(chǔ)在里面的任何垃圾信息。
總而言之,還是有很多回旋余地的。特別的,仔細(xì)的審查可以發(fā)現(xiàn)ELF頭中的大部分必
需域位于前半部分――后半部分幾乎可以完全用來(lái)種綠豆(free for munging)。知道
了這些,我們可以把兩個(gè)結(jié)構(gòu)重疊的更多一些:
; 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 $ - $$
(希望)你可以看到,現(xiàn)在程序頭表的開(kāi)始12個(gè)字節(jié)與ELF頭的最后12個(gè)字節(jié)重疊了。
實(shí)際上,這兩者吻合得非常好。ELF頭中重疊區(qū)域里面只有兩部分有關(guān)系。第一個(gè)是
e_phnum域,它剛好遇p_paddr域一致,p_paddr是程序頭表中確定被忽略的少數(shù)域之一。
另一個(gè)是e_phentsize域,它和p_vaddr域的頭半部一致。這是通過(guò)為我們的程序選擇一
個(gè)非標(biāo)準(zhǔn)的裝載地址而達(dá)到的,其頭半部分等于0x0020。
現(xiàn)在我們真的拋棄了所有的可移植性...
$ nasm -f bin -o a.out tiny.asm
$ chmod +x a.out
$ ./a.out ; echo $?
42
$ wc -c a.out
64 a.out
...但是它能工作!并且程序又小了12字節(jié),正如我們預(yù)測(cè)的。
這就是我所說(shuō)的我們?cè)僖膊荒鼙冗@做得更好了,但是當(dāng)然,我們已經(jīng)知道了我們可以―
―如果我們能夠把程序頭表完全放進(jìn)ELF頭中。這能做到么?
我們不能簡(jiǎn)單得把它再移上12字節(jié)而不遇到?jīng)]有希望的障礙――需要使兩個(gè)結(jié)構(gòu)中幾個(gè)
域匹配。僅有的另一種可能是讓它緊接著開(kāi)始的4個(gè)字節(jié)開(kāi)始。這可以把程序頭表的前半
部分舒適地放進(jìn)e_ident區(qū)域中,但是其余部分還有問(wèn)題。在一些試驗(yàn)之后,看起來(lái)這
不太可能達(dá)到了。
然而,結(jié)果表明程序頭表中還有幾個(gè)域我們可以使用的。
我們指出了p_memsz指示為內(nèi)存段分配多少內(nèi)存。顯然它至少要和p_filesz一樣大,但是
如果它更大也不會(huì)有什么危害...
其次,結(jié)果證明,與每個(gè)人的期望相反,可執(zhí)行位可以從p_flags域中去掉,而Linux將
為我們把它置位。為什么會(huì)這樣,老實(shí)說(shuō)我并不知道――或許是因?yàn)長(zhǎng)inux發(fā)現(xiàn)入口點(diǎn)
位于這個(gè)段中?不管如何,它可以工作。
所以,有了這些事實(shí),我們可以把文件重新組織成這個(gè)小畸形物:
; 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,如我們指出的這么做我們能夠脫身。這個(gè)4也是e_phoff域的
值,它給出了程序頭表在文件中的偏移,這剛好是我們放它的地方。程序(還記得它
嗎?)被移動(dòng)到了ELF頭的低半部分,從e_shoff域開(kāi)始并結(jié)束于e_flags域中。
注意裝載地址被變成了一個(gè)更低的數(shù)――盡可能的低,實(shí)際上是。這使得e_entry域保
持為一個(gè)小的數(shù),這樣有好處因?yàn)樗彩莗_memesz數(shù)。(實(shí)際上,對(duì)于虛擬內(nèi)存這幾乎
沒(méi)有關(guān)系――我們可以保留它為原先的值可能也能正常工作。但是禮貌一些總沒(méi)有壞
處。)
那現(xiàn)在...
$ nasm -f bin -o a.out tiny.asm
$ chmod +x a.out
$ ./a.out ; echo $?
42
$ wc -c a.out
52 a.out
...現(xiàn)在,程序頭表和程序自身都完全嵌入到了ELF頭中,我們的可執(zhí)行文件現(xiàn)在剛好就
是ELF頭的大小。不大不小。并且仍然能在Linux下順利運(yùn)行。
現(xiàn)在,最終,我們真真的并且當(dāng)然的到達(dá)了絕對(duì)的最小可能值。這沒(méi)什么問(wèn)題了,是吧
?畢竟,我們必須要有一個(gè)完整的ELF頭(盡管它被破壞得亂七八糟),否則Linux不會(huì)
理我們!
對(duì)嗎?
錯(cuò)。我們還有最后一個(gè)dirty技巧沒(méi)有用。
情況是如果文件不是一個(gè)完整的ELF頭的大小,Linux仍然能工作,并且用0填充所缺的字
節(jié)。我們?cè)谖募膊恐辽儆?個(gè)0,如果我們把它們從文件映像中扔掉:
; 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 $ - $$
...我們?nèi)匀荒軌?,不可思議地,產(chǎn)生一個(gè)能工作的可執(zhí)行文件:
$ nasm -f bin -o a.out tiny.asm
$ chmod +x a.out
$ ./a.out ; echo $?
42
$ wc -c a.out
45 a.out
現(xiàn)在,終于終于,我們真的到了我們所能達(dá)到的地方。我們無(wú)法避免文件中的第45個(gè)字
節(jié),它指出程序頭表中的項(xiàng)數(shù),需要為非零,需要存在,并且需要位于從ELF頭開(kāi)始的
第45個(gè)位置處的事實(shí)。我們被迫要承認(rèn)再?zèng)]有什么可以做的了。
――――――――――――――――――――――――――――――――――――――
這個(gè)45字節(jié)的文件比我們用標(biāo)準(zhǔn)工具所能創(chuàng)建的最小的ELF可執(zhí)行文件的八分之一都要
小,并且比我們使用純C代碼所能創(chuàng)建的最小文件的四十分之一都要小。我們已經(jīng)把任
何可能的東西都從文件中剔除了,并且盡可能得讓文件中的內(nèi)容具有雙重目的。
當(dāng)然,這個(gè)文件中的半數(shù)內(nèi)容違反了ELF標(biāo)準(zhǔn)的某些部分,然而Linux仍然愿意認(rèn)它(
sneeze on it),這真是一個(gè)奇跡,更不用說(shuō)Linux還會(huì)給它一個(gè)進(jìn)程ID了。這不是那
種人們?cè)敢馔侣镀渥髡呱矸莸某绦颉?br>
另一方面,這個(gè)可執(zhí)行文件中的每一個(gè)字節(jié)都是有理由存在并且可以被證明的。最近你
創(chuàng)建了多少可以這么說(shuō)的可執(zhí)行文件呢?