青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

興海北路

---男兒仗劍自橫行
<2008年3月>
2425262728291
2345678
9101112131415
16171819202122
23242526272829
303112345

統(tǒng)計

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

常用鏈接

留言簿(6)

隨筆分類

隨筆檔案

收藏夾

全是知識啊

搜索

  •  

最新評論

閱讀排行榜

評論排行榜

將你的可執(zhí)行文件“減肥”
by falcon <zhangjinw@gmail.com>
2008-02-23

    這篇blog從減少可執(zhí)行文件大小的角度分析了ELF文件,期間通過經(jīng)典的"Hello World"實例逐步演示如何通過各種常用工具來分析ELF文件,并逐步精簡代碼。
   
    為了能夠盡量減少可執(zhí)行文件的大小,我們必須了解可執(zhí)行文件的格式,以及鏈接生成可執(zhí)行文件時的后臺細(xì)節(jié)(即最終到底有哪些內(nèi)容被鏈接到了目標(biāo)代碼中)。 通過選擇合適的可執(zhí)行文件格式并剔除對可執(zhí)行文件的最終運行沒有影響的內(nèi)容,就可以實現(xiàn)目標(biāo)代碼的裁減。因此,通過探索減少可執(zhí)行文件大小的方法,就相當(dāng) 于實踐性地去探索了可執(zhí)行文件的格式以及鏈接過程的細(xì)節(jié)。

    當(dāng)然,算法的優(yōu)化和編程語言的選擇可能對目標(biāo)文件的大小有很大的影響,在這篇blog的后面我們會跟參考資料[1]的作者那樣去探求一個打印“Hello World”的可執(zhí)行文件能夠小到什么樣的地步。

1、可執(zhí)行文件格式的選取
   
    可執(zhí)行文件格式的選擇要滿足的一個基本條件是:目標(biāo)系統(tǒng)支持該可執(zhí)行文件格式,資料[2]分析和比較了UNIX平臺下的三種可執(zhí)行文件格式,這三種格式實際上代表著可執(zhí)行文件的一個發(fā)展過程。
    a.out文件格式非常緊湊,只包含了程序運行所必須的信息(文本、數(shù)據(jù)、BSS),而且每個 section的順序是固定的。
    coff文件格式雖然引入了一個節(jié)區(qū)表以支持更多節(jié)區(qū)信息,從而提高了可擴展性,但是這種文件格式的重定位在鏈接時就已經(jīng)完成,因此不支持動態(tài)鏈接(不過擴展的coff支持)。
    elf文件格式不僅動態(tài)鏈接,而且有很好的擴展性。它可以描述可重定位文件、可執(zhí)行文件和可共享文件(動態(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ū)頭部表還是各個節(jié)區(qū),都是通過特定的結(jié)構(gòu)體(struct)描述的,這些結(jié)構(gòu)在elf.h文件中定義。文件頭部用于描述 整個文件的類型、大小、運行平臺、程序入口、程序頭部表和節(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ū)表)和程序頭部表有什么用呢?實際上前者只對可重定向文件有用,而后者只對可執(zhí)行文件和可共享文件有用。
    節(jié)區(qū)表是用來描述各節(jié)區(qū)的,包括各節(jié)區(qū)的名字、大小、類型、虛擬內(nèi)存中的位置、相對文件頭的位置等,這樣所有節(jié)區(qū)都通過節(jié)區(qū)表給描述了,這樣連接器就可以 根據(jù)文件頭部表和節(jié)區(qū)表的描述信息對各種輸入的可重定位文件進行合適的鏈接,包括節(jié)區(qū)的合并與重組、符號的重定位(確認(rèn)符號在虛擬內(nèi)存中的地址)等,把各 個可重定向輸入文件鏈接成一個可執(zhí)行文件(或者是可共享文件)。如果可執(zhí)行文件中使用了動態(tài)連接庫,那么將包含一些用于動態(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ū)(各個常見節(jié)區(qū)的作用請參考資料[11])可能不一樣,但是有幾個節(jié)區(qū),例如.text, .data, .bss是必須的,特別是.text,因為這個節(jié)區(qū)包含了代碼。如果一個程序使用了動態(tài)鏈接庫(引用了動態(tài)連接庫中的某個函數(shù)),那么需要.interp 節(jié)區(qū)以便告知系統(tǒng)使用什么動態(tài)連接器程序來進行動態(tài)符號鏈接,進行某些符號地址的重定位。通常,.rel.text節(jié)區(qū)只有可重定向文件有,用于鏈接時對 代碼區(qū)進行重定向,而.hash,.plt,.got等節(jié)區(qū)則只有可執(zhí)行文件(或可共享庫)有,這些節(jié)區(qū)對程序的運行特別重要。還有一些節(jié)區(qū),可能僅僅是 用于注釋,比如.comment,這些對程序的運行似乎沒有影響,是可有可無的,不過有些節(jié)區(qū)雖然對程序的運行沒有用處,但是卻可以用來輔助對程序進行調(diào) 試或者對程序運行效率有影響。
    雖然三類文件都必須包含某些節(jié)區(qū),但是節(jié)區(qū)表對可重定位文件來說才是必須的,而程序的執(zhí)行卻不需要節(jié)區(qū)表,只需要程序頭部表以便知道如何加載和執(zhí)行文件。 不過如果需要對可執(zhí)行文件或者動態(tài)連接庫進行調(diào)試,那么節(jié)區(qū)表卻是必要的,否則調(diào)試器將不知道如何工作。下面來介紹程序頭部表,它可通過readelf -l(或objdump -p)查看。
