在线观看www成人影院-在线观看www日本免费网站-在线观看www视频-在线观看操-欧美18在线-欧美1级

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

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

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

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

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

本文將深入探討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 :
 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)是在干什么?用一個示意圖表示 bar 首次地址重定位過程(橙色是調(diào)用入口,藍色是運行的指令,紫色代表修正的地址)。

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

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

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

使用 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>  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 ,和上圖的相對地址偏移相同,重定向后結(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 入口地址,也對應(yīng)起來了。

抽象一下,如下示意圖:

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

通過上圖指令跳轉(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*/
#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() 的實現(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
                  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 變量的訪問直接通過偏移量完成,這種方式更高效,因為 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)會有什么變化?


#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 直接可以通過相對地址跳轉(zhuǎ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)。隱藏的符號 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 status
nopic_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

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

.dynsym
RA
僅包含程序運行中需要動態(tài)鏈接的符號,若GCC中通過__attribute__((visibility("hidden")))隱藏的符號,在這里不會出現(xiàn)。 readelf-S xxx/objdump-h XXX #查看 section 地址分布

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


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

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

.plt
RA
一組跳板函數(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中保存的是動態(tài)鏈接器用到的基本信息,如動態(tài)鏈接符號表(.dynsym),字符串表(.dynstr),重定位表
(.rela.dyn/rela.plt),依賴的運行時庫,庫查找路徑等
readelf-dxxx # 查看.dynmaic段地址

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

.got 和.got.plt
RWA
存儲重定位指針的地方 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
用于存儲初始化的全局變量和靜態(tài)變量 readelf-S xxx/objdump-h XXX #查看 section 地址分布

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

.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 # 查看所有符號

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


'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

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

收藏 人收藏

    評論

    相關(guān)推薦

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    學(xué)習(xí)Linux動態(tài)鏈接庫是一個繞不開的話題,我們今天就一起來看一下什么是動態(tài)鏈接庫動態(tài)
    的頭像 發(fā)表于 02-17 10:49 ?1282次閱讀
    <b class='flag-5'>Linux</b>下的靜態(tài)<b class='flag-5'>鏈接庫</b>和<b class='flag-5'>動態(tài)</b><b class='flag-5'>鏈接庫</b>的區(qū)別是什么?
    主站蜘蛛池模板: 一级片在线视频| 午夜在线影视| 欧美日韩影院| 欧美亚洲网站| 日本x色视频| 天天爽天天爽天天片a久久网| 岛国大片在线播放| 网红和老师啪啪对白清晰| 黄色在线播放网站| 婷婷五月情| 在线视频精品免费| 国产免费啪啪| 色爱区综合激情五月综合色| 影音先锋色偷偷米奇四色| 中文字幕一区二区三区有限公司 | 国产三级在线看| 国产美女影院| 男男np主受高h啪啪肉| 香港澳门a级三级三级全黄| 国产精品一区二区三区免费视频| 激情啪啪网站| 天天谢天天干| 激情五月社区| 欧美一级艳片视频免费观看| 国产美女免费| 国产视频h| 欧美freesex10一|3| 夜夜操夜夜操| 日本不卡1| 精品乩伦视频| 欧美亚洲综合另类型色妞| 色黄污在线看黄污免费看黄污| 午夜999| 在线亚洲国产精品区| www.成人在线| 色吧色吧色吧网| 国产一二三区精品| 38pao强力打造永久免费高清视频| 欧美xxxxbbbb| 四虎884tt紧急大通知| 在线欧美色图|