国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費(fèi)電子書(shū)等14項(xiàng)超值服

開(kāi)通VIP
怎樣創(chuàng)建真正很小的Linux下的ELF可執(zhí)行文件
http://blog.chinaunix.net/uid-223060-id-2215318.html
2001 
原文地址:

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í)行文件呢? 
本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)。
打開(kāi)APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
NASM匯編初探(入門教程)
NASM匯編筆記
[原創(chuàng)]CRC技術(shù)的實(shí)現(xiàn)與破解(10.21更新)
【Linux操作系統(tǒng)分析】進(jìn)程的創(chuàng)建與可執(zhí)行程序的加載
linux簡(jiǎn)單之美
nasm calling subroutine from another file
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服