庫用於將相似函數打包在一個單元中。Linux支援兩種類型的庫:靜態庫(在編譯時間靜態繫結到程式)和動態庫(在運行時綁定到程式)。Linux系統使用的動態庫是ELF格式,尾碼名為so。 1 載入
動態庫內部劃分為段,段分為不同的類型:
PT_LOAD段:包含代碼或資料,是需要被映射到記憶體中的,每個段有不同的存取權限(讀、些、執行);
PT_DYNAMIC段:包含動態連結資訊,如符號表、重定位表、引用的其他庫等。
其他段類型暫不說明。
載入器將庫檔案第一個PT_LOAD段和最後一個PT_LOAD段之間的內容映射到一段連續的記憶體位址空間(好處是任意代碼和資料的相對位址固定),其首地址稱為基地址(如圖)。
庫的載入只是把檔案內容映射到記憶體位址,但沒有真正讀取檔案資料,在發生記憶體缺頁異常時才由作業系統讀入對應的檔案資料到記憶體。延遲讀取檔案可以加快庫的載入速度。 1.1 預連結
一般來說,映射的基地址是不固定的,但如果動態庫使用了預連結(prelink)技術,則會被映射到預定的地址(儲存在檔案上)。如果預定的位址範圍已經被佔用了,則載入失敗(Androidlinker是這樣,其他載入器可能不同)。Prelink的好處是簡化重定位,加快載入速度。 2 重定位 2.1 內建函式和變數
在沒有使用prelink的情況下,庫的基地址不是固定的(運行時才確定),其全域變數和函數的絕對位址也不是固定的。由於庫載入之後任意代碼和資料的相對位址是固定的(如前一節所述),因此一些系統(如x86)可以使用相對位址來訪問全域變數和函數。ARM系統由於指令長度限制(32位),無法在指令中直接使用大範圍的位移量(但可通過寄存器指定),另外絕對位址在執行效率上要優於相對位址,因此還是需要重定位。
如這個例子:
__attribute__((visibility("hidden")))int errBase = 1;void setErr(){ errBase = 0x999; }
編譯得到so,然後反編譯(ARM架構):
$ gcc -shared-nostdlib -o libtest.so test.c$ objdump -dlibtest.so000002c4<setErr>:2c4: mov ip,sp2c8: push {fp,ip, lr, pc}2cc: sub fp,ip, #42d0: ldr r2,[pc, #12] ; r2 = &errBase2d4: mov r3,#2448 ; r3 = 9902d8: add r3,r3, #9 ; r3 += 92dc: str r3,[r2] ; *r2 = r32e0: ldm sp,{fp, sp, pc}2e4: .word 0x0000109c ; 這裡儲存著errBase變數的地址
查看重定位表:
$ readelf -rlibtest.soRelocationsection '.rel.dyn' at offset 0x2bc contains 1 entries:Offset Info Type Sym.Value Sym. Name000002e4 00000017 R_ARM_RELATIVE
對比彙編代碼和重定位表,2e4即是儲存errBase變數地址的位移量。
重定位表中一個RELATIVE類型的表項,指向變數和函數的相對位址,載入器把它加上基地址,使成為絕對位址。如果使用了prelink,則不需要進行重定位。 2.2 外部函數和變數
外部變數和函數是指目標庫引用依賴庫的變數和函數,需要載入器在依賴庫的符號表尋找對應的名稱和絕對位址,然後寫入目標庫的全域位移量表(GlobalOffset Table,簡稱GOT)。目標庫通過GOT來訪問外部變數和函數。
外部變數重定位對應一個GLOB_DAT類型的表項,外部函數重定位對應一個JMP_SLOT類型表項,表項的值是外部變數或函數的絕對位址,由載入器進行設定。
如這個例子:
extern interrBase;void setErr(){ errBase = 0x999; }
編譯得到so,然後反編譯(ARM架構):
$ gcc -shared-nostdlib -o libtest.so test.c$ objdump -dlibtest.so00000218<setErr>:218: push {fp}21c: add fp,sp, #0220: ldr r3,[pc, #28] ; r3=GOT位移224: add r3,pc, r3 ; r3=GOT地址228: ldr r2,[pc, #24] ; r2=errBase項在GOT的位移22c: ldr r3,[r3, r2] ; r3=errBase的地址230: ldr r2,[pc, #20] ; r2=0x999234: str r2,[r3] ; r3=r2238: add sp,fp, #023c: pop {fp}240: bx lr244: .word 0x00008dc4 ; GOT位移248: .word 0x0000000c ; errBase在GOT的位移24c: .word 0x00000999
查看重定位表:
$readelf -rlibtest.soRelocationsection '.rel.dyn' at offset 0x210 contains 1 entries:Offset Info Type Sym.Value Sym.Name00008ffc 00000415 R_ARM_GLOB_DAT 00000000 errBase
8ffcc正好對應errBase的GOT表項地址。 2.3 延遲綁定
外部函數和變數的重定位需要尋找依賴庫的符號表,並進行字串比較,效率較低,不過一般一個庫使用的外部變數和函數都不會太多。如果使用了較多的外部函數,為了加快動態庫載入速度,可以使用過程連結資料表(ProcedureLinkageTable,簡稱PLT),把外部函數的定位延遲到第一次調用的時候(稱為延遲綁定)。函數延遲綁定需要編譯器對函數調用產生額外的代碼,主要由編譯器實現。
看這個例子:
voidprintf1(const char*, ...);void setErr(){ printf1("setErr\n"); }
對應彙編代碼(x86-64):
4c0<printf1@plt>:4c0: jmpq *0x200b3a(%rip)4c6: pushq $0x04cb: jmpq 4b0 <_init+0x18>5ac <setErr>:5ac: push %rbp5ad: mov %rsp,%rbp5b0: lea 0x5f(%rip),%rdi5b7: mov $0x0,%eax5bc: callq 4c05c1: pop %rbp5c2: retq
調用printf1會調用printf1@plt,然後跳轉到*0x200b3a(%rip),即*(基地址+0x201000)。
如果是第一次執行,*0x(基地址+0x201000)的值是(基地址+4c6),後面的代碼會進行函數綁定,對應的重定位項是:
$ readelf -rlibtest.soRelocationsection '.rela.plt' at offset 0x468 contains 2 entries:201000 000300000007 R_X86_64_JUMP_SLO printf1 + 0
綁定之後*0x(基地址+0x201000)會對應printf1函數的地址,下次再進入printf1@plt,就可以直接跳轉到printf1函數了。 2.4 位置無關代碼
一般來說,程式和動態庫的代碼和唯讀資料被載入到記憶體之後,可以被多個進程共用,但被寫的髒資料則不能被多個進程共用。RELATIVE類型的重定位會修改程式碼片段的變數地址,導致程式碼片段被汙染,從而不能被多個進程共用。為了讓動態庫的程式碼片段可以在進程間共用,可以讓編譯器編譯出位置無關代碼(簡稱PIC),通過GOT來訪問變數和函數。
PIC使程式碼片段可在進程間共用,從而節省了記憶體,但是通過GOT表來訪問變數和函數會比相對定位慢一點,如果沒有需要則可以不使用PIC。