Quote:

$ readelf -l hello.o #對于可重定向文件,gcc沒有產(chǎ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)著一個或者多個節(jié)區(qū),上面的readelf -l很好地顯示了各個段與節(jié)區(qū)的映射。這些段描述了段的名字、類型、大小、第一個字節(jié)在文件中的位置、將占用的虛擬內(nèi)存大小、在虛擬內(nèi)存中的位置等。這樣 系統(tǒng)程序解釋器將知道如何把可執(zhí)行文件加載到內(nèi)存中以及進行動態(tài)鏈接等動作。
    該可執(zhí)行文件包含7個段,PHDR指程序頭部,INTERP正好對應(yīng).interp節(jié)區(qū),兩個LOAD段包含程序的代碼和數(shù)據(jù)部分,分別包含有.text 和.data,.bss節(jié)區(qū),DYNAMIC段包含.daynamic,這個節(jié)區(qū)可能包含動態(tài)連接庫的搜索路徑、可重定位表的地址等信息,它們用于動態(tài)連 接器。NOTE和GNU_STACK段貌似作用不大,只是保存了一些輔助信息。因此,對于一個不使用動態(tài)連接庫的程序來說,可能只包含LOAD段,如果一 個程序沒有數(shù)據(jù),那么只有一個LOAD段就可以了。

    總結(jié)一下,Linux雖然支持很多種可執(zhí)行文件格式,但是目前ELF較通用,所以選擇ELF作為我們的討論對象。通過上面對ELF文件分析發(fā)現(xiàn)一個可執(zhí)行 的文件可能包含一些對它的運行沒用的信息,比如節(jié)區(qū)表、一些用于調(diào)試、注釋的節(jié)區(qū)。如果能夠刪除這些信息就可以減少可執(zhí)行文件的大小,而且不會影響可執(zhí)行 文件的正常運行。

