by falcon <zhangjinw@gmail.com>
2008-02-23
這篇blog從減少可執(zhí)行文件大小的角度分析了ELF文件,期間通過經(jīng)典的"Hello World"實(shí)例逐步演示如何通過各種常用工具來分析ELF文件,并逐步精簡代碼。
為了能夠盡量減少可執(zhí)行文件的大小,我們必須了解可執(zhí)行文件的格式,以及鏈接生成可執(zhí)行文件時(shí)的后臺(tái)細(xì)節(jié)(即最終到底有哪些內(nèi)容被鏈接到了目標(biāo)代碼中)。
通過選擇合適的可執(zhí)行文件格式并剔除對可執(zhí)行文件的最終運(yùn)行沒有影響的內(nèi)容,就可以實(shí)現(xiàn)目標(biāo)代碼的裁減。因此,通過探索減少可執(zhí)行文件大小的方法,就相當(dāng)
于實(shí)踐性地去探索了可執(zhí)行文件的格式以及鏈接過程的細(xì)節(jié)。
當(dāng)然,算法的優(yōu)化和編程語言的選擇可能對目標(biāo)文件的大小有很大的影響,在這篇blog的后面我們會(huì)跟參考資料[1]的作者那樣去探求一個(gè)打印“Hello World”的可執(zhí)行文件能夠小到什么樣的地步。
1、可執(zhí)行文件格式的選取
可執(zhí)行文件格式的選擇要滿足的一個(gè)基本條件是:目標(biāo)系統(tǒng)支持該可執(zhí)行文件格式,資料[2]分析和比較了UNIX平臺(tái)下的三種可執(zhí)行文件格式,這三種格式實(shí)際上代表著可執(zhí)行文件的一個(gè)發(fā)展過程。
a.out文件格式
非常緊湊,只包含了程序運(yùn)行所必須的信息(文本、數(shù)據(jù)、BSS),而且每個(gè) section的順序是固定的。
coff文件格式雖然引入了一個(gè)節(jié)區(qū)表以支持更多節(jié)區(qū)信息,從而提高了可擴(kuò)展性,但是這種文件格式的重定位在鏈接時(shí)就已經(jīng)完成,因此不支持動(dòng)態(tài)鏈接(不過擴(kuò)展的coff支持)。
elf文件格式不僅動(dòng)態(tài)鏈接,而且有很好的擴(kuò)展性。它可以描述可重定位文件、可執(zhí)行文件和可共享文件(動(dòng)態(tài)鏈接庫)三類文件。
下面來看看ELF文件的結(jié)構(gòu)圖。
文件頭部(ELF Header)
程序頭部表(Program Header Table)
節(jié)區(qū)1(Section1)
節(jié)區(qū)2(Section2)
節(jié)區(qū)3(Section3)
...
節(jié)區(qū)頭部(Section Header Table)
無論是文件頭部、程序頭部表、節(jié)區(qū)頭部表還是各個(gè)節(jié)區(qū),都是通過特定的結(jié)構(gòu)體(struct)描述的,這些結(jié)構(gòu)在elf.h文件中定義。文件頭部用于描述
整個(gè)文件的類型、大小、運(yùn)行平臺(tái)、程序入口、程序頭部表和節(jié)區(qū)頭部表等信息。例如,我們可以通過文件頭部查看該ELF文件的類型。
Quote: |
$ cat hello.c #典型的hello, world程序 #include <stdio.h>
int main(void) { printf("hello, world!\n"); return 0; } $ gcc -c hello.c #編譯,產(chǎn)生可重定向的目標(biāo)代碼 $ readelf -h hello.o | grep Type #通過readelf查看文件頭部找出該類型 Type: REL (Relocatable file) $ gcc -o hello hello.o #生成可執(zhí)行文件 $ readelf -h hello | grep Type Type: EXEC (Executable file) $ gcc -fpic -shared -W1,-soname,libhello.so.0 -o libhello.so.0.0 hello.o #生成共享庫 $ readelf -h libhello.so.0.0 | grep Type Type: DYN (Shared object file)
|
那節(jié)區(qū)頭部表(將簡稱節(jié)區(qū)表)和程序頭部表有什么用呢?實(shí)際上前者只對可重定向文件有用,而后者只對可執(zhí)行文件和可共享文件有用。
節(jié)區(qū)表是用來描述各節(jié)區(qū)的,包括各節(jié)區(qū)的名字、大小、類型、虛擬內(nèi)存中的位置、相對文件頭的位置等,這樣所有節(jié)區(qū)都通過節(jié)區(qū)表給描述了,這樣連接器就可以
根據(jù)文件頭部表和節(jié)區(qū)表的描述信息對各種輸入的可重定位文件進(jìn)行合適的鏈接,包括節(jié)區(qū)的合并與重組、符號的重定位(確認(rèn)符號在虛擬內(nèi)存中的地址)等,把各
個(gè)可重定向輸入文件鏈接成一個(gè)可執(zhí)行文件(或者是可共享文件)。如果可執(zhí)行文件中使用了動(dòng)態(tài)連接庫,那么將包含一些用于動(dòng)態(tài)符號鏈接的節(jié)區(qū)。我們可以通過
readelf -S(或objdump -h)查看節(jié)區(qū)表信息。
Quote: |
$ readelf -S hello #可執(zhí)行文件、可共享庫、可重定位文件默認(rèn)都生成有節(jié)區(qū)表 ... Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048114 000114 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048128 000128 000020 00 A 0 0 4 [ 3] .hash HASH 08048148 000148 000028 04 A 5 0 4 ... [ 7] .gnu.version VERSYM 0804822a 00022a 00000a 02 A 5 0 2 ... [11] .init PROGBITS 08048274 000274 000030 00 AX 0 0 4 ... [13] .text PROGBITS 080482f0 0002f0 000148 00 AX 0 0 16 [14] .fini PROGBITS 08048438 000438 00001c 00 AX 0 0 4 ...
|
三種類型文件的節(jié)區(qū)(各個(gè)常見節(jié)區(qū)的作用請參考資料[11])可能不一樣,但是有幾個(gè)節(jié)區(qū),例如.text, .data,
.bss是必須的,特別是.text,因?yàn)檫@個(gè)節(jié)區(qū)包含了代碼。如果一個(gè)程序使用了動(dòng)態(tài)鏈接庫(引用了動(dòng)態(tài)連接庫中的某個(gè)函數(shù)),那么需要.interp
節(jié)區(qū)以便告知系統(tǒng)使用什么動(dòng)態(tài)連接器程序來進(jìn)行動(dòng)態(tài)符號鏈接,進(jìn)行某些符號地址的重定位。通常,.rel.text節(jié)區(qū)只有可重定向文件有,用于鏈接時(shí)對
代碼區(qū)進(jìn)行重定向,而.hash,.plt,.got等節(jié)區(qū)則只有可執(zhí)行文件(或可共享庫)有,這些節(jié)區(qū)對程序的運(yùn)行特別重要。還有一些節(jié)區(qū),可能僅僅是
用于注釋,比如.comment,這些對程序的運(yùn)行似乎沒有影響,是可有可無的,不過有些節(jié)區(qū)雖然對程序的運(yùn)行沒有用處,但是卻可以用來輔助對程序進(jìn)行調(diào)
試或者對程序運(yùn)行效率有影響。
雖然三類文件都必須包含某些節(jié)區(qū),但是節(jié)區(qū)表對可重定位文件來說才是必須的,而程序的執(zhí)行卻不需要節(jié)區(qū)表,只需要程序頭部表以便知道如何加載和執(zhí)行文件。
不過如果需要對可執(zhí)行文件或者動(dòng)態(tài)連接庫進(jìn)行調(diào)試,那么節(jié)區(qū)表卻是必要的,否則調(diào)試器將不知道如何工作。下面來介紹程序頭部表,它可通過readelf
-l(或objdump -p)查看。
Quote: |
$ readelf -l hello.o #對于可重定向文件,gcc沒有產(chǎn)生程序頭部,因?yàn)樗鼘芍囟ㄏ蛭募]用
There are no program headers in this file.
$ readelf -l hello #而可執(zhí)行文件和可共享文件都有程序頭部
...
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00470 0x00470 R E 0x1000
LOAD 0x000470 0x08049470 0x08049470 0x0010c 0x00110 RW 0x1000
DYNAMIC 0x000484 0x08049484 0x08049484 0x000d0 0x000d0 RW 0x4
NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr
.gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini
.rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
$ readelf -l libhello.so.0.0 #節(jié)區(qū)和上面類似,這里省略
|
從上面可看出程序頭部表描述了一些段(Segment),這些段對應(yīng)著一個(gè)或者多個(gè)節(jié)區(qū),上面的readelf
-l很好地顯示了各個(gè)段與節(jié)區(qū)的映射。這些段描述了段的名字、類型、大小、第一個(gè)字節(jié)在文件中的位置、將占用的虛擬內(nèi)存大小、在虛擬內(nèi)存中的位置等。這樣
系統(tǒng)程序解釋器將知道如何把可執(zhí)行文件加載到內(nèi)存中以及進(jìn)行動(dòng)態(tài)鏈接等動(dòng)作。
該可執(zhí)行文件包含7個(gè)段,PHDR指程序頭部,INTERP正好對應(yīng).interp節(jié)區(qū),兩個(gè)LOAD段包含程序的代碼和數(shù)據(jù)部分,分別包含有.text
和.data,.bss節(jié)區(qū),DYNAMIC段包含.daynamic,這個(gè)節(jié)區(qū)可能包含動(dòng)態(tài)連接庫的搜索路徑、可重定位表的地址等信息,它們用于動(dòng)態(tài)連
接器。NOTE和GNU_STACK段貌似作用不大,只是保存了一些輔助信息。因此,對于一個(gè)不使用動(dòng)態(tài)連接庫的程序來說,可能只包含LOAD段,如果一
個(gè)程序沒有數(shù)據(jù),那么只有一個(gè)LOAD段就可以了。
總結(jié)一下,Linux雖然支持很多種可執(zhí)行文件格式,但是目前ELF較通用,所以選擇ELF作為我們的討論對象。通過上面對ELF文件分析發(fā)現(xiàn)一個(gè)可執(zhí)行
的文件可能包含一些對它的運(yùn)行沒用的信息,比如節(jié)區(qū)表、一些用于調(diào)試、注釋的節(jié)區(qū)。如果能夠刪除這些信息就可以減少可執(zhí)行文件的大小,而且不會(huì)影響可執(zhí)行
文件的正常運(yùn)行。
2、鏈接優(yōu)化
從上面的討論中已經(jīng)接觸了動(dòng)態(tài)連接庫。ELF中引入動(dòng)態(tài)連接庫后極大地方便了公共函數(shù)的共享,節(jié)約了磁盤和內(nèi)存空間,因?yàn)椴辉傩枰涯切┕埠瘮?shù)的代碼鏈接到可執(zhí)行文件,這將減少了可執(zhí)行文件的大小。
與此同時(shí),靜態(tài)鏈接可能會(huì)引入一些對代碼的運(yùn)行可能并非必須的內(nèi)容。你可以從
《GCC編譯的背后(第二部分:匯編和鏈接)》 了
解到GCC鏈接的細(xì)節(jié)。從那篇Blog中似乎可以得出這樣的結(jié)論:僅僅從是否影響一個(gè)C語言程序運(yùn)行的角度上說,GCC默認(rèn)鏈接到可執(zhí)行文件的幾個(gè)可重定
位文件(crt1.o,rti.o,crtbegin.o,crtend.o,crtn.o)并不是必須的,不過值得注意的是,如果沒有鏈接那些文件但在
程序末尾使用了return語句,main函數(shù)將無法返回,因此需要替換為_exit調(diào)用;另外,既然程序在進(jìn)入main之前有一個(gè)入口,那么main入
口就不是必須的。因此,如果不采用默認(rèn)鏈接也可以減少可執(zhí)行文件的大小。
3、可執(zhí)行文件“減肥”實(shí)例
這里主要是根據(jù)上面兩點(diǎn)來介紹如何減少一個(gè)可執(zhí)行文件的大小。以"Hello World"為例。
首先來看看默認(rèn)編譯產(chǎn)生的Hello World的可執(zhí)行文件大小。
系統(tǒng)默認(rèn)編譯
代碼同上,下面是一組演示,
Quote: |
$ uname -r #先查看內(nèi)核版本和gcc版本,以便和你的結(jié)果比較 2.6.22-14-generic $ gcc --version gcc (GCC) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2) ... $ gcc -o hello hello.c #默認(rèn)編譯 $ wc -c hello #產(chǎn)生一個(gè)大小為6442字節(jié)的可執(zhí)行文件 6442 hello
|
不采用默認(rèn)編譯
可以考慮編輯時(shí)就把return 0替換成_exit(0)并包含定義該函數(shù)的unistd.h頭文件。下面是從GCC編譯的背后(第二部分:匯編和鏈接)》總結(jié)出的Makefile文件。
Code:
[Ctrl+A Select All]
把上面的代碼復(fù)制到一個(gè)Makefile文件中,并利用它來編譯hello.c。
Quote: |
$ make #編譯 sed -i -e '/#include[ "<]*unistd.h[ ">]*/d;' -i -e '1i #include <unistd.h>' -i -e 's/return 0;/_exit(0);/' hello.c cc -S hello.c sed -i -e 's/main/_start/g' hello.s cc -c hello.s ld -dynamic-linker /lib/ld-linux.so.2 -L /usr/lib/ -lc -o hello hello.o $ ./hello #這個(gè)也是可以正常工作的 Hello World $ wc -c hello #但是大小減少了4382個(gè)字節(jié),減少了將近70% 2060 hello $ echo "6442-2060" | bc
4382
$ echo "(6442-2060)/6442" | bc -l
.68022353306426575597
|
對于一個(gè)比較小的程序,能夠減少將近70%“沒用的”代碼。至于一個(gè)大一點(diǎn)的程序(這個(gè)代碼是[1]的作者寫的一個(gè)小工具,我們后面會(huì)使用它)再看看效果。
Quote: |
$ gcc -o sstrip sstrip.c #默認(rèn)編譯的情況 $ wc -c sstrip 10912 sstrip $ sed -i -e "s/hello/sstrip/g" Makefile #把Makefile中的hello替換成sstrip $ make clean #清除默認(rèn)編譯的sstrip $ make #用我們的Makefile編譯 $ wc -c sstrip 6589 sstrip $ echo "10912-6589" | bc -l #再比較大小,減少的代碼還是4323個(gè)字節(jié),減幅40% 4323 $ echo "(10912-6589)/10912" | bc -l .39616935483870967741
|
通過這兩個(gè)簡單的實(shí)驗(yàn),我們發(fā)現(xiàn),能夠減少掉4000個(gè)字節(jié)左右,相當(dāng)于4k左右。
刪除對程序運(yùn)行沒有影響的節(jié)區(qū)
使用上述Makefile來編譯程序,不鏈接那些對程序運(yùn)行沒有多大影響的文件,實(shí)際上也相當(dāng)于刪除了一些“沒用”的節(jié)區(qū),可以通過下列演示看出這個(gè)實(shí)質(zhì)。
Quote: |
$ sed -i -e "s/sstrip/hello/g" Makefile #先看看用Makefile編譯的結(jié)果,替換回hello $ make clean $ make $ readelf -l hello | grep "0[0-9]\ \ " 00 01 .interp 02 .interp .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.plt .plt .text .rodata 03 .dynamic .got.plt 04 .dynamic 05 $ make clean $ gcc -o hello hello.c $ readelf -l hello | grep "0[0-9]\ \ " 00 01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr
.gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini
.rodata .eh_frame 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag 06
|
通過比較發(fā)現(xiàn)使用自定義的Makefile文件,少了這么多節(jié)區(qū):.bss .ctors .data .dtors .eh_frame .fini .gnu.hash .got .init .jcr .note.ABI-tag .rel.dyn。
再看看還有哪些節(jié)區(qū)可以刪除呢?通過之前的分析發(fā)現(xiàn)有些節(jié)區(qū)是必須的,那.hash?.gnu.version?呢,通過strip -R(或objcop -R)刪除這些節(jié)區(qū)試試。
Quote: |
$ wc -c hello #查看大小,以便比較 2060 $ time ./hello #我們比較一下一些節(jié)區(qū)對執(zhí)行時(shí)間可能存在的影響 Hello World
real 0m0.001s user 0m0.000s sys 0m0.000s $ strip -R .hash hello #刪除.hash節(jié)區(qū) $ wc -c hello 1448 hello $ echo "2060-1448" | bc #減少了612字節(jié) 612 $ time ./hello #發(fā)現(xiàn)執(zhí)行時(shí)間長了一些(實(shí)際上也可能是進(jìn)程調(diào)度的問題) Hello World
real 0m0.006s user 0m0.000s sys 0m0.000s $ strip -R .gnu.version hello #刪除.gnu.version還是可以工作 $ wc -c hello 1396 hello $ echo "1448-1396" | bc #又減少了52字節(jié) 52 $ time ./hello Hello World
real 0m0.130s user 0m0.004s sys 0m0.000s $ strip -R .gnu.version_r hello #刪除.gnu.version_r就不工作了 $ time ./hello ./hello: error while loading shared libraries: ./hello: unsupported version 0 of Verneed record
|
通過刪除各個(gè)節(jié)區(qū)可以查看哪些節(jié)區(qū)對程序來說是必須的,不過有些節(jié)區(qū)雖然并不影響程序的運(yùn)行卻可能會(huì)影響程序的執(zhí)行效率,這個(gè)可以上面的運(yùn)行時(shí)間看出個(gè)大概。
通過刪除兩個(gè)“沒用”的節(jié)區(qū),我們又減少了52+612,即664字節(jié)。
刪除可執(zhí)行文件的節(jié)區(qū)表
用普通的工具沒有辦法刪除節(jié)區(qū)表,但是參考資料[1]的作者已經(jīng)寫了這樣一個(gè)工具。你可以從這里http://www.muppetlabs.com/~breadbox/software/elfkickers.html下載到那個(gè)工具,即我們上面作為一個(gè)演示例子的sstrip,它是該作者寫的一序列工具ELFkickers中的一個(gè)。下載以后,編譯,并復(fù)制到/usr/bin下,下面用它來刪除節(jié)區(qū)表。
Quote: |
$ sstrip hello #刪除ELF可執(zhí)行文件的節(jié)區(qū)表 $ ./hello #還是可以正常運(yùn)行,說明節(jié)區(qū)表對可執(zhí)行文件的運(yùn)行沒有任何影響 Hello World $ wc -c hello #大小只剩下708個(gè)字節(jié)了 708 hello $ echo "1396-708" | bc #又減少了688個(gè)字節(jié)。 688
|
通過刪除節(jié)區(qū)表又把可執(zhí)行文件減少了688字節(jié)。現(xiàn)在回頭看看相對于gcc默認(rèn)產(chǎn)生的可執(zhí)行文件,通過刪除一些節(jié)區(qū)和節(jié)區(qū)表到底減少了多少字節(jié)?減幅達(dá)到了多少?
Quote: |
$ echo "6442-708" | bc # 5734 $ echo "(6442-708)/6442" | bc -l .89009624340266997826
|
減少了5734多字節(jié),減幅將近90%,這說明:對于一個(gè)簡短的hello.c程序而言,gcc引入了將近90%的對程序運(yùn)行沒有影響的數(shù)據(jù)。雖然通過刪
除節(jié)區(qū)和節(jié)區(qū)表,使得最終的文件只有708字節(jié),但是打印一個(gè)"Hello World"真的需要這么多字節(jié)么?
事實(shí)上未必,因?yàn)椋?br> 1、打印一段Hello World字符串,我們無須調(diào)用printf,也就無須包含動(dòng)態(tài)連接庫,因此.interp,.dynamic等節(jié)區(qū)又可以去掉。為什么?我們可以直接使用系統(tǒng)調(diào)用(sys_write)來打印字符串。
2、另外,我們無須把Hello World字符串存放到可執(zhí)行文件中?而是讓用戶把它當(dāng)作參數(shù)輸入。
下面,繼續(xù)進(jìn)行可執(zhí)行文件的“減肥”。
4、用匯編語言來重寫"Hello World"
先來看看gcc默認(rèn)產(chǎn)生的匯編代碼情況。通過gcc的-S選項(xiàng)可得到匯編代碼。
Quote: |
$ cat hello.c #這個(gè)是使用_exit和printf函數(shù)的版本 #include <stdio.h> /* printf */ #include <unistd.h> /* _exit */
int main() { printf("Hello World\n"); _exit(0); } $ gcc -S hello.c #生成匯編 $ cat hello.s #這里是匯編代碼 .file "hello.c" .section .rodata .LC0: .string "Hello World" .text .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp movl $.LC0, (%esp) call puts movl $0, (%esp) call _exit .size main, .-main .ident "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)" .section .note.GNU-stack,"",@progbits $ gcc -o hello hello.s #看看默認(rèn)產(chǎn)生的代碼大小 $ wc -c hello 6523 hello
|
現(xiàn)在對匯編代碼(hello.s)進(jìn)行簡單的處理得到,
Code:
[Ctrl+A Select All]
再編譯看看,
Quote: |
$ gcc -o hello.o hello.s $ wc -c hello
6443 hello $ echo "6523-6443" | bc #僅僅減少了80個(gè)字節(jié) 80
|
如果不采用默認(rèn)編譯呢并且刪除掉對程序運(yùn)行沒有影響的節(jié)區(qū)和節(jié)區(qū)表呢?
Quote: |
$ sed -i -e "s/main/_start/g" hello.s #因?yàn)闆]有初始化,所以得直接進(jìn)入代碼,替換main為_start $ as -o hello.o hello.s $ ld -o hello hello.o --dynamic-linker /lib/ld-linux.so.2 -L /usr/lib -lc $ ./hello hello world! $ wc -c hello 1812 hello $ echo "6443-1812" | bc -l #和之前的實(shí)驗(yàn)類似,也減少了4k左右 4631 $ readelf -l hello | grep "\ [0-9][0-9]\ " 00 01 .interp 02 .interp .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.plt .plt .text 03 .dynamic .got.plt 04 .dynamic $ strip -R .hash hello $ strip -R .gnu.version hello $ wc -c hello 1200 hello $ sstrip hello $ wc -c hello #這個(gè)結(jié)果比之前的708(在刪除所有垃圾信息以后)個(gè)字節(jié)少了708-676,即32個(gè)字節(jié) 676 hello $ ./hello Hello World
|
容易發(fā)現(xiàn)這32字節(jié)可能跟節(jié)區(qū).rodata有關(guān)系,因?yàn)閯偛旁阪溄油暌院蟛榭垂?jié)區(qū)信息時(shí),并沒有.rodata節(jié)區(qū)。
前面提到,實(shí)際上還可以不用動(dòng)態(tài)連接庫中的printf函數(shù),也不用直接調(diào)用_exit,而是在匯編里頭使用系統(tǒng)調(diào)用,這樣就可以去掉和動(dòng)態(tài)連接庫關(guān)聯(lián)的內(nèi)容。(如果想了解如何在匯編中使用系統(tǒng)調(diào)用,請參考資料[9])。使用系統(tǒng)調(diào)用重寫以后得到如下代碼,
Code:
[Ctrl+A Select All]
現(xiàn)在編譯就不再需要?jiǎng)討B(tài)鏈接器ld-linux.so了,也不再需要鏈接任何庫。
Quote: |
$ as -o hello.o hello.s $ ld -o hello hello.o $ readelf -l hello
Elf file type is EXEC (Executable file) Entry point 0x8048062 There are 1 program headers, starting at offset 52
Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x0007b 0x0007b R E 0x1000
Section to Segment mapping: Segment Sections... 00 .text $ sstrip hello $ ./hello #完全可以正常工作 Hello World $ wc -c hello 123 hello $ echo "676-123" | bc #相對于之前,已經(jīng)只需要123個(gè)字節(jié)了,又減少了553個(gè)字節(jié) 553
|
可以看到效果很明顯,只剩下一個(gè)LOAD段,它對應(yīng).text節(jié)區(qū)。
不過是否還有辦法呢?把Hello World作為參數(shù)輸入,而不是硬編碼在文件中。所以如果處理參數(shù)的代碼少于Hello World字符串的長度,那么就可以達(dá)到減少目標(biāo)文件大小的目的。
先來看一個(gè)能夠打印程序參數(shù)的匯編語言程序,它來自參考資料[9]。
Code:
[Ctrl+A Select All]
編譯看看效果,
Quote: |
$ as -o args.o args.s $ ld -o args args.o $ ./args "Hello World" #能夠打印輸入的字符串,不錯(cuò) ./args Hello World $ sstrip args $ wc -c args #處理以后只剩下130字節(jié) 130 args
|
可以看到,這個(gè)程序可以接收用戶輸入的參數(shù)并打印出來,不過得到的可執(zhí)行文件為130字節(jié),比之前的123個(gè)字節(jié)還多了7個(gè)字節(jié),看看還有改進(jìn)么?分析上面的代碼后,發(fā)現(xiàn),原來的代碼有些地方可能進(jìn)行優(yōu)化,優(yōu)化后得到如下代碼。
Code:
[Ctrl+A Select All]
再測試(記得先重新匯編、鏈接并刪除沒用的節(jié)區(qū)和節(jié)區(qū)表)。
Quote: |
$ wc -c hello 124 hello
|
現(xiàn)在只有124個(gè)字節(jié),不過還是比123個(gè)字節(jié)多一個(gè),還有什么優(yōu)化的辦法么?
先來看看目前hello的功能,感覺不太符合要求,因?yàn)橹恍枰蛴ello
World,所以不必處理所有的參數(shù),僅僅需要接收并打印一個(gè)參數(shù)就可以。這樣的話,把jmp vnext(2字節(jié))這個(gè)循環(huán)去掉,然后在第一個(gè)pop
%ecx語句之前加一個(gè)pop %ecx(1字節(jié))語句就可以。
Quote: |
.global _start _start: popl %ecx popl %ecx #彈出argc[0]的地址 popl %ecx #彈出argv[1]的地址 test %ecx, %ecx jz exit movl %ecx, %ebx xorl %edx, %edx strlen: movb (%ebx), %al inc %edx inc %ebx test %al, %al jnz strlen movb $10, -1(%ebx) xorl %eax, %eax movb $4, %al xorl %ebx, %ebx incl %ebx int $0x80 exit: xorl %eax, %eax movl %eax, %ebx incl %eax int $0x80
|
現(xiàn)在剛好123字節(jié),和原來那個(gè)代碼大小一樣,不過仔細(xì)分析,還是有減少代碼的余地:因?yàn)樵谶@個(gè)代碼中,用了一段額外的代碼計(jì)算字符串的長度,實(shí)際上如果
僅僅需要打印Hello
World,那么字符串的長度是固定的,即12。所以這段代碼可去掉,與此同時(shí)測試字符串是否為空也就沒有必要(不過可能影響代碼健壯性!),當(dāng)然,為了
能夠在打印字符串后就換行,在串的末尾需要加一個(gè)回車($10)并且設(shè)置字符串的長度為12+1,即13,
Code:
[Ctrl+A Select All]
再看看效果,
Quote: |
$ wc -c hello 111 hello
|
現(xiàn)在只剩下111字節(jié),比剛才少了12字節(jié)。貌似到了極限?還有措施么?
還有,仔細(xì)分析發(fā)現(xiàn):系統(tǒng)調(diào)用sys_exit和sys_write都用到了eax和ebx寄存器,它們之間剛好有那么一點(diǎn)巧合:
1、sys_exit調(diào)用時(shí),eax需要設(shè)置為1,ebx需要設(shè)置為0。
2、sys_write調(diào)用時(shí),ebx剛好是1。
因此,如果在sys_exit調(diào)用之前,先把ebx復(fù)制到eax中,再對ebx減一,則可減少兩個(gè)字節(jié)。
不過,因?yàn)闃?biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)注錯(cuò)誤都指向終端,如果往標(biāo)準(zhǔn)輸入寫入一些東西,它還是會(huì)輸出到標(biāo)準(zhǔn)輸出上,所以在上述代碼中如果在sys_write
之前ebx設(shè)置為0,那么也可正常往屏幕上打印Hell
World,這樣的話,sys_exit調(diào)用前就沒必要修改ebx,而僅需把eax設(shè)置為1,這樣就可減少3個(gè)字節(jié)。
Code:
[Ctrl+A Select All]
看看效果,
Quote: |
$ wc -c hello 108 hello
|
現(xiàn)在看一下純粹的指令還有多少?
Quote: |
$ readelf -h hello | grep Size Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Size of section headers: 0 (bytes) $ echo "108-52-32" | bc 24
|
純粹的指令只有24個(gè)字節(jié)了,還有辦法再減少目標(biāo)文件的大小么?如果看了參考資料[1],看樣子你又要蠢蠢欲動(dòng)了:這24個(gè)字節(jié)是否可以插入到文件頭部或程序頭部?如果可以那是否意味著還可減少可執(zhí)行文件的大小呢?現(xiàn)在來比較一下這三部分的十六進(jìn)制內(nèi)容。
Quote: |
$ hexdump -C hello -n 52 #文件頭(52bytes) 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 02 00 03 00 01 00 00 00 54 80 04 08 34 00 00 00 |........T...4...| 00000020 00 00 00 00 00 00 00 00 34 00 20 00 01 00 00 00 |........4. .....| 00000030 00 00 00 00 |....| 00000034 $ hexdump -C hello -s 52 -n 32 #程序頭(32bytes) 00000034 01 00 00 00 00 00 00 00 00 80 04 08 00 80 04 08 |................| 00000044 6c 00 00 00 6c 00 00 00 05 00 00 00 00 10 00 00 |l...l...........| 00000054 $ hexdump -C hello -s 84 #實(shí)際代碼部分(24bytes) 00000054 59 59 59 c6 41 0c 0a 31 d2 b2 0d 31 c0 b0 04 31 |YYY.A..1...1...1| 00000064 db cd 80 31 c0 40 cd 80 |...1.@..| 0000006c
|
從上面結(jié)果發(fā)現(xiàn)ELF文件頭部和程序頭部還有好些空洞(0),是否可以通過引入跳轉(zhuǎn)指令把24個(gè)字節(jié)分散放入到那些空洞里或者是直接覆蓋掉那些系統(tǒng)并不關(guān)
心的內(nèi)容?抑或是把代碼壓縮以后放入可執(zhí)行文件中,并在其中實(shí)現(xiàn)一個(gè)解壓縮算法?還可以是通過一些代碼覆蓋率測試工具(gcov,prof)對你的代碼進(jìn)
行優(yōu)化?這個(gè)作為我們共同的練習(xí)吧!
由于時(shí)間關(guān)系,這里不再進(jìn)一步討論,如果想進(jìn)一步研究,請閱讀參考資料[1],它更深層次地討論了ELF文件,特別是Linux系統(tǒng)對ELF文件頭部和程序頭部的解析。
到這里,關(guān)于可執(zhí)行文件的討論暫且結(jié)束,最后來一段小小的總結(jié),那就是我們設(shè)法去減少可執(zhí)行文件大小的意義?
實(shí)際上,通過這樣一個(gè)討論深入到了很多技術(shù)的細(xì)節(jié),包括可執(zhí)行文件的格式、目標(biāo)代碼鏈接的過程、Linux下匯編語言開發(fā)等。與此同時(shí),可執(zhí)行文件大小的
減少本身對嵌入式系統(tǒng)非常有用,如果刪除那些對程序運(yùn)行沒有影響的節(jié)區(qū)和節(jié)區(qū)表將減少目標(biāo)系統(tǒng)的大小,適應(yīng)嵌入式系統(tǒng)資源受限的需求。除此之外,動(dòng)態(tài)連接
庫中的很多函數(shù)可能不會(huì)被使用到,因此也可以通過某種方式剔除[8][10]。
或許,你還會(huì)發(fā)現(xiàn)更多有趣的意義,歡迎給我發(fā)送郵件,一起討論。
參考資料:
[1] A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux
http://www.muppetlabs.com/~breadbox/software/tiny/teensy.html
[2] UNIX/LINUX 平臺(tái)可執(zhí)行文件格式分析
http://blog.chinaunix.net/u/19881/showart_215242.html
[3] C/C++程序編譯步驟詳解
http://www.xxlinux.com/linux/article/development/soft/20070424/8267.html
[4] The Linux GCC HOW TO
http://www.faqs.org/docs/Linux-HOWTO/GCC-HOWTO.html
[5] ELF: From The Programmer's Perspective
http://linux.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/elf.html
[6] Understanding ELF using readelf and objdump
http://www.linuxforums.org/misc/understanding_elf_using_readelf_and_objdump.html
[7] Dissecting shared libraries
http://www.ibm.com/developerworks/linux/library/l-shlibs.html
[8] 嵌入式Linux小型化技術(shù)
http://www.gexin.com.cn/UploadFile/document2008119102415.pdf
[9] Linux匯編語言開發(fā)指南
http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html
[10] Library Optimizer
http://sourceforge.net/projects/libraryopt
[11] ELF file format and ABI
http://www.x86.org/ftp/manuals/tools/elf.pdf
http://www.muppetlabs.com/~breadbox/software/ELF.txt
(北大OS實(shí)驗(yàn)室)http://162.105.203.48/web/gaikuang/submission/TN05.ELF.Format.Summary.pdf
(alert7 大牛翻譯)http://www.xfocus.net/articles/200105/174.html