庫用于將相似函數(shù)打包在一個單元中。Linux 支持兩種類型的庫:靜態(tài)庫(在編譯時靜態(tài)綁定到程序)和動態(tài)庫(在運行時綁定到程序)。Linux 系統(tǒng)使用的動態(tài)庫是ELF 格式,后綴名為so 。
1 加載
動態(tài)庫內(nèi)部劃分為段,段分為不同的類型:
其他段類型暫不說明。
加載器將庫文件第一個PT_LOAD 段和最后一個PT_LOAD 段之間的內(nèi)容映射到一段連續(xù)的內(nèi)存地址空間(好處是任意代碼和數(shù)據(jù)的相對地址固定),其首地址稱為基地址 (如圖)。
庫的加載只是把文件內(nèi)容映射到內(nèi)存地址,但沒有真正讀取文件數(shù)據(jù),在發(fā)生內(nèi)存缺頁異常時才由操作系統(tǒng)讀入對應(yīng)的文件數(shù)據(jù)到內(nèi)存。延遲讀取文件可以加快庫的加載速度。
1.1 預(yù)鏈接
一般來說,映射的基地址是不固定的,但如果動態(tài)庫使用了預(yù)鏈接( prelink ) 技術(shù),則會被映射到預(yù)定的地址(保存在文件上)。如果預(yù)定的地址范圍已經(jīng)被占用了,則加載失敗(Androidlinker 是這樣,其他加載器可能不同)。Prelink 的好處是簡化重定位,加快加載速度。
2 重定位
2.1 內(nèi)部函數(shù)和變量
在沒有使用prelink 的情況下,庫的基地址不是固定的(運行時才確定),其全局變量和函數(shù)的絕對地址也不是固定的。由于庫加載之后任意代碼和數(shù)據(jù)的相對地址是固定的(如前一節(jié)所述),因此一些系統(tǒng)(如x86 )可以使用相對地址來訪問全局變量和函數(shù)。ARM 系統(tǒng)由于指令長度限制(32 位),無法在指令中直接使用大范圍的偏移量(但可通過寄存器指定),另外絕對地址在執(zhí)行效率上要優(yōu)于相對地址,因此還是需要重定位。
如這個例子:
[cpp] view plaincopyprint?
__attribute__((visibility("hidden" )))int ?errBase?=?1;?? void ?setErr(){?errBase?=?0x999;?}??
__attribute__((visibility("hidden")))int errBase = 1;
void setErr(){ errBase = 0x999; }
編譯得到so ,然后反編譯(ARM 架構(gòu)):
[plain] view plaincopyprint?
$?gcc?-shared-nostdlib?-o?libtest.so?test.c?? $?objdump?-dlibtest.so?? 000002c4<setErr>:?? 2c4:??mov????ip,sp?? 2c8:??push???{fp,ip,?lr,?pc}?? 2cc:??sub????fp,ip,?#4?? 2d0:??ldr????r2,[pc,?#12]??????;?r2?=?&errBase?? 2d4:??mov????r3,#2448??????????;?r3?=?990?? 2d8:??add????r3,r3,?#9?????????;?r3?+=?9?? 2dc:??str????r3,[r2]???????????;?*r2?=?r3?? 2e0:??ldm????sp,{fp,?sp,?pc}?? 2e4:??.word??0x0000109c????????;?這里保存著errBase變量的地址??
$ gcc -shared-nostdlib -o libtest.so test.c
$ objdump -dlibtest.so
000002c4<setErr>:
2c4: mov ip,sp
2c8: push {fp,ip, lr, pc}
2cc: sub fp,ip, #4
2d0: ldr r2,[pc, #12] ; r2 = &errBase
2d4: mov r3,#2448 ; r3 = 990
2d8: add r3,r3, #9 ; r3 += 9
2dc: str r3,[r2] ; *r2 = r3
2e0: ldm sp,{fp, sp, pc}
2e4: .word 0x0000109c ; 這里保存著errBase變量的地址
查看重定位表:
[plain] view plaincopyprint?
$?readelf?-rlibtest.so?? Relocationsection?'.rel.dyn'?at?offset?0x2bc?contains?1?entries:?? Offset????Info???????Type????????????Sym.Value??Sym.?Name?? 000002e4??00000017???R_ARM_RELATIVE??
$ readelf -rlibtest.so
Relocationsection '.rel.dyn' at offset 0x2bc contains 1 entries:
Offset Info Type Sym.Value Sym. Name
000002e4 00000017 R_ARM_RELATIVE
對比匯編代碼和重定位表,2e4 即是保存errBase 變量地址的偏移量。
重定位表中一個RELATIVE 類型的表項,指向變量和函數(shù)的相對地址,加載器把它加上基地址,使成為絕對地址。如果使用了prelink ,則不需要進(jìn)行重定位。
2.2 外部函數(shù)和變量
外部變量和函數(shù)是指目標(biāo)庫引用依賴庫的變量和函數(shù),需要加載器在依賴庫的符號表 查找對應(yīng)的名稱和絕對地址,然后寫入目標(biāo)庫的全局偏移量表( GlobalOffset Table ,簡稱 GOT ) 。目標(biāo)庫通過GOT 來訪問外部變量和函數(shù)。
外部變量重定位對應(yīng)一個GLOB_DAT 類型 的表項,外部函數(shù)重定位對應(yīng)一個JMP_SLOT 類型 表項,表項的值是外部變量或函數(shù)的絕對地址,由加載器進(jìn)行設(shè)置。
如這個例子:
[cpp] view plaincopyprint?
extern ?interrBase;??void ?setErr(){?errBase?=?0x999;?}??
extern interrBase;
void setErr(){ errBase = 0x999; }
編譯得到so ,然后反編譯(ARM 架構(gòu)):
[cpp] view plaincopyprint?
$?gcc?-shared-nostdlib?-o?libtest.so?test.c?? $?objdump?-dlibtest.so?? 00000218<setErr>:?? 218:??push???{fp}?? 21c:??add????fp,sp,?#0?? 220:??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=0x999?? 234:??str????r2,[r3]?????????;?r3=r2?? 238:??add????sp,fp,?#0?? 23c:??pop????{fp}?? 240:??bx?????lr?? 244:??.word??0x00008dc4??????;?GOT偏移?? 248:??.word??0x0000000c??????;?errBase在GOT的偏移?? 24c:??.word??0x00000999??
$ gcc -shared-nostdlib -o libtest.so test.c
$ objdump -dlibtest.so
00000218<setErr>:
218: push {fp}
21c: add fp,sp, #0
220: 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=0x999
234: str r2,[r3] ; r3=r2
238: add sp,fp, #0
23c: pop {fp}
240: bx lr
244: .word 0x00008dc4 ; GOT偏移
248: .word 0x0000000c ; errBase在GOT的偏移
24c: .word 0x00000999
查看重定位表:
[cpp] view plaincopyprint?
$readelf?-rlibtest.so?? Relocationsection?'.rel.dyn' ?at?offset?0x210?contains?1?entries:?? Offset??????Info????????Type??????????????Sym.Value????Sym.Name?? 00008ffc????00000415????R_ARM_GLOB_DAT????00000000?????errBase??
$readelf -rlibtest.so
Relocationsection '.rel.dyn' at offset 0x210 contains 1 entries:
Offset Info Type Sym.Value Sym.Name
00008ffc 00000415 R_ARM_GLOB_DAT 00000000 errBase
8ffcc 正好對應(yīng)errBase 的GOT 表項地址。
2.3 延遲綁定
外部函數(shù)和變量的重定位需要查找依賴庫的符號表,并進(jìn)行字符串比較,效率較低,不過一般一個庫使用的外部變量和函數(shù)都不會太多。如果使用了較多的外部函數(shù),為了加快動態(tài)庫加載速度,可以使用過程鏈接表( ProcedureLinkageTable ,簡稱 PLT ) ,把外部函數(shù)的定位延遲到第一次調(diào)用的時候(稱為延遲綁定)。函數(shù)延遲綁定需要編譯器對函數(shù)調(diào)用生成額外的代碼,主要由編譯器實現(xiàn)。
看這個例子:
[cpp] view plaincopyprint?
voidprintf1(const ?char *,?...);?? void ?setErr(){?printf1("setErr\n" );?}??
voidprintf1(const char*, ...);
void setErr(){ printf1("setErr\n"); }
對應(yīng)匯編代碼(x86-64 ):
[html] view plaincopyprint?
4c0< printf1 @plt> :?? 4c0:??jmpq???*0x200b3a(%rip)?? 4c6:??pushq??$0x0?? 4cb:??jmpq???4b0?< _init +0x18> ?? ?? 5ac?< setErr > :?? 5ac:??push???%rbp?? 5ad:??mov????%rsp,%rbp?? 5b0:??lea????0x5f(%rip),%rdi?? 5b7:??mov????$0x0,%eax?? 5bc:??callq??4c0?? 5c1:??pop????%rbp?? 5c2:??retq??
4c0<printf1@plt>:
4c0: jmpq *0x200b3a(%rip)
4c6: pushq $0x0
4cb: jmpq 4b0 <_init+0x18>5ac <setErr>:
5ac: push %rbp
5ad: mov %rsp,%rbp
5b0: lea 0x5f(%rip),%rdi
5b7: mov $0x0,%eax
5bc: callq 4c0
5c1: pop %rbp
5c2: retq
調(diào)用printf1 會調(diào)用printf1@plt ,然后跳轉(zhuǎn)到*0x200b3a(%rip) ,即*( 基地址+0x201000 )。
如果是第一次執(zhí)行,*0x( 基地址+0x201000) 的值是( 基地址+4c6) ,后面的代碼會進(jìn)行函數(shù)綁定,對應(yīng)的重定位項是:
[plain] view plaincopyprint?
$?readelf?-rlibtest.so?? Relocationsection?'.rela.plt'?at?offset?0x468?contains?2?entries:?? 201000?000300000007?R_X86_64_JUMP_SLO??printf1?+?0??
$ readelf -rlibtest.so
Relocationsection '.rela.plt' at offset 0x468 contains 2 entries:
201000 000300000007 R_X86_64_JUMP_SLO printf1 + 0
綁定之后*0x( 基地址+0x201000) 會對應(yīng)printf1 函數(shù)的地址,下次再進(jìn)入printf1@plt ,就可以直接跳轉(zhuǎn)到printf1 函數(shù)了。
2.4 位置無關(guān)代碼
一般來說,程序和動態(tài)庫的代碼和只讀數(shù)據(jù)被加載到內(nèi)存之后,可以被多個進(jìn)程共享,但被寫的臟數(shù)據(jù)則不能被多個進(jìn)程共享。RELATIVE 類型的重定位會修改代碼段的變量地址,導(dǎo)致代碼段被污染,從而不能被多個進(jìn)程共享。為了讓動態(tài)庫的代碼段可以在進(jìn)程間共享,可以讓編譯器編譯出位置無關(guān)代碼(簡稱 PIC ) ,通過GOT 來訪問變量和函數(shù)。
PIC 使代碼段可在進(jìn)程間共享,從而節(jié)省了內(nèi)存,但是通過GOT 表來訪問變量和函數(shù)會比相對定位慢一點,如果沒有需要則可以不使用PIC 。
總結(jié)
以上是生活随笔 為你收集整理的ELF动态库加载技术 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。