• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            興海北路

            ---男兒仗劍自橫行
            <2009年3月>
            22232425262728
            1234567
            891011121314
            15161718192021
            22232425262728
            2930311234

            統(tǒng)計(jì)

            • 隨筆 - 85
            • 文章 - 0
            • 評論 - 17
            • 引用 - 0

            常用鏈接

            留言簿(6)

            隨筆分類

            隨筆檔案

            收藏夾

            全是知識(shí)啊

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            將你的可執(zhí)行文件“減肥”
            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í)行文件大小。

          1. 系統(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



          2. 不采用默認(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左右。

          3. 刪除對程序運(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é)

          4. 刪除可執(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
          5. posted on 2008-03-14 15:33 隨意門 閱讀(1962) 評論(0)  編輯 收藏 引用

            久久久久久免费视频| 精品久久久久一区二区三区| 久久久久人妻精品一区| 久久99精品久久只有精品| 国产毛片久久久久久国产毛片 | 嫩草伊人久久精品少妇AV| 97久久精品无码一区二区 | 青草国产精品久久久久久| 久久亚洲国产欧洲精品一| 亚洲七七久久精品中文国产| 久久久久久亚洲AV无码专区| 亚洲另类欧美综合久久图片区| 97久久香蕉国产线看观看| 久久午夜无码鲁丝片秋霞| 久久福利片| 久久国产乱子伦免费精品| 久久久久久精品免费免费自慰| 久久久精品久久久久特色影视| 久久精品国产91久久麻豆自制| 久久亚洲私人国产精品| 国产一区二区久久久| 欧美日韩精品久久久久| 91精品无码久久久久久五月天| 久久久久久精品免费看SSS| 亚洲人AV永久一区二区三区久久 | 亚洲精品综合久久| 久久se这里只有精品| 精品无码人妻久久久久久| 99久久国产亚洲高清观看2024| 国产欧美一区二区久久| 97久久超碰成人精品网站| 国产精品美女久久久久| 69SEX久久精品国产麻豆| 国内精品九九久久久精品| 久久国产乱子伦免费精品| 97精品伊人久久大香线蕉app| 国产成人精品免费久久久久| 色综合久久久久无码专区| 亚洲av日韩精品久久久久久a| 亚洲国产美女精品久久久久∴| 久久久无码精品亚洲日韩蜜臀浪潮|