色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

深入探討Linux系統(tǒng)中的動(dòng)態(tài)鏈接庫機(jī)制

OSC開源社區(qū) ? 來源:OSC開源社區(qū) ? 2024-12-18 10:06 ? 次閱讀

本文將深入探討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 :
 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>
那上面一系列地址跳轉(zhuǎn)是在干什么?用一個(gè)示意圖表示 bar 首次地址重定位過程(橙色是調(diào)用入口,藍(lán)色是運(yùn)行的指令,紫色代表修正的地址)。

bf05e634-bad8-11ef-8732-92fbcf53809c.png

_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ì)簡單很多,如下圖所示:

bf1e7f8c-bad8-11ef-8732-92fbcf53809c.png

使用 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>  0x7f7a97f75686 
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

.got.plt 中 bar 地址 = 0x201018 +0x7F7A97F75000(基地址)= 0x7F7A98176018,0x7F7A98176018 內(nèi)容為0x7f7a97f75686 ,和上圖的相對(duì)地址偏移相同,重定向后結(jié)果如下


(gdb) x/16a 0x7f7a98176000
0x7f7a98176000:  0x200e08  0x7f7a983976a8
0x7f7a98176010:  0x7f7a9818d890 <_dl_runtime_resolve_xsave>  0x7f7a97f757b8 
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
0x7f7a97f757b8 為代碼段,0x7f7a97f757b8 - 0x7F7A97F75000(基地址)=0x7B8,該偏移在.text 的 bar 入口地址,也對(duì)應(yīng)起來了。

抽象一下,如下示意圖:

bf3608b4-bad8-11ef-8732-92fbcf53809c.png

通過上圖指令跳轉(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*/
#include 
void 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
                  ALLOC
static 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ì)有什么變化?


#include 
static 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 ==> .data section

查看.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 status
nopic_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

bf443fb0-bad8-11ef-8732-92fbcf53809c.png

.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 地址分布

bf5310bc-bad8-11ef-8732-92fbcf53809c.png


'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 地址分布

bf6fe296-bad8-11ef-8732-92fbcf53809c.png

.plt
RA
一組跳板函數(shù),用于實(shí)現(xiàn)共享庫函數(shù)的延遲綁定。 readelf-S xxx/objdump-h XXX #查看 section 地址分布

bf88313e-bad8-11ef-8732-92fbcf53809c.png

.text
RA
代碼 section readelf-S xxx/objdump-h XXX #查看 section 地址分布

bf8c3a36-bad8-11ef-8732-92fbcf53809c.png

.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段地址

bf9100d4-bad8-11ef-8732-92fbcf53809c.png

.got 和.got.plt
RWA
存儲(chǔ)重定位指針的地方 readelf-S xxx/objdump-h XXX #查看 section 地址分布
readelf-x
# 查看特定 section 內(nèi)容

bfa01506-bad8-11ef-8732-92fbcf53809c.png

bfa429de-bad8-11ef-8732-92fbcf53809c.png

.data
RWA
用于存儲(chǔ)初始化的全局變量和靜態(tài)變量 readelf-S xxx/objdump-h XXX #查看 section 地址分布

bfb4869e-bad8-11ef-8732-92fbcf53809c.png

.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)

bfcac724-bad8-11ef-8732-92fbcf53809c.png