2、鏈接優(yōu)化

    從上面的討論中已經(jīng)接觸了動態(tài)連接庫。ELF中引入動態(tài)連接庫后極大地方便了公共函數(shù)的共享,節(jié)約了磁盤和內(nèi)存空間,因為不再需要把那些公共函數(shù)的代碼鏈接到可執(zhí)行文件,這將減少了可執(zhí)行文件的大小。
    與此同時,靜態(tài)鏈接可能會引入一些對代碼的運行可能并非必須的內(nèi)容。你可以從《GCC編譯的背后(第二部分:匯編和鏈接)》 了 解到GCC鏈接的細(xì)節(jié)。從那篇Blog中似乎可以得出這樣的結(jié)論:僅僅從是否影響一個C語言程序運行的角度上說,GCC默認(rèn)鏈接到可執(zhí)行文件的幾個可重定 位文件(crt1.o,rti.o,crtbegin.o,crtend.o,crtn.o)并不是必須的,不過值得注意的是,如果沒有鏈接那些文件但在 程序末尾使用了return語句,main函數(shù)將無法返回,因此需要替換為_exit調(diào)用;另外,既然程序在進入main之前有一個入口,那么main入 口就不是必須的。因此,如果不采用默認(rèn)鏈接也可以減少可執(zhí)行文件的大小。

