本文將深入探討Linux系統(tǒng)中的動(dòng)態(tài)鏈接庫機(jī)制,這其中包括但不限于全局符號(hào)介入、延遲綁定以及地址無關(guān)代碼等內(nèi)容。
引言
在軟件開發(fā)過程中,動(dòng)態(tài)庫鏈接問題時(shí)常出現(xiàn),這可能導(dǎo)致符號(hào)沖突,從而引起程序運(yùn)行異常或崩潰。為深入理解動(dòng)態(tài)鏈接機(jī)制及其工作原理,我重溫了《程序員的自我修養(yǎng)》,并通過實(shí)踐演示與反匯編分析,了解了動(dòng)態(tài)鏈接的過程。
本文將深入探討Linux系統(tǒng)中的動(dòng)態(tài)鏈接庫機(jī)制,這其中包括但不限于全局符號(hào)介入(Global Symbol Interposition)、延遲綁定(Lazy Binding)以及地址無關(guān)代碼(Position-Independent Code, PIC)等內(nèi)容。通過對(duì)上述概念和技術(shù)細(xì)節(jié)的討論,希望能夠提供一個(gè)更加清晰的認(rèn)知框架,從而揭示符號(hào)沖突背后隱藏的本質(zhì)原因。這樣一來,在實(shí)際軟件開發(fā)過程中遇到類似問題時(shí),開發(fā)者們便能更加游刃有余地采取措施進(jìn)行預(yù)防或解決,確保程序穩(wěn)定運(yùn)行的同時(shí)提升整體質(zhì)量與用戶體驗(yàn)。
為便于讀者查閱,本文中提及的一些基本概念,例如ELF、PIC、GOT、PLT、常用的section等,被歸納整理于附錄部分。
一、先舉個(gè)
我們將通過一個(gè)簡單的 C 語言程序,逐步探討動(dòng)態(tài)鏈接庫在模塊內(nèi)部及模塊間的運(yùn)行機(jī)制,其中涉及變量和函數(shù)之間的交互過程。同時(shí),我們將使用 -fPIC 選項(xiàng),以確保生成位置無關(guān)代碼。
#include// 靜態(tài)變量 a 僅在本模塊中可見 static int a; // 用 extern 聲明外部全局變量 b extern int b; // 在本模塊訪問的全局變量 c int c = 3; // 聲明外部函數(shù) ext() extern void ext(); // 靜態(tài)函數(shù) inner() 的作用域僅限于本模塊 static void inner() {} // bar() 函數(shù)修改靜態(tài)變量 a 和外部全局變量 b void bar() { a = 1; // 修改靜態(tài)變量 a 的值 b = 2; // 修改外部全局變量 b 的值 c = 4; // 修改模塊內(nèi)的全局變量 c 的值 } // foo() 函數(shù)內(nèi)調(diào)用了 inner、bar 和 ext,并打印變量值 void foo() { inner(); // 調(diào)用靜態(tài)函數(shù) inner() bar(); // 調(diào)用函數(shù) bar() ext(); // 調(diào)用外部函數(shù) ext() printf("a = %d, b = %d, c = %d ", a, b, c); // 輸出變量的值 }
// 定義外部全局變量 b int b = 1; // 外部函數(shù) ext() 修改外部全局變量 b 的值 void ext() { b = 3; // 修改外部全局變量 b 的值 } // main.c int main() { foo(); // 調(diào)用 foo() 函數(shù),演示模塊間交互 return 0; // 程序正常結(jié)束 }
gcc -shared -fPIC -o libpic.so pic.c -g
gcc -o main main.c -L. -lpic在此代碼示例中,使用 -fPIC 編譯選項(xiàng)可以生成位置無關(guān)的代碼,適用于創(chuàng)建共享庫。代碼中包含了多個(gè)場景:
模塊內(nèi)函數(shù)調(diào)用:foo 函數(shù)中調(diào)用了 inner 和 bar 函數(shù)。由于 inner 是靜態(tài)函數(shù),其作用域僅限于本模塊。bar 函數(shù)操作了模塊內(nèi)的靜態(tài)變量 a 和全局變量 c。
模塊間函數(shù)調(diào)用:foo 函數(shù)調(diào)用了外部函數(shù) ext,這是一個(gè)在其他模塊中定義的函數(shù)。ext 負(fù)責(zé)修改外部全局變量 b。
不同類型的變量:
靜態(tài)變量 a 僅在本模塊可見,其值不會(huì)在程序的其他模塊中改變,也不會(huì)因函數(shù)調(diào)用而丟失。
外部全局變量 b 可以在多個(gè)模塊間共享,其值在整個(gè)程序中是唯一且可改變的。
模塊內(nèi)的全局變量 c 僅能在當(dāng)前模塊訪問和修改。
我們都知道動(dòng)態(tài)鏈接庫需要能夠在多個(gè)進(jìn)程之間共享同一段代碼。為了實(shí)現(xiàn)這一點(diǎn),代碼必須是位置無關(guān)的,從而可以在加載時(shí)按需被鏈接到不同的地址,編譯時(shí)添加編譯選項(xiàng)-fPIC 可以生成地址無關(guān)代碼,那這些函數(shù)和變量運(yùn)行時(shí),如何做到呢?接下來將逐步分析動(dòng)態(tài)鏈接的過程。
二、從例子來深入動(dòng)態(tài)鏈接庫
2.1 模塊內(nèi)函數(shù)調(diào)用
例子中 foo 函數(shù)實(shí)現(xiàn)中有兩個(gè)函數(shù)調(diào)用:靜態(tài)函數(shù) inner()和非靜態(tài)函數(shù) bar(),反匯編后結(jié)果。
Disassembly of section .plt: 0000000000000670: 670: ff 35 92 09 20 00 push QWORD PTR [rip+0x200992] # 201008 <_GLOBAL_OFFSET_TABLE_+0x8> 676: ff 25 94 09 20 00 jmp QWORD PTR [rip+0x200994] # 201010 <_GLOBAL_OFFSET_TABLE_+0x10> 67c: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 0000000000000680 : 680: ff 25 92 09 20 00 jmp QWORD PTR [rip+0x200992] # 201018 <_GLOBAL_OFFSET_TABLE_+0x18> 686: 68 00 00 00 00 push 0x0 68b: e9 e0 ff ff ff jmp 670 <_init+0x20> ... 00000000000007e8 : foo(): 00000000000007e2 : inner(): /mnt/share/demo1/pic.c:12 static void inner() {} 7e2: 55 push rbp 7e3: 48 89 e5 mov rbp,rsp 7e6: 5d pop rbp 7e7: c3 ret ... /mnt/share/demo1/pic.c:15 inner(); 7ec: b8 00 00 00 00 mov eax,0x0 7f1: e8 ec ff ff ff call 7e2 /mnt/share/demo1/pic.c:16 bar(); 7f6: b8 00 00 00 00 mov eax,0x0 7fb: e8 80 fe ff ff call 680
2.1.1 靜態(tài)函數(shù)調(diào)用:inner()函數(shù)調(diào)用
和靜態(tài)編譯重定位相似,這里更簡單,具體如下:
7f1: e8 ec ff ff ff call 7e2
e8:相對(duì)偏移調(diào)用指令
ec ff ff ff:小端 0XFFFFFFEC 是-20 的補(bǔ)碼,該數(shù)值為目的地址相對(duì)于當(dāng)前指令下一條指令的偏移。即 inner 地址為 0x7f6(下一條指令偏移) - 0x14 = 0x7e2
結(jié)論:靜態(tài)函數(shù)調(diào)用很簡單,通過相對(duì)地址偏移就可以跳轉(zhuǎn)。
2.1.2 全局函數(shù)調(diào)用:bar()函數(shù)調(diào)用
首次調(diào)用
7fb: e8 80 fe ff ff call 680
解析規(guī)則同上,不展開,但是跳轉(zhuǎn)的地址為 0x680
第一條指令為jmp QWORD PTR [rip+0x200992],這是一個(gè)間接跳轉(zhuǎn)(jmp)指令,運(yùn)行跳轉(zhuǎn)地址 0x201018,該地址是什么?
objdump -s libpic.so Contents of section .got: 200fc8 00000000 00000000 00000000 00000000 ................ 200fd8 00000000 00000000 00000000 00000000 ................ 200fe8 00000000 00000000 00000000 00000000 ................ 200ff8 00000000 00000000 ........ Contents of section .got.plt: 201000 080e2000 00000000 00000000 00000000 .. ............. 201010 00000000 00000000 86060000 00000000 ................ 201020 96060000 00000000 a6060000 00000000 ................ 201030 b6060000 00000000 c6060000 00000000 ................
發(fā)現(xiàn)這個(gè)地址在.got.plt section,0x00000686, 該地址存的地址為
0000000000000680那上面一系列地址跳轉(zhuǎn)是在干什么?用一個(gè)示意圖表示 bar 首次地址重定位過程(橙色是調(diào)用入口,藍(lán)色是運(yùn)行的指令,紫色代表修正的地址)。: 680: ff 25 92 09 20 00 jmp QWORD PTR [rip+0x200992] # 201018 <_GLOBAL_OFFSET_TABLE_+0x18> 686: 68 00 00 00 00 push 0x0 68b: e9 e0 ff ff ff jmp 670 <_init+0x20>
_dl_runtime_resolve()函數(shù)實(shí)現(xiàn)不展開,該函數(shù)的入?yún)槿霔5姆?hào)索引 index 和庫 ID,解析過程會(huì)依賴.dynamic、.rela.plt 等 section 信息,解析后重定向地址后填入地址0x201018 。可以查看下.rela.plt 段內(nèi)容有什么。
[root@docker-desktop demo1]# readelf -r libpic.so Relocation section '.rela.dyn' at offset 0x4e8 contains 10 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000200de8 000000000008 R_X86_64_RELATIVE 780 000000200df0 000000000008 R_X86_64_RELATIVE 740 000000200e00 000000000008 R_X86_64_RELATIVE 200e00 000000200fc8 000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0 000000200fd0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 b + 0 000000200fd8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000200fe0 000e00000006 R_X86_64_GLOB_DAT 0000000000201040 c + 0 000000200fe8 000700000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0 000000200ff0 000800000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0 000000200ff8 000900000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0 Relocation section '.rela.plt' at offset 0x5d8 contains 5 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000201018 000b00000007 R_X86_64_JUMP_SLO 00000000000007b8 bar + 0 000000201020 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0 000000201028 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0 000000201030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 ext + 0 000000201038 000900000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0.rela.plt是 ELF 文件中包含了函數(shù)跳轉(zhuǎn)槽重定位信息。具體代表含義:
Offset - 表示在內(nèi)存中的偏移地址,即在 GOT 中重定位項(xiàng)的地址。
Info - 包含兩個(gè)部分:符號(hào)的索引和重定位類型。在這種情況下,重定位類型是 R_X86_64_JUMP_SLOT,用于處理函數(shù)調(diào)用的跳轉(zhuǎn)。
Type - 描述了重定位的類型,這里是 R_X86_64_JUMP_SLOT,用于通過懶加載解析符號(hào)的PLT入口。其他類型還有很多,常見的還有
R_X86_64_GLOB_DAT - 設(shè)置全局偏移表的內(nèi)容。
R_X86_64_64 - 64位直接重定位;修改64位的值。
R_X86_64_PC32 - 32位PC相對(duì)重定位;修改指令內(nèi)偏移的32位值。
R_X86_64_GOT32 - 32位的全局偏移表(GOT)入口。
R_X86_64_PLT32 - 用于函數(shù)調(diào)用的32位PLT重定位。
R_X86_64_GLOB_DAT - 設(shè)置全局偏移表的內(nèi)容。
R_X86_64_RELATIVE - 需要基地址重置,用于模塊加載專用的相對(duì)地址調(diào)整。
R_X86_64_GOTPCREL - 訪問GOT的PC相對(duì)重定位。
Sym. Value - 是符號(hào)在它本身定義模塊內(nèi)的值。在重定位發(fā)生之前,符號(hào)可能還沒有最終的運(yùn)行時(shí)地址。對(duì)于本地符號(hào)(比如 bar 函數(shù)),這里通常是它們?cè)诋?dāng)前模塊中的偏移地址。對(duì)于外部符號(hào)(比如 printf),在重定位前這里通常是 0,表示地址還未確定。
Sym. Name + Addend - 顯示了符號(hào)的名稱以及添加量。添加量在這里是 0,因?yàn)槲覀冋诓榭?.rela 格式的重定位項(xiàng),添加量已經(jīng)包含在每個(gè)重定位項(xiàng)中。
在運(yùn)行時(shí),動(dòng)態(tài)鏈接器會(huì)依據(jù)這些重定位項(xiàng)進(jìn)行地址解析工作。例如,當(dāng)程序第一次調(diào)用 printf 時(shí),控制流首先跳轉(zhuǎn)到 printf 在 PLT 中的對(duì)應(yīng)項(xiàng),PLT 中會(huì)有一段存根代碼觸發(fā)動(dòng)態(tài)鏈接器,動(dòng)態(tài)鏈接器解析出 printf 的真實(shí)地址并更新 GOT 中對(duì)應(yīng)的地址。
第二次調(diào)用
運(yùn)行后地址重定位后,第二次調(diào)用就會(huì)簡單很多,如下圖所示:
使用 GDB 調(diào)試運(yùn)行后,單步調(diào)試地址重定向.got.plt 段內(nèi)容(基地址為:0x7F7A97F75000)。
201000 080e2000 00000000 00000000 00000000 .. .............
(gdb) x/16a 0x7f7a98176000 0x7f7a98176000: 0x200e08 0x7f7a983976a8 0x7f7a98176010: 0x7f7a9818d890 <_dl_runtime_resolve_xsave> 0x7f7a97f756860x7f7a98176020: 0x7f7a97f75696 0x7f7a97f756a6 <__gmon_start__@plt+6> 0x7f7a98176030: 0x7f7a97f756b6 0x7f7a97f756c6 <__cxa_finalize@plt+6> 0x7f7a98176040 : 0x3 0x0 0x7f7a98176050: 0x31303220352e382e 0x5228203332363035 0x7f7a98176060: 0x3420746148206465 0x2936332d352e382e 0x7f7a98176070: 0x20000002c00 0x8000000
.got.plt 中 bar 地址 = 0x201018 +0x7F7A97F75000(基地址)= 0x7F7A98176018,0x7F7A98176018 內(nèi)容為0x7f7a97f75686
(gdb) x/16a 0x7f7a98176000 0x7f7a98176000: 0x200e08 0x7f7a983976a8 0x7f7a98176010: 0x7f7a9818d890 <_dl_runtime_resolve_xsave> 0x7f7a97f757b80x7f7a97f757b8 為代碼段,0x7f7a97f757b8 - 0x7F7A97F75000(基地址)=0x7B8,該偏移在.text 的 bar 入口地址,也對(duì)應(yīng)起來了。0x7f7a98176020: 0x7f7a97f75696 0x7f7a97f756a6 <__gmon_start__@plt+6> 0x7f7a98176030: 0x7f7a97f756b6 0x7f7a97f756c6 <__cxa_finalize@plt+6> 0x7f7a98176040 : 0x3 0x0 0x7f7a98176050: 0x31303220352e382e 0x5228203332363035 0x7f7a98176060: 0x3420746148206465 0x2936332d352e382e 0x7f7a98176070: 0x20000002c00 0x8000000
抽象一下,如下示意圖:
通過上圖指令跳轉(zhuǎn)得出,.plt,利用.got.plt 可寫權(quán)限,在程序運(yùn)行時(shí),修正.got.plt 對(duì)應(yīng)函數(shù)指向的.text (不可寫)地址,從而實(shí)現(xiàn)了地址無關(guān)代碼。
該過程還隱藏了一個(gè)知識(shí)點(diǎn),延遲綁定(lazy binding)。動(dòng)態(tài)鏈接器在運(yùn)行時(shí)完成,若已一開始執(zhí)行,要加載完所有的符號(hào)的話,想必會(huì)減慢程序的啟動(dòng)速度,影響性能。所以當(dāng)函數(shù)第一次被用到時(shí)再進(jìn)行綁定,如果沒有用就不綁定,這樣可以大大加快程序啟動(dòng)速度。本例子中的 bar 也是在調(diào)用時(shí)才進(jìn)行重定向,不調(diào)用不進(jìn)行地址重定向綁定,即實(shí)現(xiàn)了延遲綁定效果。
是不是外部函數(shù)重定向一定在 .rela.plt?
不是,如果是PIC 編譯,會(huì)在.rela.plt;如果不是PIC 編譯,會(huì)在.rela.dyn 出現(xiàn)。
原因:開啟 PIC 調(diào)用指令會(huì)指向 PLT 中的一個(gè)條目,需要.rela.plt section 配合實(shí)現(xiàn) Lazy Binding,.rela.dyn 段用于動(dòng)態(tài)鏈接器在加載時(shí)將符號(hào)綁定到其運(yùn)行時(shí)地址的重定位條目。它包含了不特定于PLT條目的其他動(dòng)態(tài)重定位信息,.rela.plt 主要針對(duì)PLT進(jìn)行重定位,用于動(dòng)態(tài)鏈接時(shí)解析函數(shù)地址,實(shí)現(xiàn)惰性綁定,而 .rela.dyn 用于更廣泛的動(dòng)態(tài)重定位需求。
疑問?
問題一:模塊內(nèi)全局函數(shù)調(diào)用和模塊間全局函數(shù)調(diào)用有什么區(qū)別?
問題二:為什么都是函數(shù)調(diào)用,靜態(tài)函數(shù)和全局函數(shù)調(diào)用跳轉(zhuǎn)差別這么大?
這兩個(gè)問題先不著急回答,我們接著看模塊間函數(shù)調(diào)用。
2.2 模塊間函數(shù)調(diào)用
例子中是 foo() 對(duì) ext()函數(shù)的調(diào)用,查看匯編,發(fā)現(xiàn)和模塊內(nèi)函數(shù)調(diào)用方式一模一樣。匯編指令如下:
/mnt/share/demo1/pic.c:17 ext(); 800: b8 00 00 00 00 mov eax,0x0 805: e8 a6 fe ff ff call 6b0那現(xiàn)在回答上一節(jié)的第一個(gè)問題,模塊內(nèi)和模塊間全局函數(shù)調(diào)用沒有區(qū)別,為什么呢?
先回憶下加載過程,動(dòng)態(tài)鏈接器完成自舉后,會(huì)將可執(zhí)行文件和鏈接器本身的符號(hào)表都合并到一個(gè)符號(hào)表中,該符號(hào)表叫做全局符號(hào)表(Global Symbol Table)。當(dāng)一個(gè)符號(hào)需要被加入全局符號(hào)表時(shí),如果相同的符號(hào)已經(jīng)存在,則后加入的符號(hào)被忽略,這種規(guī)則叫做全局符號(hào)介入。
由于全局符號(hào)介入規(guī)則,若上一節(jié)的模塊內(nèi)部函數(shù)調(diào)用 bar() 直接采用相對(duì)地址調(diào)用話,可能會(huì)被其他模塊的同名函數(shù)符號(hào)覆蓋,那相對(duì)地址就是無法準(zhǔn)確找到正確的函數(shù)地址,故模塊內(nèi)和模塊外的函數(shù)調(diào)用,都需要通過.got.plt 重定位方法間接調(diào)用。
那上一節(jié)第二個(gè)問題答案也顯而易見,靜態(tài)函數(shù)不涉及全局符號(hào)介入問題,可以通過模塊內(nèi)部相對(duì)地址跳轉(zhuǎn)就可以。這樣調(diào)用的尋址速度也比全局函數(shù)的尋址速度快。
為了更深入理解全局符號(hào)介入,我們?cè)倥e個(gè)例子。
/* a1.c*/ #includevoid a() { printf("a1.c "); } /* a2.c */ #include void a() { printf("a2.c "); } /* b1.c */ void a(); void b1() { a(); } /* b2.c */ void a(); void b2() { a(); } /* main.c */ #include void b1(); void b2(); int main() { b1(); b2(); return 0; }
[root@docker-desktop priority]# g++ -fPIC -shared a1.c -o a1.so [root@docker-desktop priority]# g++ -fPIC -shared a2.c -o a2.so [root@docker-desktop priority]# g++ -fPIC -shared b1.c a1.so -o b1.so [root@docker-desktop priority]# g++ -fPIC -shared b2.c a2.so -o b2.so [root@docker-desktop priority]# ldd b1.so a1.so (0x0000004001c2a000) libstdc++.so.6 => /usr/local/gcc-5.4.0/lib64/libstdc++.so.6 (0x0000004001e2c000) libm.so.6 => /lib64/libm.so.6 (0x00000040021ad000) libgcc_s.so.1 => /usr/local/gcc-5.4.0/lib64/libgcc_s.so.1 (0x00000040024b0000) libc.so.6 => /lib64/libc.so.6 (0x00000040026c7000) /lib64/ld-linux-x86-64.so.2 (0x0000004000000000) [root@docker-desktop priority]# ldd b2.so a2.so (0x0000004001c2a000) libstdc++.so.6 => /usr/local/gcc-5.4.0/lib64/libstdc++.so.6 (0x0000004001e2c000) libm.so.6 => /lib64/libm.so.6 (0x00000040021ad000) libgcc_s.so.1 => /usr/local/gcc-5.4.0/lib64/libgcc_s.so.1 (0x00000040024b0000) libc.so.6 => /lib64/libc.so.6 (0x00000040026c7000) /lib64/ld-linux-x86-64.so.2 (0x0000004000000000) [root@docker-desktop priority]# g++ main.c b1.so b2.so -o main [root@docker-desktop priority]# ./main a1.c a1.c在上述例子中,雖然 b1.so 和 b2.so 中都調(diào)用了 a() 函數(shù),但由于 main 程序首先鏈接了 b1.so,導(dǎo)致 a() 的實(shí)現(xiàn)使用了 a1.so 中的定義。因此,無論 b2.so 如何變化,main 程序中調(diào)用的都始終是 a1.so 的實(shí)現(xiàn)。這種現(xiàn)象強(qiáng)調(diào)了在動(dòng)態(tài)鏈接庫中符號(hào)的解析順序及如何影響最終的執(zhí)行結(jié)果,開發(fā)者在設(shè)計(jì)接口時(shí)需謹(jǐn)慎考慮符號(hào)的命名和庫的加載順序,以避免潛在的符號(hào)沖突和不確定性。
2.3 模塊內(nèi)變量 和模塊間變量
例子中的靜態(tài)變量 a 、外部全局變量 b、 內(nèi)部全局變量 c,看下反匯編后結(jié)果:
void bar() { 7b8: 55 push rbp 7b9: 48 89 e5 mov rbp,rsp /mnt/share/demo1/pic.c:7 a = 1; 7bc: c7 05 82 08 20 00 01 mov DWORD PTR [rip+0x200882],0x1 # 201048 <__TMC_END__> 7c3: 00 00 00 /mnt/share/demo1/pic.c:8 b = 2; 7c6: 48 8b 05 03 08 20 00 mov rax,QWORD PTR [rip+0x200803] # 200fd0 <_DYNAMIC+0x1c8> 7cd: c7 00 02 00 00 00 mov DWORD PTR [rax],0x2 /mnt/share/demo1/pic.c:9 c = 4; 7d3: 48 8b 05 06 08 20 00 mov rax,QWORD PTR [rip+0x200806] # 200fe0 <_DYNAMIC+0x1d8> 7da: c7 00 04 00 00 00 mov DWORD PTR [rax],0x4 /mnt/share/demo1/pic.c:10 }
Idx Name Size VMA LMA File off Algn CONTENTS, ALLOC, LOAD, DATA 20 .got 00000038 0000000000200fc8 0000000000200fc8 00000fc8 2**3 CONTENTS, ALLOC, LOAD, DATA 21 .got.plt 00000040 0000000000201000 0000000000201000 00001000 2**3 CONTENTS, ALLOC, LOAD, DATA 22 .data 00000004 0000000000201040 0000000000201040 00001040 2**2 CONTENTS, ALLOC, LOAD, DATA 23 .bss 0000000c 0000000000201044 0000000000201044 00001044 2**2 ALLOCstatic int a; # 201048 <__TMC_END__> ==> .bss
extern int b; # 200fd0 <_DYNAMIC+0x1c8> ==> .got
int c; # 200fe0 <_DYNAMIC+0x1d8> ==> .got
結(jié)合上面了解的函數(shù)調(diào)用,變量調(diào)用跳轉(zhuǎn)類似,static 變量的訪問直接通過偏移量完成,這種方式更高效,因?yàn)?static 變量的作用域限制在同一個(gè)編譯單元,所以它們的地址可以在編譯時(shí)確定(相對(duì)于 rip)。而非 static 變量(包括定義在當(dāng)前模塊的全局變量和 extern 變量)可能被其他模塊引用或修改,其地址需要在運(yùn)行時(shí)通過動(dòng)態(tài)鏈接器解析,對(duì)于全局和 extern 變量,共享庫使用基于 rip 的尋址加上 運(yùn)行時(shí)重定位.got 段中地址,以確保位置無關(guān)。
全局變量的地址不存在延遲綁定,因?yàn)橥ǔ?huì)在加載時(shí)解析,并通過全局偏移表(Global Offset Table, GOT)來訪問,而不是延遲到首次使用時(shí)。因此,把它們的地址解析延遲將不會(huì)帶來明顯的優(yōu)勢(shì),而且會(huì)在運(yùn)行時(shí)增加額外的性能負(fù)擔(dān)。
三、地址無關(guān)延伸
3.1 隱藏符號(hào)影響
如果把 bar 和變量 c 使用__attribute__((visibility("hidden")))隱藏的符號(hào),那函數(shù)調(diào)用跳轉(zhuǎn)會(huì)有什么變化?
#includestatic int a; extern int b; __attribute__((visibility("hidden"))) int c = 3; extern void ext(); void bar() __attribute__((visibility("hidden"))); void bar() { a = 1; b = 2; c = 4; } static void inner() {} void foo() { inner(); bar(); ext(); printf("a = %d, b = %d, c = %d ", a, b, c); }
反匯編后結(jié)果
[root@docker-desktop demo1]# objdump -d -M intel -S -l libpic_hidden.so Disassembly of section .text: ... 0000000000000738: bar(): /mnt/share/demo1/pic_hidden.c:7 static int a; extern int b; __attribute__((visibility("hidden"))) int c = 3; extern void ext(); void bar() __attribute__((visibility("hidden"))); void bar() { 738: 55 push rbp 739: 48 89 e5 mov rbp,rsp /mnt/share/demo1/pic_hidden.c:8 a = 1; 73c: c7 05 fa 08 20 00 01 mov DWORD PTR [rip+0x2008fa],0x1 # 201040 <__TMC_END__> 743: 00 00 00 /mnt/share/demo1/pic_hidden.c:9 b = 2; 746: 48 8b 05 8b 08 20 00 mov rax,QWORD PTR [rip+0x20088b] # 200fd8 <_DYNAMIC+0x1c8> 74d: c7 00 02 00 00 00 mov DWORD PTR [rax],0x2 /mnt/share/demo1/pic_hidden.c:10 c = 4; 753: c7 05 db 08 20 00 04 mov DWORD PTR [rip+0x2008db],0x4 # 201038 75a: 00 00 00 ... /mnt/share/demo1/pic_hidden.c:17 bar(); 773: b8 00 00 00 00 mov eax,0x0 778: e8 bb ff ff ff call 738
[root@docker-desktop demo1]# readelf -S libpic_hidden.so There are 34 section headers, starting at offset 0x1470: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align ...... [23] .data PROGBITS 0000000000201038 00001038 0000000000000004 0000000000000000 WA 0 0 4
bar: 反匯編后看到調(diào)用 bar 直接可以通過相對(duì)地址跳轉(zhuǎn),不需要運(yùn)行重定位。
int c; # 201038
查看.rela.plt section
[root@docker-desktop demo1]# readelf -r libpic_hidden.so Relocation section '.rela.dyn' at offset 0x4a8 contains 9 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000200df0 000000000008 R_X86_64_RELATIVE 700 000000200df8 000000000008 R_X86_64_RELATIVE 6c0 000000200e08 000000000008 R_X86_64_RELATIVE 200e08 000000200fd0 000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0 000000200fd8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 b + 0 000000200fe0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0 000000200fe8 000700000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0 000000200ff0 000800000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0 000000200ff8 000900000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0 Relocation section '.rela.plt' at offset 0x580 contains 4 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000201018 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0 000000201020 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0 000000201028 000600000007 R_X86_64_JUMP_SLO 0000000000000000 ext + 0 000000201030 000900000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0.rela.plt 中已經(jīng)沒有 bar(),.rela.dyn中沒有變量 c ,所以隱藏后,bar() 不需要重定位,變量 c也不需要間接跳轉(zhuǎn)。隱藏的符號(hào) bar() 和 c 也不會(huì)出現(xiàn)在動(dòng)態(tài)鏈接庫的動(dòng)態(tài)符號(hào)表(.dynsym)中,因此它們?cè)阪溄訒r(shí)不可見于其他共享對(duì)象或者可執(zhí)行文件,所以隱藏符號(hào)不存在全局符號(hào)介入的場景。
3.2 關(guān)于 PIC 回答幾個(gè)小問題
如何區(qū)分一個(gè) DSO 是否為 PIC
readelf -d xxx.so | grep TEXTREL
如果沒有輸出,則動(dòng)態(tài)庫是使用 PIC 生成的。文本重定位(TEXTREL)意味著代碼部分(.text section)需要修改以引用正確的地址,在非PIC的代碼中,會(huì)存在基于絕對(duì)地址的引用,這就需要在加載時(shí)進(jìn)行修改,從而使得代碼能夠正確運(yùn)行,這個(gè)過程就是文本重定位。
2. 如何區(qū)分一個(gè)靜態(tài)庫是否為 PIC
ar -t xxx.a readelf -r xxx.o你需要檢查輸出中是否有基于絕對(duì)地址的重定位類型比如 R_X86_64_GOTPCREL 或其他類似的不是專為 PIC 代碼的重定位類型。
3. 假設(shè)靜態(tài)編譯庫編譯不使用-fPIC,動(dòng)態(tài)庫編譯使用-fPIC,是否 ok?
不行。實(shí)測(cè)靜態(tài)庫 a.a 不使用-fPIC,動(dòng)態(tài)庫 b.so 使用-fPIC,可執(zhí)行程序 main 鏈接兩個(gè)庫會(huì)編譯失敗。報(bào)錯(cuò)日志如下:
g++ -c nopic_common.c -o nopic_common.o ar rcs libnopic_common.a nopic_common.o g++ -shared -o libnopic.so pic.c -L. -lnopic_common -fPIC /usr/bin/ld: ./libnopic_common.a(nopic_common.o): relocation R_X86_64_PC32 against symbol `b' can not be used when making a shared object; recompile with -fPIC /usr/bin/ld: final link failed: Bad value collect2: error: ld returned 1 exit statusnopic_common.o 對(duì)象文件是沒有使用 -fPIC 編譯的,因此包含以 PC 相對(duì)的方式(R_X86_64_PC32 relocation type)引用全局變量 b。這種類型的重定位不兼容于動(dòng)態(tài)庫的創(chuàng)建,因?yàn)樗蟠a必須在特定地址執(zhí)行,而動(dòng)態(tài)庫加載的地址在運(yùn)行時(shí)是未知的,甚至每次運(yùn)行都可能不同。即靜態(tài)庫的代碼假定某些數(shù)據(jù)或函數(shù)存在于固定地址,而該地址已經(jīng)被其他代碼或庫占用,則可能會(huì)導(dǎo)致鏈接錯(cuò)誤或運(yùn)行時(shí)錯(cuò)誤。
要修復(fù)這個(gè)錯(cuò)誤,你需要重新編譯 nopic_common.o,將其中的代碼編譯為位置無關(guān)代碼(PIC)。
4. 為什么動(dòng)態(tài)庫編譯時(shí)不默認(rèn)采用PIC:
歷史原因:歷史慣性,較早的編譯器版本中沒有將生成PIC作為默認(rèn)選項(xiàng)。
選項(xiàng)傳遞的問題:-fPIC是編譯器的選項(xiàng),是在源代碼編譯階段決定的,而-shared是鏈接器的選項(xiàng), 是在不同階段,所以無法通過-shared自動(dòng)啟用-fPIC。
性能:雖然PIC對(duì)于共享庫的高效運(yùn)行是很重要的,但在某些情況下PIC代碼也可能稍微慢于非PIC代碼,因?yàn)樗枰褂瞄g接地址引用全局變量和函數(shù)。這種性能影響一般是很小的,但在對(duì)性能要求非常高的應(yīng)用程序中,這可能是一個(gè)因素。
編譯器和構(gòu)建系統(tǒng)設(shè)計(jì):編譯器和構(gòu)建系統(tǒng)往往允許開發(fā)者根據(jù)項(xiàng)目需求選擇是否生成PIC。允許靈活配置使開發(fā)者能夠根據(jù)具體的使用場景和需求,選擇最合適的編譯選項(xiàng)。
3.3 動(dòng)態(tài)和靜態(tài)鏈接的重定向區(qū)別
靜態(tài)鏈接 | 動(dòng)態(tài)鏈接 | |
階段 | 編譯鏈接階段 | 裝載運(yùn)行階段 |
執(zhí)行控制權(quán) | 控制權(quán)直接交給可執(zhí)行文件 | 控制權(quán)限交給動(dòng)態(tài)鏈接器,映射完成后再交給可執(zhí)行文件 |
運(yùn)行尋址速度 | 速度快 | 由于間接跳轉(zhuǎn),比靜態(tài)鏈接慢約 1%~5%,使用 lazy binding 改善 |
重定位表名 |
.rela.text 代碼段重定位表 .rela.data 數(shù)據(jù)段重定位表 |
.rela.plt 代碼段重定位表 .rela.dyn 數(shù)據(jù)段重定位表 |
四、如何指定全局變量和函數(shù)裝載時(shí)的順序
上面主要介紹了動(dòng)態(tài)裝載過程,在初始化和反初始化的時(shí)候,特別需要關(guān)注全局變量和函數(shù)的構(gòu)造與析構(gòu)順序。這些過程直接影響到模塊間的依賴關(guān)系和對(duì)象之間的交互。因此,我們需要了解如何通過使用特定的屬性來控制這些順序,以確保程序的穩(wěn)定性和預(yù)期行為。特別是在多模塊動(dòng)態(tài)庫的環(huán)境中,合理安排初始化和反初始化的順序,是避免運(yùn)行時(shí)錯(cuò)誤和崩潰的重要措施。
4.1 全局變量初始化順序
對(duì)于跨共享庫的全局變量,其初始化順序受這些共享庫之間的依賴關(guān)系影響。如果共享庫 A 依賴于共享庫 B,那么 B 的初始化代碼將會(huì)在 A 的初始化代碼之前執(zhí)行,因此 B 中的全局變量會(huì)在 A 中的全局變量之前被初始化。
再來看一下《第一章 2 模塊間函數(shù)調(diào)用》例子中,通過LD_DEBUG=files ./main命令看鏈接順序和初始化順序。
[root@docker-desktop]# LD_DEBUG=files ./main 112: find library=b1.so [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64/tls/i686:/usr/local/gcc-5.4.0/lib64/tls:/usr/local/gcc-5.4.0/lib64/i686:/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/tls/i686/b1.so 112: trying file=/usr/local/gcc-5.4.0/lib64/tls/b1.so 112: trying file=/usr/local/gcc-5.4.0/lib64/i686/b1.so 112: trying file=/usr/local/gcc-5.4.0/lib64/b1.so 112: trying file=tls/i686/b1.so 112: trying file=tls/b1.so 112: trying file=i686/b1.so 112: trying file=b1.so 112: 112: find library=b2.so [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/b2.so 112: trying file=tls/i686/b2.so 112: trying file=tls/b2.so 112: trying file=i686/b2.so 112: trying file=b2.so 112: 112: find library=libstdc++.so.6 [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/libstdc++.so.6 112: 112: find library=libm.so.6 [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/libm.so.6 112: trying file=tls/i686/libm.so.6 112: trying file=tls/libm.so.6 112: trying file=i686/libm.so.6 112: trying file=libm.so.6 112: search cache=/etc/ld.so.cache 112: trying file=/lib64/libm.so.6 112: 112: find library=libgcc_s.so.1 [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/libgcc_s.so.1 112: 112: find library=libc.so.6 [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/libc.so.6 112: trying file=tls/i686/libc.so.6 112: trying file=tls/libc.so.6 112: trying file=i686/libc.so.6 112: trying file=libc.so.6 112: search cache=/etc/ld.so.cache 112: trying file=/lib64/libc.so.6 112: 112: find library=a1.so [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/a1.so 112: trying file=tls/i686/a1.so 112: trying file=tls/a1.so 112: trying file=i686/a1.so 112: trying file=a1.so 112: 112: find library=a2.so [0]; searching 112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686i686: (LD_LIBRARY_PATH) 112: trying file=/usr/local/gcc-5.4.0/lib64/a2.so 112: trying file=tls/i686/a2.so 112: trying file=tls/a2.so 112: trying file=i686/a2.so 112: trying file=a2.so 112: 112: 112: calling init: /lib64/libc.so.6 112: 112: 112: calling init: /lib64/libm.so.6 112: 112: 112: calling init: /usr/local/gcc-5.4.0/lib64/libgcc_s.so.1 112: 112: 112: calling init: /usr/local/gcc-5.4.0/lib64/libstdc++.so.6 112: 112: 112: calling init: a2.so 112: 112: 112: calling init: a1.so 112: 112: 112: calling init: b2.so 112: 112: 112: calling init: b1.so 112: 112: 112: initialize program: ./main 112: 112: 112: transferring control: ./main 112: a1.c a1.c ......從日志中可以看到,動(dòng)態(tài)庫的加載順序如下:b1.so,b2.so,a1.so,a2.so,這些庫根據(jù)依賴關(guān)系進(jìn)行加載,使用 find library 語句可以看到它們被搜索并找到成功的路徑。
初始化的順序則是:a2.so,a1.so,b2.so,b1.so
這個(gè)順序展示了在執(zhí)行 main 函數(shù)之前,各個(gè)庫的構(gòu)造函數(shù)是如何被調(diào)用的。從中可以看出,動(dòng)態(tài)庫的初始化是按照依賴順序進(jìn)行的,即一個(gè)庫的初始化會(huì)在它所依賴的庫都初始化完成后進(jìn)行。
__attribute__((__init_priority__(PRIORITY)))是GCC提供的一個(gè)特性,用于對(duì)一個(gè)全局變量或函數(shù)的初始化優(yōu)先級(jí)進(jìn)行控制。只能用于全局或靜態(tài)對(duì)象的聲明。它改變了對(duì)象構(gòu)造函數(shù)的調(diào)用順序,其作用是在程序啟動(dòng)時(shí)(即 main() 函數(shù)執(zhí)行之前)確保不同對(duì)象的構(gòu)造函數(shù)按照指定的優(yōu)先級(jí)順序調(diào)用。PRIORITY 必須是一個(gè)介于 101 和 65535 之間的整數(shù),其中 101 是最高優(yōu)先級(jí)(最先初始化),65535 是最低優(yōu)先級(jí)(最后初始化)。
若都沒有定義優(yōu)先級(jí), 其初始化順序取決于鏈接時(shí),全局變量定義所在’.o’ 在命令行參數(shù)中的出現(xiàn)順序。
若部分全局變量使用了init_priority,部分沒有; 所有使用了init_priority的全局變量其初始化順序均先于未使用init_priority 的全局變量。
使用方式如下:
TestClass obj __attribute__((init_priority(102)))
4.2 函數(shù)的構(gòu)造/析構(gòu)順序
函數(shù)可使用 __attribute__(constructor(PRIORITY)) 和 __attribute__(destructor(PRIORITY)) 。
__attribute__(constructor(PRIORITY))屬性用于標(biāo)記函數(shù),它告訴編譯器這個(gè)函數(shù)應(yīng)該在 main() 函數(shù)執(zhí)行之前自動(dòng)執(zhí)行。如果指定了 PRIORITY,則可以影響多個(gè)此類函數(shù)的執(zhí)行順序:數(shù)值較小的 PRIORITY 意味著該初始化函數(shù)將更早執(zhí)行。
__attribute__(destructor(PRIORITY)) 修飾的函數(shù)可讓系統(tǒng)在main()函數(shù)退出或者調(diào)用了exit()之后調(diào)用。優(yōu)先級(jí)同上。
使用方式如下:
void __attribute__((constructor(102))) test()
4.3 注意事項(xiàng)
可移植性:__attribute__ 是 GCC 特有的,雖然許多其他編譯器也提供類似的擴(kuò)展,但它們?cè)诓煌幾g器之間并不兼容,應(yīng)考慮使用其他機(jī)制或添加兼容性條件編譯。
初始化依賴:當(dāng)使用這些屬性來修改初始化順序時(shí),必須非常小心地管理對(duì)象之間的依賴關(guān)系。錯(cuò)誤地規(guī)劃初始化順序會(huì)導(dǎo)致程序在使用未初始化或半初始化狀態(tài)的對(duì)象時(shí)崩潰。
默認(rèn)優(yōu)先級(jí):對(duì)于沒有指定優(yōu)先級(jí)的全局對(duì)象,編譯器也會(huì)分配一個(gè)默認(rèn)的初始化優(yōu)先級(jí)。然而,這個(gè)默認(rèn)優(yōu)先級(jí)可能因編譯器而異,所以最好顯式指定優(yōu)先級(jí)以避免不確定性。
與其他特性的兼容性:使用構(gòu)造函數(shù)屬性時(shí),請(qǐng)考慮它們可能與其他語言特性(如智能指針、靜態(tài)局部變量的延遲初始化等)的兼容性。
五、總結(jié)
上述內(nèi)容闡述了動(dòng)態(tài)鏈接的過程。從程序的整體運(yùn)行流程來看,可以分為編譯、鏈接、裝載和執(zhí)行幾個(gè)關(guān)鍵階段,以下將對(duì)這幾個(gè)階段進(jìn)行簡要總結(jié)。
主要工作 | 示例命令 | |
編譯(Compile) | 源文件被gcc/g++轉(zhuǎn)換為ELF格式對(duì)象文件,該文件包含編譯后的代碼但未綁定到依賴的地址。會(huì)在磁盤生成.o 文件 |
gcc -fPIC -c test.c -o test.o gcc -c main.c -o main.o -fPIC: 表示生成位置無關(guān)代碼 -c: 表示只執(zhí)行編譯步驟,不進(jìn)行鏈接。 -o test.o: 指定輸出的目標(biāo)文件的名稱。 |
鏈接 (Linking) |
設(shè)置必要的信息供鏈接器(ld.so)使用,為運(yùn)行時(shí)動(dòng)態(tài)鏈接準(zhǔn)備各種表結(jié)構(gòu)和引用占位符。會(huì)在磁盤生成.so 文件。 詳細(xì)過程: 創(chuàng)建符號(hào)引用的表,以便裝載器和動(dòng)態(tài)鏈接器用于后續(xù)解析。 創(chuàng)建用于運(yùn)行時(shí)符號(hào)解析的數(shù)據(jù)結(jié)構(gòu),如全局偏移表(GOT)和程序鏈接表(PLT)的占位符。 提供必要的重定向條目,告訴裝載器在哪里找到對(duì)動(dòng)態(tài)庫的所有引用。 |
gcc-shared-o libtest.so test.o gcc -o main main.o -L. -ltest -shared: 告訴鏈接器我們要?jiǎng)?chuàng)建一個(gè)共享對(duì)象,即動(dòng)態(tài)庫。 -o libtest.so: 指定生成的動(dòng)態(tài)庫文件名稱。 |
裝載(Loading) (本文的重點(diǎn)) |
動(dòng)態(tài)鏈接器工作過程,負(fù)責(zé)動(dòng)態(tài)庫裝載到內(nèi)存,并結(jié)合動(dòng)態(tài)鏈接器解析符號(hào)、進(jìn)行重定向和重新定位,確保程序可以在內(nèi)存中正確運(yùn)行。 詳細(xì)過程: 1.啟動(dòng)動(dòng)態(tài)鏈接器,通過GOT、.dynamic信息進(jìn)行自身的重定位工作,完成自舉。 2.裝載共享目標(biāo)文件:將可執(zhí)行文件和鏈接器本身符號(hào)合并入全局符號(hào)表,依次廣度優(yōu)先遍歷共享目標(biāo)文件,它們的符號(hào)表會(huì)不斷合并到全局符號(hào)表中,如果多個(gè)共享對(duì)象有相同的符號(hào),則優(yōu)先載入的共享目標(biāo)文件會(huì)屏蔽掉后面的符號(hào) 4. 重定位(內(nèi)存):對(duì)需要修正的函數(shù)調(diào)用、變量地址等進(jìn)行重定位,使它們指向正確的內(nèi)存地址。 5. 初始化 。運(yùn)行動(dòng)態(tài)庫的初始化代碼,如.init和構(gòu)造函數(shù)等。 |
./main |
運(yùn)行(Running) | 控制權(quán)交給main函數(shù)運(yùn)行,在需要時(shí)(如延遲綁定的情況),解析并更新更多的符號(hào)引用。 |
附錄 1:幾個(gè)關(guān)鍵概念
ELF (Executable and Linkable Format)
一種執(zhí)行和鏈接格式標(biāo)準(zhǔn),被用來作為Unix系統(tǒng)中的標(biāo)準(zhǔn)二進(jìn)制文件格式,包括可執(zhí)行文件、對(duì)象代碼、共享庫和核心轉(zhuǎn)儲(chǔ)(core dumps)。ELF文件包含了程序運(yùn)行所需的所有信息,如程序指令、程序入口點(diǎn)、數(shù)據(jù)和符號(hào)表等。
PIC (Position Independent Code)
概念: 地址無關(guān)代碼,指不依賴于具體加載地址能夠執(zhí)行的代碼。編譯為 PIC 意味著生成的代碼可以在進(jìn)程的地址空間中的任何位置運(yùn)行。這在動(dòng)態(tài)庫中尤為重要,因?yàn)槎鄠€(gè)程序可能共享同一動(dòng)態(tài)庫的單個(gè)副本,但這個(gè)庫可能被加載到這些程序的地址空間中的不同位置。
使用階段:編譯階段。使用 `-fPIC` 選項(xiàng)進(jìn)行編譯就可以生成位置獨(dú)立的代碼。
GOT (Global Offset Table)
概念:全局偏移表,提供了一個(gè)固定的位置,用于存儲(chǔ)外部符號(hào)的絕對(duì)地址,由鏈接器進(jìn)行填充。用于支持共享庫中的位置無關(guān)代碼(PIC)。
使用階段:鏈接/裝載。鏈接器創(chuàng)建 GOT,并在程序啟動(dòng)時(shí)由動(dòng)態(tài)鏈接器(裝載器的一部分)填充。
PLT (Procedure Linkage Table)
概念:程序連接表,與GOT共同工作用于動(dòng)態(tài)鏈接中的函數(shù)調(diào)用。存有從.got.plt 中查找外部函數(shù)地址的代碼,若是第一次調(diào)用該函數(shù),則會(huì)觸發(fā)鏈接器解析函數(shù)地址并填充在.got.plt 相應(yīng)的位置;若函數(shù)地址已經(jīng)存儲(chǔ)在.got.plt 中則直接跳轉(zhuǎn)到對(duì)應(yīng)地址繼續(xù)執(zhí)行。
使用階段: 鏈接/裝載。與 GOT 類似,PLT 的創(chuàng)建發(fā)生在鏈接階段,其填充和更新則是在程序開始運(yùn)行時(shí)、動(dòng)態(tài)符號(hào)被首次訪問時(shí)發(fā)生。
ld.so
Linux系統(tǒng)中的動(dòng)態(tài)鏈接器程序,負(fù)責(zé)加載共享庫并進(jìn)行動(dòng)態(tài)鏈接和綁定。它讀取可執(zhí)行文件指定的動(dòng)態(tài)庫依賴并將這些庫加載到內(nèi)存中,同時(shí)也處理符號(hào)的解析和重定位。當(dāng)你運(yùn)行一個(gè)動(dòng)態(tài)鏈接的可執(zhí)行文件時(shí),它首先運(yùn)行的實(shí)際上是ld.so,然后才是你的程序本身。ld.so會(huì)查看程序所需要的庫,并將它們加載到內(nèi)存中去。
關(guān)鍵 section
section 名 | 查看命令 | 實(shí)例結(jié)果 | |
.interp | 保存了動(dòng)態(tài)鏈接器的路徑 | objdump -s xxx # 查看所有 section |
.dynsym RA |
僅包含程序運(yùn)行中需要?jiǎng)討B(tài)鏈接的符號(hào),若GCC中通過__attribute__((visibility("hidden")))隱藏的符號(hào),在這里不會(huì)出現(xiàn)。 | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
'Ndx'(索引)顯示為 UND(意味著“未定義”的縮寫),表示該符號(hào)未在該共享對(duì)象中定義,并需要從其他共享對(duì)象中解析(導(dǎo)入)。 'Value' 列會(huì)有一個(gè)非零地址值,表示符號(hào)在共享對(duì)象文件(.so 文件)中的位置。 |
|||
.rela.dyn 和rela.plt RA |
重定位表段,用于存儲(chǔ)重定位信息。 .rela.dyn 對(duì)數(shù)據(jù)引用修正,修正位置:.got 和數(shù)據(jù)段 .rela.plt 對(duì)函數(shù)引用(開啟 PIC 編譯)修正,修正位置:.got.plt。只要有過程鏈表,通常就會(huì)有此表,因?yàn)閜lt導(dǎo)致了絕對(duì)跳轉(zhuǎn),那么所有plt表中所有需要?jiǎng)討B(tài)鏈接/重定位的絕對(duì)地址(可能在.got.plt或.got中,依賴于是否開啟延遲綁定),都需要通過.rela.plt記錄 |
readelf -r xxx #查看重定位表內(nèi)容 readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.plt RA |
一組跳板函數(shù),用于實(shí)現(xiàn)共享庫函數(shù)的延遲綁定。 | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.text RA |
代碼 section | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.dynamic RWA |
.dynamic中保存的是動(dòng)態(tài)鏈接器用到的基本信息,如動(dòng)態(tài)鏈接符號(hào)表(.dynsym),字符串表(.dynstr),重定位表 (.rela.dyn/rela.plt),依賴的運(yùn)行時(shí)庫,庫查找路徑等 |
readelf-dxxx # 查看.dynmaic段地址 |
.got 和.got.plt RWA |
存儲(chǔ)重定位指針的地方 |
readelf-S xxx/objdump-h XXX #查看 section 地址分布 readelf-x |
.data RWA |
用于存儲(chǔ)初始化的全局變量和靜態(tài)變量 | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.bss RWA |
用于存儲(chǔ)未初始化的全局變量和靜態(tài)變量,.bss 并不占據(jù)實(shí)際的磁盤空間,它只是一個(gè)占位符. | readelf-S xxx/objdump-h XXX #查看 section 地址分布 | |
.symtab | 不僅包括導(dǎo)出和導(dǎo)入的符號(hào),也包括局部符號(hào)(如靜態(tài)函數(shù)和靜態(tài)全局變量)和調(diào)dynsym試符號(hào)。 | readelf -s xxx # 查看所有符號(hào) |
'Ndx'(索引)顯示為 UND(意味著“未定義”的縮寫),表示該符號(hào)未在該共享對(duì)象中定義,并需要從其他共享對(duì)象中解析(導(dǎo)入)。 'Value' 列會(huì)有一個(gè)非零地址值,表示符號(hào)在共享對(duì)象文件(.so 文件)中的位置。 |
附錄 2:常用命令
顯示運(yùn)行時(shí)鏈接
dlopen:加載動(dòng)態(tài)鏈接庫(.so 文件),返回一個(gè)句柄。
dlsym:通過給定的動(dòng)態(tài)鏈接庫句柄和符號(hào)名稱,查找并返回符號(hào)的地址。
dlclose:關(guān)閉由 dlopen 打開的動(dòng)態(tài)鏈接庫句柄,釋放資源。
dlerror:返回描述最后一次錯(cuò)誤的字符串。如果沒有發(fā)生錯(cuò)誤,則返回NULL。
環(huán)境變量:
LD_LIBRARY_PATH: 為動(dòng)態(tài)鏈接器指定額外的庫搜索路徑,預(yù)先定義路徑。
LD_PRELOAD:指定在所有其他庫之前加載的共享庫列表。動(dòng)態(tài)鏈接器查看".dynamic"段里 NEEDED 類型,查找路徑依次為LD_LIBRARY_PATH、/etc/ld.so.conf (/etc/ld.so.cache)配置文件指定目錄、/lib、/usr/lib、進(jìn)行查找。即LD_PRELOAD 環(huán)境變量的庫會(huì)最先被加載。
LD_DEBUG: 設(shè)置此環(huán)境變量可以讓動(dòng)態(tài)鏈接器打印出調(diào)試信息,幫助開發(fā)者了解鏈接過程中發(fā)生了什么,包括庫搜索路徑、符號(hào)解析等。當(dāng)被設(shè)置時(shí),會(huì)輸出大量的信息到標(biāo)準(zhǔn)輸出,這可能會(huì)導(dǎo)致性能下降,所以通常只在調(diào)試期間使用它。格式為:LD_DEBUG=[參數(shù)值] ./[程序名稱] ,例如LD_DEBUG=libs ./your_program。參數(shù)如下:
libs打印出每個(gè)需要加載的庫的信息,包括庫的搜索和加載過程。
files報(bào)告輸入文件即二進(jìn)制對(duì)象(程序或庫)的打開、關(guān)閉操作。
symbols報(bào)告符號(hào)解析的詳細(xì)信息,包括符號(hào)查找和綁定到具體地址的過程。
bindings提供綁定到全局和局部符號(hào)的信息。
versions輸出有關(guān)版本化符號(hào)信息,可以顯示庫的版本綁定情況。
all輸出上述所有調(diào)試信息,提供最全面的調(diào)試信息。
工具使用
ldd:用于打印共享庫的依賴關(guān)系。例如,運(yùn)行 ldd /path/to/your/program 可以列出程序運(yùn)行所需的所有動(dòng)態(tài)鏈接庫。
strip:用于去除程序或庫中的調(diào)試信息、符號(hào)表.symtab等,可以減小產(chǎn)生的二進(jìn)制文件大小。使用該命令時(shí),需要注意由于去除了一些信息,會(huì)使得調(diào)試變得更加困難。使用方法:strip --strip-debug /path/to/library.so
-
Linux
+關(guān)注
關(guān)注
87文章
11314瀏覽量
209786 -
動(dòng)態(tài)鏈接
+關(guān)注
關(guān)注
0文章
5瀏覽量
5758
原文標(biāo)題:動(dòng)態(tài)鏈接的魔法:Linux下動(dòng)態(tài)鏈接庫機(jī)制探討
文章出處:【微信號(hào):OSC開源社區(qū),微信公眾號(hào):OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論