編譯GNU/Linux共享庫, 爲什麼要用PIC編譯?( 轉)

編譯GNU/Linux共享庫, 爲什麼要用PIC編譯?

一直以爲不管是編譯共享庫還是靜態庫,中間生成的目標文件(.o文件)是沒有區別的,
區別只在:最後是用-shared編譯還是用ar打包; 可是事情的真相併不是這樣的:

from <<Binary Hacks ―ハッカー祕伝のテクニック100選>>  <<Binary Hacks:黑客祕笈100選>>

本hack中,我們來研究編譯共享庫時,爲什麼要用PIC(選項)編譯?

通常編譯GNU/Linux共享庫時,把各個.c文件編譯編譯成PIC(Position Independent Code, 位置無關代碼)。但是,實際上不用PIC編譯的話也可以編譯共享庫。那麼使用PIC還有意義嗎?

讓我們來進行一個實驗:
    #include <stdio.h>
    void func() {
        printf(" ");
        printf(" ");
        printf(" ");
    }

用PIC編譯必須把參數-fpic或-fPIC傳給gcc,-fpic可以生成小而高效的代碼,但是不同的處理器中-fpic生成的GOT(Global Offset Table, 全局偏移表)的大小有限制,另一方面,使用-fPIC的話,任何處理器都可以放心使用。在這裏,使用-fPIC。(在X86中使用-fpic和-fPIC沒有任何區別)。
    $ gcc -o fpic-no-pic.s -S fpic.c
    $ gcc -fPIC -o fpic-pic.s -S fpic.c

閱讀上述生成的彙編代碼,則可以知道PIC版本通過PLT(Procedure Linkage Table)調用printf。
    $ grep printf fpic-no-pic.s
             call printf
             call printf
             call printf
    $ grep printf fpic-pic.s
             call printf@PLT
             call printf@PLT
             call printf@PLT
下面,編譯共享庫
    $ gcc -shared -o fpic-no-pic.so fpic.c
    $ gcc -shared -fPIC -o fpic-pic.so fpic.c

這些共享庫的動態節(dynamic section)用readelf閱讀的話,非PIC版本中有TEXTREL輸入方法(需要在text內進行再配置),並且RELCOUNT(再配置的數量)爲5 -- 比PIC版本的多3個。多出三個是因爲printf()的調用進行了3次。
    $ readelf -d fpic-no-pic.so | egrep 'TEXTREL|RELCOUNT'
     0x00000016 (TEXTREL)                  0x0
     0x6ffffffa (RELCOUNT)                 5
    $ readelf -d fpic-pic.so | egrep 'TEXTREL|RELCOUNT'
     0x6ffffffa (RELCOUNT)  2

PIC版本的RELCOUNT非0是由於gcc在缺省時使用的是包含在啓動文件裏的代碼。若加-nostartfiles選項,則RELCOUNT值爲0。

PIC和非PIC共享庫的性能對比
上面例子闡述了非PIC版本運行時(動態運行時)需要5個地址的再分配。那麼,若在配置的數量大增時會出現什麼樣的情況呢?

運行下面的shell腳本,用非PIC版本和PIC版本編譯含有1000萬次printf()調用的共享庫,和相應的可執行文件fpic-no-pic和fpic-pic。

    #! /bin/sh
    rm -f *.o *.so
    num=1000
    for i in `seq $num`; do
        echo -e "#include <stdio.h>/nvoid func$i() {" >fpic$i.c
        #ruby -e "10000.times { puts 'printf(/" /");' }" >>fpic$i.c
        perl -e 'print("printf(/" /");/n"x10000);' >>fpic$i.c
        echo "}" >> fpic$i.c
        gcc -o fpic-no-pic$i.o -c fpic$i.c
        gcc -o fpic-pic$i.o -fPIC -c fpic$i.c
    done
    gcc -o fpic-no-pic.so -shared fpic-no-pic*.o
    gcc -o fpic-pic.so -shared fpic-pic*.o

    echo "int main() { return 0; }" >fpic-main.c
    gcc -o no-pic-load fpic-main.c ./fpic-no-pic.so
    gcc -o pic-load fpic-main.c ./fpic-pic.so

    echo "int main() {" >main.c
    for i in `seq $num`; do echo "func$i();"; done >>main.c
    echo "}" >>main.c
    gcc -o fpic-no-pic main.c ./fpic-no-pic.so
    gcc -o fpic-pic main.c ./fpic-pic.so

兩個版本程序,運行結果如下: 非PIC版本首次運行時間2.15秒,第二次以後大約0.55秒,而PIC版本的首次用了0.02秒,第二次以後用了0.00秒。

    $ repeat 3 time ./no-pic-load
    2.15s total : 0.29s user 0.48s system 35% cpu
    0.56s total : 0.25s user 0.31s system 99% cpu
    0.55s total : 0.30s user 0.25s system 99% cpu
    $ repeat 3 time ./pic-load
    0.02s total : 0.00s user 0.00s system 0% cpu
    0.00s total : 0.00s user 0.01s system 317% cpu
    0.00s total : 0.00s user 0.00s system 0% cpu
main() 本身是空的,可以知道非PIC版本動態鏈接時的再分配需要2.15~0.55秒。運行環境Xeon-2.8GHz+Debian GNU/Linux sarge+GCC 3.3.5。

非PIC版本的缺點不僅是在運行時再配置上花時間。爲了更新再配置部分的代碼,將會發生這樣的情況(下載text segment裏需要再配置的頁->更新->進行copy on write -> 不能與其他路徑和text共享)。

另外比較非PIC版本的fpic-no-pic.so和PIC版本的fpic-pic.so的大小,前者268M,後者134M,差別很明顯。用readelf -S查看節頭,會有以下區別:
                 .rel.dyn      .text
非PIC            152MB         114MB
PIC              0MB           133MB

非PIC版本的代碼(.text)比PIC版本的小,但再配置所需要的信息佔很大的空間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章