3、可執(zhí)行文件“減肥”實例

    這里主要是根據(jù)上面兩點來介紹如何減少一個可執(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)生一個大小為6442字節(jié)的可執(zhí)行文件
    6442 hello



  • 不采用默認(rèn)編譯

        可以考慮編輯時就把return 0替換成_exit(0)并包含定義該函數(shù)的unistd.h頭文件。下面是從GCC編譯的背后(第二部分:匯編和鏈接)》總結(jié)出的Makefile文件。



    Code:

    [Ctrl+A Select All]


        把上面的代碼復(fù)制到一個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   #這個也是可以正常工作的
    Hello World
    $ wc -c hello   #但是大小減少了4382個字節(jié),減少了將近70%
    2060 hello  
    $ echo "6442-2060" | bc
    4382
    $ echo "(6442-2060)/6442" | bc -l
    .68022353306426575597


        對于一個比較小的程序,能夠減少將近70%“沒用的”代碼。至于一個大一點的程序(這個代碼是[1]的作者寫的一個小工具,我們后面會使用它)再看看效果。
    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個字節(jié),減幅40%
    4323
    $ echo "(10912-6589)/10912" | bc -l
    .39616935483870967741


        通過這兩個簡單的實驗,我們發(fā)現(xiàn),能夠減少掉4000個字節(jié)左右,相當(dāng)于4k左右。

  • 刪除對程序運行沒有影響的節(jié)區(qū)

        使用上述Makefile來編譯程序,不鏈接那些對程序運行沒有多大影響的文件,實際上也相當(dāng)于刪除了一些“沒用”的節(jié)區(qū),可以通過下列演示看出這個實質(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í)行時間可能存在的影響
    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í)行時間長了一些(實際上也可能是進程調(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


        通過刪除各個節(jié)區(qū)可以查看哪些節(jié)區(qū)對程序來說是必須的,不過有些節(jié)區(qū)雖然并不影響程序的運行卻可能會影響程序的執(zhí)行效率,這個可以上面的運行時間看出個大概。
        通過刪除兩個“沒用”的節(jié)區(qū),我們又減少了52+612,即664字節(jié)。

  • 刪除可執(zhí)行文件的節(jié)區(qū)表

        用普通的工具沒有辦法刪除節(jié)區(qū)表,但是參考資料[1]的作者已經(jīng)寫了這樣一個工具。你可以從這里http://www.muppetlabs.com/~breadbox/software/elfkickers.html下載到那個工具,即我們上面作為一個演示例子的sstrip,它是該作者寫的一序列工具ELFkickers中的一個。下載以后,編譯,并復(fù)制到/usr/bin下,下面用它來刪除節(jié)區(qū)表。

    Quote:

    $ sstrip hello      #刪除ELF可執(zhí)行文件的節(jié)區(qū)表
    $ ./hello           #還是可以正常運行,說明節(jié)區(qū)表對可執(zhí)行文件的運行沒有任何影響
    Hello World
    $ wc -c hello       #大小只剩下708個字節(jié)了
    708 hello
    $ echo "1396-708" | bc  #又減少了688個字節(jié)。
    688


        通過刪除節(jié)區(qū)表又把可執(zhí)行文件減少了688字節(jié)?,F(xiàn)在回頭看看相對于gcc默認(rèn)產(chǎn)生的可執(zhí)行文件,通過刪除一些節(jié)區(qū)和節(jié)區(qū)表到底減少了多少字節(jié)?減幅達到了多少?
    Quote:

    $ echo "6442-708" | bc   #
    5734
    $ echo "(6442-708)/6442" | bc -l
    .89009624340266997826


        減少了5734多字節(jié),減幅將近90%,這說明:對于一個簡短的hello.c程序而言,gcc引入了將近90%的對程序運行沒有影響的數(shù)據(jù)。雖然通過刪 除節(jié)區(qū)和節(jié)區(qū)表,使得最終的文件只有708字節(jié),但是打印一個"Hello World"真的需要這么多字節(jié)么?
        事實上未必,因為:
        1、打印一段Hello World字符串,我們無須調(diào)用printf,也就無須包含動態(tài)連接庫,因此.interp,.dynamic等節(jié)區(qū)又可以去掉。為什么?我們可以直接使用系統(tǒng)調(diào)用(sys_write)來打印字符串。
        2、另外,我們無須把Hello World字符串存放到可執(zhí)行文件中?而是讓用戶把它當(dāng)作參數(shù)輸入。
        下面,繼續(xù)進行可執(zhí)行文件的“減肥”。

    4、用匯編語言來重寫"Hello World"

        先來看看gcc默認(rèn)產(chǎn)生的匯編代碼情況。通過gcc的-S選項可得到匯編代碼。
    Quote:

    $ cat hello.c  #這個是使用_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)進行簡單的處理得到,


    Code:

    [Ctrl+A Select All]


        再編譯看看,
    Quote:

    $ gcc -o hello.o hello.s
    $ wc -c hello
    6443 hello
    $ echo "6523-6443" | bc   #僅僅減少了80個字節(jié)
    80


        如果不采用默認(rèn)編譯呢并且刪除掉對程序運行沒有影響的節(jié)區(qū)和節(jié)區(qū)表呢?
    Quote:

    $ sed -i -e "s/main/_start/g" hello.s   #因為沒有初始化,所以得直接進入代碼,替換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   #和之前的實驗類似,也減少了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  #這個結(jié)果比之前的708(在刪除所有垃圾信息以后)個字節(jié)少了708-676,即32個字節(jié)
    676 hello
    $ ./hello
    Hello World


        容易發(fā)現(xiàn)這32字節(jié)可能跟節(jié)區(qū).rodata有關(guān)系,因為剛才在鏈接完以后查看節(jié)區(qū)信息時,并沒有.rodata節(jié)區(qū)。
        前面提到,實際上還可以不用動態(tài)連接庫中的printf函數(shù),也不用直接調(diào)用_exit,而是在匯編里頭使用系統(tǒng)調(diào)用,這樣就可以去掉和動態(tài)連接庫關(guān)聯(lián)的內(nèi)容。(如果想了解如何在匯編中使用系統(tǒng)調(diào)用,請參考資料[9])。使用系統(tǒng)調(diào)用重寫以后得到如下代碼,



    Code:

    [Ctrl+A Select All]


        現(xiàn)在編譯就不再需要動態(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個字節(jié)了,又減少了553個字節(jié)
    553


        可以看到效果很明顯,只剩下一個LOAD段,它對應(yīng).text節(jié)區(qū)。
        不過是否還有辦法呢?把Hello World作為參數(shù)輸入,而不是硬編碼在文件中。所以如果處理參數(shù)的代碼少于Hello World字符串的長度,那么就可以達到減少目標(biāo)文件大小的目的。
        先來看一個能夠打印程序參數(shù)的匯編語言程序,它來自參考資料[9]。


    Code:

    [Ctrl+A Select All]


        編譯看看效果,
    Quote:

    $ as -o args.o args.s
    $ ld -o args args.o
    $ ./args "Hello World"  #能夠打印輸入的字符串,不錯
    ./args
    Hello World
    $ sstrip args
    $ wc -c args           #處理以后只剩下130字節(jié)
    130 args


        可以看到,這個程序可以接收用戶輸入的參數(shù)并打印出來,不過得到的可執(zhí)行文件為130字節(jié),比之前的123個字節(jié)還多了7個字節(jié),看看還有改進么?分析上面的代碼后,發(fā)現(xiàn),原來的代碼有些地方可能進行優(yōu)化,優(yōu)化后得到如下代碼。



    Code:

    [Ctrl+A Select All]


        再測試(記得先重新匯編、鏈接并刪除沒用的節(jié)區(qū)和節(jié)區(qū)表)。
    Quote:

    $ wc -c hello
    124 hello


        現(xiàn)在只有124個字節(jié),不過還是比123個字節(jié)多一個,還有什么優(yōu)化的辦法么?
        先來看看目前hello的功能,感覺不太符合要求,因為只需要打印Hello World,所以不必處理所有的參數(shù),僅僅需要接收并打印一個參數(shù)就可以。這樣的話,把jmp vnext(2字節(jié))這個循環(huán)去掉,然后在第一個pop %ecx語句之前加一個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é),和原來那個代碼大小一樣,不過仔細(xì)分析,還是有減少代碼的余地:因為在這個代碼中,用了一段額外的代碼計算字符串的長度,實際上如果 僅僅需要打印Hello World,那么字符串的長度是固定的,即12。所以這段代碼可去掉,與此同時測試字符串是否為空也就沒有必要(不過可能影響代碼健壯性!),當(dāng)然,為了 能夠在打印字符串后就換行,在串的末尾需要加一個回車($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寄存器,它們之間剛好有那么一點巧合:
        1、sys_exit調(diào)用時,eax需要設(shè)置為1,ebx需要設(shè)置為0。
        2、sys_write調(diào)用時,ebx剛好是1。
        因此,如果在sys_exit調(diào)用之前,先把ebx復(fù)制到eax中,再對ebx減一,則可減少兩個字節(jié)。
        不過,因為標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)注錯誤都指向終端,如果往標(biāo)準(zhǔn)輸入寫入一些東西,它還是會輸出到標(biāo)準(zhǔn)輸出上,所以在上述代碼中如果在sys_write 之前ebx設(shè)置為0,那么也可正常往屏幕上打印Hell World,這樣的話,sys_exit調(diào)用前就沒必要修改ebx,而僅需把eax設(shè)置為1,這樣就可減少3個字節(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個字節(jié)了,還有辦法再減少目標(biāo)文件的大小么?如果看了參考資料[1],看樣子你又要蠢蠢欲動了:這24個字節(jié)是否可以插入到文件頭部或程序頭部?如果可以那是否意味著還可減少可執(zhí)行文件的大小呢?現(xià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          #實際代碼部分(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個字節(jié)分散放入到那些空洞里或者是直接覆蓋掉那些系統(tǒng)并不關(guān) 心的內(nèi)容?抑或是把代碼壓縮以后放入可執(zhí)行文件中,并在其中實現(xiàn)一個解壓縮算法?還可以是通過一些代碼覆蓋率測試工具(gcov,prof)對你的代碼進 行優(yōu)化?這個作為我們共同的練習(xí)吧!
        由于時間關(guān)系,這里不再進一步討論,如果想進一步研究,請閱讀參考資料[1],它更深層次地討論了ELF文件,特別是Linux系統(tǒng)對ELF文件頭部和程序頭部的解析。  
        到這里,關(guān)于可執(zhí)行文件的討論暫且結(jié)束,最后來一段小小的總結(jié),那就是我們設(shè)法去減少可執(zhí)行文件大小的意義?
        實際上,通過這樣一個討論深入到了很多技術(shù)的細(xì)節(jié),包括可執(zhí)行文件的格式、目標(biāo)代碼鏈接的過程、Linux下匯編語言開發(fā)等。與此同時,可執(zhí)行文件大小的 減少本身對嵌入式系統(tǒng)非常有用,如果刪除那些對程序運行沒有影響的節(jié)區(qū)和節(jié)區(qū)表將減少目標(biāo)系統(tǒng)的大小,適應(yīng)嵌入式系統(tǒng)資源受限的需求。除此之外,動態(tài)連接 庫中的很多函數(shù)可能不會被使用到,因此也可以通過某種方式剔除[8][10]。
        或許,你還會發(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 平臺可執(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實驗室)http://162.105.203.48/web/gaikuang/submission/TN05.ELF.Format.Summary.pdf
    (alert7 大牛翻譯)http://www.xfocus.net/articles/200105/174.html
  • posted on 2008-03-14 15:33 隨意門 閱讀(1973) 評論(0)  編輯 收藏 引用


    只有注冊用戶登錄后才能發(fā)表評論。
    網(wǎng)站導(dǎo)航: 博客園   IT新聞   BlogJava   博問   Chat2DB   管理


    青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <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>
            欧美在线一级va免费观看| 国产精品毛片a∨一区二区三区|国 | 久久久久久久久久久久久女国产乱 | 欧美黄色一区| 国产精品女人久久久久久| 亚洲视频欧美视频| 国产欧美韩国高清| 久久九九全国免费精品观看| 久久激情五月婷婷| 亚洲激情在线| 亚洲欧美日韩成人| 亚洲欧洲在线一区| 亚洲无人区一区| 亚洲国产裸拍裸体视频在线观看乱了中文 | 中文一区字幕| 亚洲欧美成aⅴ人在线观看| 激情久久久久久| 亚洲欧洲日产国产网站| 国产精品乱子久久久久| 亚洲国产视频a| 国产欧美91| 亚洲最新在线视频| 亚洲欧洲在线视频| 蘑菇福利视频一区播放| 欧美在线啊v| 国产精品ⅴa在线观看h| 亚洲欧洲日本在线| 亚洲激情电影在线| 久久国产99| 久久精品1区| 亚洲激情成人网| 国产日韩欧美综合| 午夜免费久久久久| 欧美一区二区在线视频| 欧美三区美女| 亚洲自拍电影| 久久日韩粉嫩一区二区三区| 国产欧美一区二区白浆黑人| 亚洲一区二区在线播放| 亚洲伊人伊色伊影伊综合网| 欧美日韩一区二区三区在线看| 亚洲高清在线视频| 99re8这里有精品热视频免费 | 久久久一二三| 亚洲第一主播视频| 制服丝袜激情欧洲亚洲| 国产精品久久久久一区二区三区共 | 亚洲午夜性刺激影院| 久久国产精品99久久久久久老狼| 国产欧美日韩91| 欧美在线亚洲| 99re6热在线精品视频播放速度| 在线一区观看| 狠狠综合久久| 欧美午夜精品久久久久久浪潮| 午夜免费日韩视频| 日韩视频一区二区三区| 欧美中文字幕| 一区二区三区精品久久久| 日韩视频免费看| 午夜精品视频网站| 亚洲经典视频在线观看| 国产精品久久久一区麻豆最新章节 | 久久精品系列| 亚洲啪啪91| 欧美福利视频| 狼狼综合久久久久综合网| 亚洲欧美第一页| 亚洲人成绝费网站色www| 好看的日韩视频| 国产亚洲欧美aaaa| 国产视频一区免费看| 国产精品日产欧美久久久久| 欧美午夜视频| 欧美丝袜一区二区| 欧美特黄a级高清免费大片a级| 欧美阿v一级看视频| 欧美一区二区三区在线播放| 亚洲免费在线观看视频| 中文亚洲免费| 欧美在线视频免费| 久久美女性网| 欧美激情久久久久| 欧美人与禽猛交乱配视频| 欧美日韩成人一区| 国产精品国产三级国产普通话三级| 欧美日韩免费在线观看| 欧美先锋影音| 在线观看视频一区二区欧美日韩 | 91久久久久久| 亚洲一区二区在线观看视频| 欧美一区二区三区的| 香蕉成人久久| 久久久午夜视频| 亚洲第一精品福利| 亚洲欧洲av一区二区| 免费看av成人| 国产伦精品一区二区| 一区二区亚洲精品国产| 亚洲视频免费在线| 欧美国产高清| 亚洲女同性videos| 能在线观看的日韩av| 国产日韩一区| 亚洲综合成人在线| 亚洲日本无吗高清不卡| 久久精品日韩一区二区三区| 国产精品hd| 国产精品99久久久久久www| 欧美不卡一区| 久久久五月婷婷| 国产午夜精品久久久久久久| 亚洲一区免费观看| 夜夜嗨av一区二区三区四区| 久久久久久九九九九| 国产在线麻豆精品观看| 香蕉久久夜色精品国产| 亚洲午夜精品网| 国产精品电影观看| 亚洲视频你懂的| 亚洲一区二区免费看| 国产精品视频内| 欧美一区成人| 久久精品欧美日韩| 在线看成人片| 亚洲清纯自拍| 国产精品久久久爽爽爽麻豆色哟哟| 亚洲私人影院在线观看| 亚洲欧美高清| 亚洲片在线观看| 亚洲男女自偷自拍图片另类| 国产日韩欧美二区| 国产一区二区三区奇米久涩| 久久亚洲一区二区三区四区| 久久久夜精品| 亚洲欧美一区二区在线观看| 久久精品国产清自在天天线| 日韩视频在线观看国产| 亚洲婷婷综合久久一本伊一区| 国产日韩欧美a| 亚洲第一页自拍| 国产精品亚洲аv天堂网| 蜜桃久久av一区| 国产日韩欧美中文| 在线视频你懂得一区| 亚洲国产美国国产综合一区二区| 99亚洲一区二区| 日韩视频久久| 老司机一区二区| 久久综合影音| 国产日韩欧美一区在线 | 嫩模写真一区二区三区三州| 亚洲欧美日韩精品一区二区| 美女免费视频一区| 久久人人97超碰国产公开结果 | 午夜精品美女自拍福到在线| 欧美精品播放| 亚洲精品国产精品国自产观看| 在线观看一区二区视频| 欧美诱惑福利视频| 久久视频在线视频| 国产亚洲精品久久久| 欧美一级午夜免费电影| 欧美一区二区三区日韩| 国产精品一区视频| 欧美在线视频免费| 国产亚洲激情在线| 性久久久久久久久| 欧美在线播放| 亚洲二区在线视频| 欧美人成网站| 欧美一区二区三区啪啪| 免费视频亚洲| 亚洲欧美卡通另类91av| 国产欧美精品日韩精品| 久久精品夜色噜噜亚洲a∨| 欧美国产日韩a欧美在线观看| 亚洲乱码国产乱码精品精可以看| 欧美日韩中文字幕| 午夜精品一区二区在线观看| 欧美高潮视频| 欧美一级精品大片| 99精品久久免费看蜜臀剧情介绍| 国产精品亚洲精品| 免费观看在线综合| 亚洲一区高清| 日韩亚洲一区二区| 免费日韩精品中文字幕视频在线| 亚洲一区二区三区免费在线观看| 黄色成人免费网站| 国产亚洲一区二区三区在线观看 | 欧美一二区视频| 日韩视频精品在线| 91久久一区二区| 欧美大秀在线观看| 久久久人人人| 久久久亚洲欧洲日产国码αv| 亚洲一区二区三区免费在线观看| 亚洲人成亚洲人成在线观看图片| 中文在线资源观看视频网站免费不卡|