一直以爲不管是編譯共享庫還是靜態庫,中間生成的目標文件(.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版本的小,但再配置所需要的信息佔很大的空間。