'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

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 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)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Linux動(dòng)態(tài)鏈接庫的基本概念

    學(xué)習(xí)Linux動(dòng)態(tài)鏈接庫是一個(gè)繞不開的話題,我們今天就一起來看一下什么是動(dòng)態(tài)鏈接庫動(dòng)態(tài)
    發(fā)表于 09-27 14:31 ?1558次閱讀

    關(guān)于使用動(dòng)態(tài)鏈接庫及圖像采集的問題

    ,但是在調(diào)用動(dòng)態(tài)鏈接庫的過程,有一個(gè)函數(shù)其中一個(gè)參數(shù)是圖像顯示控件的句柄,就像VB的picture控件(picture.hwn),在LAVIEW
    發(fā)表于 05-26 18:05

    關(guān)于labview'的動(dòng)態(tài)鏈接庫的問題

    最近使用labview調(diào)用動(dòng)態(tài)鏈接庫,使用vs2017生成dll文件,然后調(diào)用,但是為什么輸入數(shù)組的情況下輸出一直為0呢,我使用公式節(jié)點(diǎn)調(diào)用同樣的c語言,就沒問題?請(qǐng)教大佬們?cè)趺唇鉀Q?還有我想問一下labview是調(diào)用公式節(jié)點(diǎn)的執(zhí)行速度快還是調(diào)用
    發(fā)表于 03-14 11:26

    基于動(dòng)態(tài)鏈接庫技術(shù)的感應(yīng)器非線性特性校正

    提出一種基于動(dòng)態(tài)鏈接庫技術(shù)的傳感器非線性特性校正新方法。將傳感器是數(shù)據(jù)采集程序與傳感器的非線性特性校正算法置于同一個(gè)動(dòng)態(tài)鏈接庫,這樣應(yīng)用程
    發(fā)表于 06-25 09:55 ?26次下載

    動(dòng)態(tài)鏈接庫在LabVIEW的高級(jí)應(yīng)用

    LabVIEW 的提供了調(diào)用共享庫函數(shù)的接口,但是一些現(xiàn)成的函數(shù)卻因?yàn)榻涌趨?shù)類型不同而不能在LabVIEW 中使用。利用重新編寫動(dòng)態(tài)鏈接庫的方法可以建立舊函數(shù)
    發(fā)表于 08-04 10:09 ?57次下載

    Linux系統(tǒng)共享編程

    一、說明 類似Windows系統(tǒng)動(dòng)態(tài)鏈接庫Linux也有相應(yīng)的共享
    發(fā)表于 09-13 16:49 ?24次下載

    C++動(dòng)態(tài)鏈接庫的創(chuàng)建和調(diào)用

    動(dòng)態(tài)連接的創(chuàng)建步驟: 一、創(chuàng)建Non-MFC DLL動(dòng)態(tài)鏈接庫 1、打開File —> New —> Project選項(xiàng),選擇Win32 Dynamic-Link Library
    發(fā)表于 11-24 18:13 ?7次下載

    深入分析Windows和Linux動(dòng)態(tài)應(yīng)用異同

    深入分析Windows和Linux動(dòng)態(tài)應(yīng)用異同 摘要:動(dòng)態(tài)鏈接庫技術(shù)實(shí)現(xiàn)和設(shè)計(jì)程序常用的技
    發(fā)表于 10-22 11:36 ?1296次閱讀

    LINUX環(huán)境下CLIPS動(dòng)態(tài)鏈接庫的實(shí)現(xiàn)方法

    LINUX環(huán)境下,為了簡便、快捷地制作出CLIPS動(dòng)態(tài)鏈接庫,本文采用了CNU AUTOTOOLS把CLIPS嵌入式高級(jí)語言編譯成動(dòng)態(tài)鏈接庫
    發(fā)表于 04-14 21:18 ?30次下載

    虛擬儀器動(dòng)態(tài)鏈接庫的應(yīng)用

    本文在闡述了動(dòng)態(tài)鏈接庫技術(shù)和虛擬儀器動(dòng)態(tài)鏈接 機(jī)制
    發(fā)表于 07-05 17:17 ?27次下載
    虛擬儀器<b class='flag-5'>中</b><b class='flag-5'>動(dòng)態(tài)</b><b class='flag-5'>鏈接庫</b>的應(yīng)用

    VC++動(dòng)態(tài)鏈接庫編程深入淺出

    靜態(tài)鏈接庫動(dòng)態(tài)鏈接庫都是共享代碼的方式,如果采用靜態(tài)鏈接庫,則無論你愿不愿意,lib的指令都被直接包含在最終生成的EXE文件中了。但是若
    發(fā)表于 10-21 17:03 ?0次下載
    VC++<b class='flag-5'>動(dòng)態(tài)</b><b class='flag-5'>鏈接庫</b>編程<b class='flag-5'>深入</b>淺出

    由MATLAB的.m文件生成動(dòng)態(tài)鏈接庫的方法說明

    由MATLAB的.m文件生成動(dòng)態(tài)鏈接庫的方法說明
    發(fā)表于 08-16 18:54 ?0次下載

    英創(chuàng)信息技術(shù)WinCE設(shè)備動(dòng)態(tài)鏈接庫的制作與調(diào)用

    在使用英創(chuàng)ARM9系列主板做開發(fā)時(shí),用戶可能希望將自己一部分代碼封裝起來,隱藏代碼的實(shí)現(xiàn)過程,只提供接口供其他程序調(diào)用。使用動(dòng)態(tài)鏈接庫(Dynamic Link Library)可以很好實(shí)現(xiàn)這個(gè)要求
    的頭像 發(fā)表于 01-15 14:33 ?1157次閱讀
    英創(chuàng)信息技術(shù)WinCE設(shè)備<b class='flag-5'>動(dòng)態(tài)</b><b class='flag-5'>鏈接庫</b>的制作與調(diào)用

    單片機(jī)高階技能之動(dòng)態(tài)鏈接庫技術(shù)實(shí)現(xiàn)

    單片機(jī)高階技能之動(dòng)態(tài)鏈接庫技術(shù)實(shí)現(xiàn)
    發(fā)表于 11-17 12:21 ?13次下載
    單片機(jī)高階技能之<b class='flag-5'>動(dòng)態(tài)</b><b class='flag-5'>鏈接庫</b>技術(shù)實(shí)現(xiàn)

    Linux下的靜態(tài)鏈接庫動(dòng)態(tài)鏈接庫的區(qū)別是什么?

    學(xué)習(xí)Linux動(dòng)態(tài)鏈接庫是一個(gè)繞不開的話題,我們今天就一起來看一下什么是動(dòng)態(tài)鏈接庫動(dòng)態(tài)
    的頭像 發(fā)表于 02-17 10:49 ?1297次閱讀
    <b class='flag-5'>Linux</b>下的靜態(tài)<b class='flag-5'>鏈接庫</b>和<b class='flag-5'>動(dòng)態(tài)</b><b class='flag-5'>鏈接庫</b>的區(qū)別是什么?
    主站蜘蛛池模板: 2021自产拍在线观看视频| 久久精品热在线观看85| 国产成人在线视频播放| 国产在线播放不卡| 久久两性视频| 日本二区三区欧美亚洲国| 小草视频免费观看在线| 中文字幕亚洲乱码熟女在线| 成年人视频在线免费看| 国产亚洲精品A久久777777| 乱亲女H秽乱长久久久| 色欲av蜜臀av高清| 岳的奶大又白又胖| 单亲妈妈3韩国电影免费观看| 精品久久久久中文字幕日本| 青柠在线观看免费全集| 亚洲视频中文字幕在线观看| 成人a视频片在线观看免费| 久久99热狠狠色一区二区| 热の中文 AV天堂| 一二三四在线观看韩国| 高H短篇辣肉纯肉| 看电影来5566一区.二区| 天天综合网网欲色| 97视频免费观看| 黑人开嫩苞| 色偷偷网址| 91福利在线观看| 国精产品一区一区三区有限公司 | av av在线| 黄网13区| 四虎永久在线精品免费A| 999久久狠狠免费精品| 韩剧19禁啪啪无遮挡大尺度| 日韩精品免费一区二区 | 视频成人永久免费下载| 99er热精品视频国产免费| 精品国产在线手机在线| 色婷婷国产精品视频一区二区| 69日本人xxxx16—18| 精品国产自在天天线2019|