本文將深入探討Linux系統(tǒng)中的動態(tài)鏈接庫機制,這其中包括但不限于全局符號介入、延遲綁定以及地址無關(guān)代碼等內(nèi)容。
引言
在軟件開發(fā)過程中,動態(tài)庫鏈接問題時常出現(xiàn),這可能導(dǎo)致符號沖突,從而引起程序運行異常或崩潰。為深入理解動態(tài)鏈接機制及其工作原理,我重溫了《程序員的自我修養(yǎng)》,并通過實踐演示與反匯編分析,了解了動態(tài)鏈接的過程。
本文將深入探討Linux系統(tǒng)中的動態(tài)鏈接庫機制,這其中包括但不限于全局符號介入(Global Symbol Interposition)、延遲綁定(Lazy Binding)以及地址無關(guān)代碼(Position-Independent Code, PIC)等內(nèi)容。通過對上述概念和技術(shù)細節(jié)的討論,希望能夠提供一個更加清晰的認知框架,從而揭示符號沖突背后隱藏的本質(zhì)原因。這樣一來,在實際軟件開發(fā)過程中遇到類似問題時,開發(fā)者們便能更加游刃有余地采取措施進行預(yù)防或解決,確保程序穩(wěn)定運行的同時提升整體質(zhì)量與用戶體驗。
為便于讀者查閱,本文中提及的一些基本概念,例如ELF、PIC、GOT、PLT、常用的section等,被歸納整理于附錄部分。
一、先舉個
我們將通過一個簡單的 C 語言程序,逐步探討動態(tài)鏈接庫在模塊內(nèi)部及模塊間的運行機制,其中涉及變量和函數(shù)之間的交互過程。同時,我們將使用 -fPIC 選項,以確保生成位置無關(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 編譯選項可以生成位置無關(guān)的代碼,適用于創(chuàng)建共享庫。代碼中包含了多個場景:
模塊內(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,這是一個在其他模塊中定義的函數(shù)。ext 負責(zé)修改外部全局變量 b。
不同類型的變量:
靜態(tài)變量 a 僅在本模塊可見,其值不會在程序的其他模塊中改變,也不會因函數(shù)調(diào)用而丟失。
外部全局變量 b 可以在多個模塊間共享,其值在整個程序中是唯一且可改變的。
模塊內(nèi)的全局變量 c 僅能在當(dāng)前模塊訪問和修改。
我們都知道動態(tài)鏈接庫需要能夠在多個進程之間共享同一段代碼。為了實現(xiàn)這一點,代碼必須是位置無關(guān)的,從而可以在加載時按需被鏈接到不同的地址,編譯時添加編譯選項-fPIC 可以生成地址無關(guān)代碼,那這些函數(shù)和變量運行時,如何做到呢?接下來將逐步分析動態(tài)鏈接的過程。
二、從例子來深入動態(tài)鏈接庫
2.1 模塊內(nèi)函數(shù)調(diào)用
例子中 foo 函數(shù)實現(xiàn)中有兩個函數(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:相對偏移調(diào)用指令
ec ff ff ff:小端 0XFFFFFFEC 是-20 的補碼,該數(shù)值為目的地址相對于當(dāng)前指令下一條指令的偏移。即 inner 地址為 0x7f6(下一條指令偏移) - 0x14 = 0x7e2
結(jié)論:靜態(tài)函數(shù)調(diào)用很簡單,通過相對地址偏移就可以跳轉(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],這是一個間接跳轉(zhuǎn)(jmp)指令,運行跳轉(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)這個地址在.got.plt section,0x00000686, 該地址存的地址為
0000000000000680那上面一系列地址跳轉(zhuǎn)是在干什么?用一個示意圖表示 bar 首次地址重定位過程(橙色是調(diào)用入口,藍色是運行的指令,紫色代表修正的地址)。: 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ù)實現(xiàn)不展開,該函數(shù)的入?yún)槿霔5姆?a target="_blank">索引 index 和庫 ID,解析過程會依賴.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 中重定位項的地址。
Info - 包含兩個部分:符號的索引和重定位類型。在這種情況下,重定位類型是 R_X86_64_JUMP_SLOT,用于處理函數(shù)調(diào)用的跳轉(zhuǎn)。
Type - 描述了重定位的類型,這里是 R_X86_64_JUMP_SLOT,用于通過懶加載解析符號的PLT入口。其他類型還有很多,常見的還有
R_X86_64_GLOB_DAT - 設(shè)置全局偏移表的內(nèi)容。
R_X86_64_64 - 64位直接重定位;修改64位的值。
R_X86_64_PC32 - 32位PC相對重定位;修改指令內(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 - 需要基地址重置,用于模塊加載專用的相對地址調(diào)整。
R_X86_64_GOTPCREL - 訪問GOT的PC相對重定位。
Sym. Value - 是符號在它本身定義模塊內(nèi)的值。在重定位發(fā)生之前,符號可能還沒有最終的運行時地址。對于本地符號(比如 bar 函數(shù)),這里通常是它們在當(dāng)前模塊中的偏移地址。對于外部符號(比如 printf),在重定位前這里通常是 0,表示地址還未確定。
Sym. Name + Addend - 顯示了符號的名稱以及添加量。添加量在這里是 0,因為我們正在查看 .rela 格式的重定位項,添加量已經(jīng)包含在每個重定位項中。
在運行時,動態(tài)鏈接器會依據(jù)這些重定位項進行地址解析工作。例如,當(dāng)程序第一次調(diào)用 printf 時,控制流首先跳轉(zhuǎn)到 printf 在 PLT 中的對應(yīng)項,PLT 中會有一段存根代碼觸發(fā)動態(tài)鏈接器,動態(tài)鏈接器解析出 printf 的真實地址并更新 GOT 中對應(yīng)的地址。
第二次調(diào)用
運行后地址重定位后,第二次調(diào)用就會簡單很多,如下圖所示:
使用 GDB 調(diào)試運行后,單步調(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 入口地址,也對應(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)限,在程序運行時,修正.got.plt 對應(yīng)函數(shù)指向的.text (不可寫)地址,從而實現(xiàn)了地址無關(guān)代碼。
該過程還隱藏了一個知識點,延遲綁定(lazy binding)。動態(tài)鏈接器在運行時完成,若已一開始執(zhí)行,要加載完所有的符號的話,想必會減慢程序的啟動速度,影響性能。所以當(dāng)函數(shù)第一次被用到時再進行綁定,如果沒有用就不綁定,這樣可以大大加快程序啟動速度。本例子中的 bar 也是在調(diào)用時才進行重定向,不調(diào)用不進行地址重定向綁定,即實現(xiàn)了延遲綁定效果。
是不是外部函數(shù)重定向一定在 .rela.plt?
不是,如果是PIC 編譯,會在.rela.plt;如果不是PIC 編譯,會在.rela.dyn 出現(xiàn)。
原因:開啟 PIC 調(diào)用指令會指向 PLT 中的一個條目,需要.rela.plt section 配合實現(xiàn) Lazy Binding,.rela.dyn 段用于動態(tài)鏈接器在加載時將符號綁定到其運行時地址的重定位條目。它包含了不特定于PLT條目的其他動態(tài)重定位信息,.rela.plt 主要針對PLT進行重定位,用于動態(tài)鏈接時解析函數(shù)地址,實現(xiàn)惰性綁定,而 .rela.dyn 用于更廣泛的動態(tài)重定位需求。
疑問?
問題一:模塊內(nèi)全局函數(shù)調(diào)用和模塊間全局函數(shù)調(diào)用有什么區(qū)別?
問題二:為什么都是函數(shù)調(diào)用,靜態(tài)函數(shù)和全局函數(shù)調(diào)用跳轉(zhuǎn)差別這么大?
這兩個問題先不著急回答,我們接著看模塊間函數(shù)調(diào)用。
2.2 模塊間函數(shù)調(diào)用
例子中是 foo() 對 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é)的第一個問題,模塊內(nèi)和模塊間全局函數(shù)調(diào)用沒有區(qū)別,為什么呢?
先回憶下加載過程,動態(tài)鏈接器完成自舉后,會將可執(zhí)行文件和鏈接器本身的符號表都合并到一個符號表中,該符號表叫做全局符號表(Global Symbol Table)。當(dāng)一個符號需要被加入全局符號表時,如果相同的符號已經(jīng)存在,則后加入的符號被忽略,這種規(guī)則叫做全局符號介入。
由于全局符號介入規(guī)則,若上一節(jié)的模塊內(nèi)部函數(shù)調(diào)用 bar() 直接采用相對地址調(diào)用話,可能會被其他模塊的同名函數(shù)符號覆蓋,那相對地址就是無法準(zhǔn)確找到正確的函數(shù)地址,故模塊內(nèi)和模塊外的函數(shù)調(diào)用,都需要通過.got.plt 重定位方法間接調(diào)用。
那上一節(jié)第二個問題答案也顯而易見,靜態(tài)函數(shù)不涉及全局符號介入問題,可以通過模塊內(nèi)部相對地址跳轉(zhuǎn)就可以。這樣調(diào)用的尋址速度也比全局函數(shù)的尋址速度快。
為了更深入理解全局符號介入,我們再舉個例子。
/* 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() 的實現(xiàn)使用了 a1.so 中的定義。因此,無論 b2.so 如何變化,main 程序中調(diào)用的都始終是 a1.so 的實現(xiàn)。這種現(xiàn)象強調(diào)了在動態(tài)鏈接庫中符號的解析順序及如何影響最終的執(zhí)行結(jié)果,開發(fā)者在設(shè)計接口時需謹慎考慮符號的命名和庫的加載順序,以避免潛在的符號沖突和不確定性。
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 變量的訪問直接通過偏移量完成,這種方式更高效,因為 static 變量的作用域限制在同一個編譯單元,所以它們的地址可以在編譯時確定(相對于 rip)。而非 static 變量(包括定義在當(dāng)前模塊的全局變量和 extern 變量)可能被其他模塊引用或修改,其地址需要在運行時通過動態(tài)鏈接器解析,對于全局和 extern 變量,共享庫使用基于 rip 的尋址加上 運行時重定位.got 段中地址,以確保位置無關(guān)。
全局變量的地址不存在延遲綁定,因為通常會在加載時解析,并通過全局偏移表(Global Offset Table, GOT)來訪問,而不是延遲到首次使用時。因此,把它們的地址解析延遲將不會帶來明顯的優(yōu)勢,而且會在運行時增加額外的性能負擔(dān)。
三、地址無關(guān)延伸
3.1 隱藏符號影響
如果把 bar 和變量 c 使用__attribute__((visibility("hidden")))隱藏的符號,那函數(shù)調(diào)用跳轉(zhuǎn)會有什么變化?
#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 直接可以通過相對地址跳轉(zhuǎ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)。隱藏的符號 bar() 和 c 也不會出現(xiàn)在動態(tài)鏈接庫的動態(tài)符號表(.dynsym)中,因此它們在鏈接時不可見于其他共享對象或者可執(zhí)行文件,所以隱藏符號不存在全局符號介入的場景。
3.2 關(guān)于 PIC 回答幾個小問題
如何區(qū)分一個 DSO 是否為 PIC
readelf -d xxx.so | grep TEXTREL
如果沒有輸出,則動態(tài)庫是使用 PIC 生成的。文本重定位(TEXTREL)意味著代碼部分(.text section)需要修改以引用正確的地址,在非PIC的代碼中,會存在基于絕對地址的引用,這就需要在加載時進行修改,從而使得代碼能夠正確運行,這個過程就是文本重定位。
2. 如何區(qū)分一個靜態(tài)庫是否為 PIC
ar -t xxx.a readelf -r xxx.o你需要檢查輸出中是否有基于絕對地址的重定位類型比如 R_X86_64_GOTPCREL 或其他類似的不是專為 PIC 代碼的重定位類型。
3. 假設(shè)靜態(tài)編譯庫編譯不使用-fPIC,動態(tài)庫編譯使用-fPIC,是否 ok?
不行。實測靜態(tài)庫 a.a 不使用-fPIC,動態(tài)庫 b.so 使用-fPIC,可執(zhí)行程序 main 鏈接兩個庫會編譯失敗。報錯日志如下:
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 對象文件是沒有使用 -fPIC 編譯的,因此包含以 PC 相對的方式(R_X86_64_PC32 relocation type)引用全局變量 b。這種類型的重定位不兼容于動態(tài)庫的創(chuàng)建,因為它要求代碼必須在特定地址執(zhí)行,而動態(tài)庫加載的地址在運行時是未知的,甚至每次運行都可能不同。即靜態(tài)庫的代碼假定某些數(shù)據(jù)或函數(shù)存在于固定地址,而該地址已經(jīng)被其他代碼或庫占用,則可能會導(dǎo)致鏈接錯誤或運行時錯誤。
要修復(fù)這個錯誤,你需要重新編譯 nopic_common.o,將其中的代碼編譯為位置無關(guān)代碼(PIC)。
4. 為什么動態(tài)庫編譯時不默認采用PIC:
歷史原因:歷史慣性,較早的編譯器版本中沒有將生成PIC作為默認選項。
選項傳遞的問題:-fPIC是編譯器的選項,是在源代碼編譯階段決定的,而-shared是鏈接器的選項, 是在不同階段,所以無法通過-shared自動啟用-fPIC。
性能:雖然PIC對于共享庫的高效運行是很重要的,但在某些情況下PIC代碼也可能稍微慢于非PIC代碼,因為它需要使用間接地址引用全局變量和函數(shù)。這種性能影響一般是很小的,但在對性能要求非常高的應(yīng)用程序中,這可能是一個因素。
編譯器和構(gòu)建系統(tǒng)設(shè)計:編譯器和構(gòu)建系統(tǒng)往往允許開發(fā)者根據(jù)項目需求選擇是否生成PIC。允許靈活配置使開發(fā)者能夠根據(jù)具體的使用場景和需求,選擇最合適的編譯選項。
3.3 動態(tài)和靜態(tài)鏈接的重定向區(qū)別
靜態(tài)鏈接 | 動態(tài)鏈接 | |
階段 | 編譯鏈接階段 | 裝載運行階段 |
執(zhí)行控制權(quán) | 控制權(quán)直接交給可執(zhí)行文件 | 控制權(quán)限交給動態(tài)鏈接器,映射完成后再交給可執(zhí)行文件 |
運行尋址速度 | 速度快 | 由于間接跳轉(zhuǎn),比靜態(tài)鏈接慢約 1%~5%,使用 lazy binding 改善 |
重定位表名 |
.rela.text 代碼段重定位表 .rela.data 數(shù)據(jù)段重定位表 |
.rela.plt 代碼段重定位表 .rela.dyn 數(shù)據(jù)段重定位表 |
四、如何指定全局變量和函數(shù)裝載時的順序
上面主要介紹了動態(tài)裝載過程,在初始化和反初始化的時候,特別需要關(guān)注全局變量和函數(shù)的構(gòu)造與析構(gòu)順序。這些過程直接影響到模塊間的依賴關(guān)系和對象之間的交互。因此,我們需要了解如何通過使用特定的屬性來控制這些順序,以確保程序的穩(wěn)定性和預(yù)期行為。特別是在多模塊動態(tài)庫的環(huán)境中,合理安排初始化和反初始化的順序,是避免運行時錯誤和崩潰的重要措施。
4.1 全局變量初始化順序
對于跨共享庫的全局變量,其初始化順序受這些共享庫之間的依賴關(guān)系影響。如果共享庫 A 依賴于共享庫 B,那么 B 的初始化代碼將會在 A 的初始化代碼之前執(zhí)行,因此 B 中的全局變量會在 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 ......從日志中可以看到,動態(tài)庫的加載順序如下:b1.so,b2.so,a1.so,a2.so,這些庫根據(jù)依賴關(guān)系進行加載,使用 find library 語句可以看到它們被搜索并找到成功的路徑。
初始化的順序則是:a2.so,a1.so,b2.so,b1.so
這個順序展示了在執(zhí)行 main 函數(shù)之前,各個庫的構(gòu)造函數(shù)是如何被調(diào)用的。從中可以看出,動態(tài)庫的初始化是按照依賴順序進行的,即一個庫的初始化會在它所依賴的庫都初始化完成后進行。
__attribute__((__init_priority__(PRIORITY)))是GCC提供的一個特性,用于對一個全局變量或函數(shù)的初始化優(yōu)先級進行控制。只能用于全局或靜態(tài)對象的聲明。它改變了對象構(gòu)造函數(shù)的調(diào)用順序,其作用是在程序啟動時(即 main() 函數(shù)執(zhí)行之前)確保不同對象的構(gòu)造函數(shù)按照指定的優(yōu)先級順序調(diào)用。PRIORITY 必須是一個介于 101 和 65535 之間的整數(shù),其中 101 是最高優(yōu)先級(最先初始化),65535 是最低優(yōu)先級(最后初始化)。
若都沒有定義優(yōu)先級, 其初始化順序取決于鏈接時,全局變量定義所在’.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ù),它告訴編譯器這個函數(shù)應(yīng)該在 main() 函數(shù)執(zhí)行之前自動執(zhí)行。如果指定了 PRIORITY,則可以影響多個此類函數(shù)的執(zhí)行順序:數(shù)值較小的 PRIORITY 意味著該初始化函數(shù)將更早執(zhí)行。
__attribute__(destructor(PRIORITY)) 修飾的函數(shù)可讓系統(tǒng)在main()函數(shù)退出或者調(diào)用了exit()之后調(diào)用。優(yōu)先級同上。
使用方式如下:
void __attribute__((constructor(102))) test()
4.3 注意事項
可移植性:__attribute__ 是 GCC 特有的,雖然許多其他編譯器也提供類似的擴展,但它們在不同編譯器之間并不兼容,應(yīng)考慮使用其他機制或添加兼容性條件編譯。
初始化依賴:當(dāng)使用這些屬性來修改初始化順序時,必須非常小心地管理對象之間的依賴關(guān)系。錯誤地規(guī)劃初始化順序會導(dǎo)致程序在使用未初始化或半初始化狀態(tài)的對象時崩潰。
默認優(yōu)先級:對于沒有指定優(yōu)先級的全局對象,編譯器也會分配一個默認的初始化優(yōu)先級。然而,這個默認優(yōu)先級可能因編譯器而異,所以最好顯式指定優(yōu)先級以避免不確定性。
與其他特性的兼容性:使用構(gòu)造函數(shù)屬性時,請考慮它們可能與其他語言特性(如智能指針、靜態(tài)局部變量的延遲初始化等)的兼容性。
五、總結(jié)
上述內(nèi)容闡述了動態(tài)鏈接的過程。從程序的整體運行流程來看,可以分為編譯、鏈接、裝載和執(zhí)行幾個關(guān)鍵階段,以下將對這幾個階段進行簡要總結(jié)。
主要工作 | 示例命令 | |
編譯(Compile) | 源文件被gcc/g++轉(zhuǎn)換為ELF格式對象文件,該文件包含編譯后的代碼但未綁定到依賴的地址。會在磁盤生成.o 文件 |
gcc -fPIC -c test.c -o test.o gcc -c main.c -o main.o -fPIC: 表示生成位置無關(guān)代碼 -c: 表示只執(zhí)行編譯步驟,不進行鏈接。 -o test.o: 指定輸出的目標(biāo)文件的名稱。 |
鏈接 (Linking) |
設(shè)置必要的信息供鏈接器(ld.so)使用,為運行時動態(tài)鏈接準(zhǔn)備各種表結(jié)構(gòu)和引用占位符。會在磁盤生成.so 文件。 詳細過程: 創(chuàng)建符號引用的表,以便裝載器和動態(tài)鏈接器用于后續(xù)解析。 創(chuàng)建用于運行時符號解析的數(shù)據(jù)結(jié)構(gòu),如全局偏移表(GOT)和程序鏈接表(PLT)的占位符。 提供必要的重定向條目,告訴裝載器在哪里找到對動態(tài)庫的所有引用。 |
gcc-shared-o libtest.so test.o gcc -o main main.o -L. -ltest -shared: 告訴鏈接器我們要創(chuàng)建一個共享對象,即動態(tài)庫。 -o libtest.so: 指定生成的動態(tài)庫文件名稱。 |
裝載(Loading) (本文的重點) |
動態(tài)鏈接器工作過程,負責(zé)動態(tài)庫裝載到內(nèi)存,并結(jié)合動態(tài)鏈接器解析符號、進行重定向和重新定位,確保程序可以在內(nèi)存中正確運行。 詳細過程: 1.啟動動態(tài)鏈接器,通過GOT、.dynamic信息進行自身的重定位工作,完成自舉。 2.裝載共享目標(biāo)文件:將可執(zhí)行文件和鏈接器本身符號合并入全局符號表,依次廣度優(yōu)先遍歷共享目標(biāo)文件,它們的符號表會不斷合并到全局符號表中,如果多個共享對象有相同的符號,則優(yōu)先載入的共享目標(biāo)文件會屏蔽掉后面的符號 4. 重定位(內(nèi)存):對需要修正的函數(shù)調(diào)用、變量地址等進行重定位,使它們指向正確的內(nèi)存地址。 5. 初始化 。運行動態(tài)庫的初始化代碼,如.init和構(gòu)造函數(shù)等。 |
./main |
運行(Running) | 控制權(quán)交給main函數(shù)運行,在需要時(如延遲綁定的情況),解析并更新更多的符號引用。 |
附錄 1:幾個關(guān)鍵概念
ELF (Executable and Linkable Format)
一種執(zhí)行和鏈接格式標(biāo)準(zhǔn),被用來作為Unix系統(tǒng)中的標(biāo)準(zhǔn)二進制文件格式,包括可執(zhí)行文件、對象代碼、共享庫和核心轉(zhuǎn)儲(core dumps)。ELF文件包含了程序運行所需的所有信息,如程序指令、程序入口點、數(shù)據(jù)和符號表等。
PIC (Position Independent Code)
概念: 地址無關(guān)代碼,指不依賴于具體加載地址能夠執(zhí)行的代碼。編譯為 PIC 意味著生成的代碼可以在進程的地址空間中的任何位置運行。這在動態(tài)庫中尤為重要,因為多個程序可能共享同一動態(tài)庫的單個副本,但這個庫可能被加載到這些程序的地址空間中的不同位置。
使用階段:編譯階段。使用 `-fPIC` 選項進行編譯就可以生成位置獨立的代碼。
GOT (Global Offset Table)
概念:全局偏移表,提供了一個固定的位置,用于存儲外部符號的絕對地址,由鏈接器進行填充。用于支持共享庫中的位置無關(guān)代碼(PIC)。
使用階段:鏈接/裝載。鏈接器創(chuàng)建 GOT,并在程序啟動時由動態(tài)鏈接器(裝載器的一部分)填充。
PLT (Procedure Linkage Table)
概念:程序連接表,與GOT共同工作用于動態(tài)鏈接中的函數(shù)調(diào)用。存有從.got.plt 中查找外部函數(shù)地址的代碼,若是第一次調(diào)用該函數(shù),則會觸發(fā)鏈接器解析函數(shù)地址并填充在.got.plt 相應(yīng)的位置;若函數(shù)地址已經(jīng)存儲在.got.plt 中則直接跳轉(zhuǎn)到對應(yīng)地址繼續(xù)執(zhí)行。
使用階段: 鏈接/裝載。與 GOT 類似,PLT 的創(chuàng)建發(fā)生在鏈接階段,其填充和更新則是在程序開始運行時、動態(tài)符號被首次訪問時發(fā)生。
ld.so
Linux系統(tǒng)中的動態(tài)鏈接器程序,負責(zé)加載共享庫并進行動態(tài)鏈接和綁定。它讀取可執(zhí)行文件指定的動態(tài)庫依賴并將這些庫加載到內(nèi)存中,同時也處理符號的解析和重定位。當(dāng)你運行一個動態(tài)鏈接的可執(zhí)行文件時,它首先運行的實際上是ld.so,然后才是你的程序本身。ld.so會查看程序所需要的庫,并將它們加載到內(nèi)存中去。
關(guān)鍵 section
section 名 | 查看命令 | 實例結(jié)果 | |
.interp | 保存了動態(tài)鏈接器的路徑 | objdump -s xxx # 查看所有 section |
.dynsym RA |
僅包含程序運行中需要動態(tài)鏈接的符號,若GCC中通過__attribute__((visibility("hidden")))隱藏的符號,在這里不會出現(xiàn)。 | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
'Ndx'(索引)顯示為 UND(意味著“未定義”的縮寫),表示該符號未在該共享對象中定義,并需要從其他共享對象中解析(導(dǎo)入)。 'Value' 列會有一個非零地址值,表示符號在共享對象文件(.so 文件)中的位置。 |
|||
.rela.dyn 和rela.plt RA |
重定位表段,用于存儲重定位信息。 .rela.dyn 對數(shù)據(jù)引用修正,修正位置:.got 和數(shù)據(jù)段 .rela.plt 對函數(shù)引用(開啟 PIC 編譯)修正,修正位置:.got.plt。只要有過程鏈表,通常就會有此表,因為plt導(dǎo)致了絕對跳轉(zhuǎn),那么所有plt表中所有需要動態(tài)鏈接/重定位的絕對地址(可能在.got.plt或.got中,依賴于是否開啟延遲綁定),都需要通過.rela.plt記錄 |
readelf -r xxx #查看重定位表內(nèi)容 readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.plt RA |
一組跳板函數(shù),用于實現(xiàn)共享庫函數(shù)的延遲綁定。 | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.text RA |
代碼 section | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.dynamic RWA |
.dynamic中保存的是動態(tài)鏈接器用到的基本信息,如動態(tài)鏈接符號表(.dynsym),字符串表(.dynstr),重定位表 (.rela.dyn/rela.plt),依賴的運行時庫,庫查找路徑等 |
readelf-dxxx # 查看.dynmaic段地址 |
.got 和.got.plt RWA |
存儲重定位指針的地方 |
readelf-S xxx/objdump-h XXX #查看 section 地址分布 readelf-x |
.data RWA |
用于存儲初始化的全局變量和靜態(tài)變量 | readelf-S xxx/objdump-h XXX #查看 section 地址分布 |
.bss RWA |
用于存儲未初始化的全局變量和靜態(tài)變量,.bss 并不占據(jù)實際的磁盤空間,它只是一個占位符. | readelf-S xxx/objdump-h XXX #查看 section 地址分布 | |
.symtab | 不僅包括導(dǎo)出和導(dǎo)入的符號,也包括局部符號(如靜態(tài)函數(shù)和靜態(tài)全局變量)和調(diào)dynsym試符號。 | readelf -s xxx # 查看所有符號 |
'Ndx'(索引)顯示為 UND(意味著“未定義”的縮寫),表示該符號未在該共享對象中定義,并需要從其他共享對象中解析(導(dǎo)入)。 'Value' 列會有一個非零地址值,表示符號在共享對象文件(.so 文件)中的位置。 |
附錄 2:常用命令
顯示運行時鏈接
dlopen:加載動態(tài)鏈接庫(.so 文件),返回一個句柄。
dlsym:通過給定的動態(tài)鏈接庫句柄和符號名稱,查找并返回符號的地址。
dlclose:關(guān)閉由 dlopen 打開的動態(tài)鏈接庫句柄,釋放資源。
dlerror:返回描述最后一次錯誤的字符串。如果沒有發(fā)生錯誤,則返回NULL。
環(huán)境變量:
LD_LIBRARY_PATH: 為動態(tài)鏈接器指定額外的庫搜索路徑,預(yù)先定義路徑。
LD_PRELOAD:指定在所有其他庫之前加載的共享庫列表。動態(tài)鏈接器查看".dynamic"段里 NEEDED 類型,查找路徑依次為LD_LIBRARY_PATH、/etc/ld.so.conf (/etc/ld.so.cache)配置文件指定目錄、/lib、/usr/lib、進行查找。即LD_PRELOAD 環(huán)境變量的庫會最先被加載。
LD_DEBUG: 設(shè)置此環(huán)境變量可以讓動態(tài)鏈接器打印出調(diào)試信息,幫助開發(fā)者了解鏈接過程中發(fā)生了什么,包括庫搜索路徑、符號解析等。當(dāng)被設(shè)置時,會輸出大量的信息到標(biāo)準(zhǔn)輸出,這可能會導(dǎo)致性能下降,所以通常只在調(diào)試期間使用它。格式為:LD_DEBUG=[參數(shù)值] ./[程序名稱] ,例如LD_DEBUG=libs ./your_program。參數(shù)如下:
libs打印出每個需要加載的庫的信息,包括庫的搜索和加載過程。
files報告輸入文件即二進制對象(程序或庫)的打開、關(guān)閉操作。
symbols報告符號解析的詳細信息,包括符號查找和綁定到具體地址的過程。
bindings提供綁定到全局和局部符號的信息。
versions輸出有關(guān)版本化符號信息,可以顯示庫的版本綁定情況。
all輸出上述所有調(diào)試信息,提供最全面的調(diào)試信息。
工具使用
ldd:用于打印共享庫的依賴關(guān)系。例如,運行 ldd /path/to/your/program 可以列出程序運行所需的所有動態(tài)鏈接庫。
strip:用于去除程序或庫中的調(diào)試信息、符號表.symtab等,可以減小產(chǎn)生的二進制文件大小。使用該命令時,需要注意由于去除了一些信息,會使得調(diào)試變得更加困難。使用方法:strip --strip-debug /path/to/library.so
-
Linux
+關(guān)注
關(guān)注
87文章
11304瀏覽量
209483 -
動態(tài)鏈接
+關(guān)注
關(guān)注
0文章
5瀏覽量
5749
原文標(biāo)題:動態(tài)鏈接的魔法:Linux下動態(tài)鏈接庫機制探討
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論