3 動态連結的解決方案
PLT和GOT
要實作動态連結共享庫,也并不困難,和前面的靜态連結裡的符号表和重定向表類似
拿出一小段代碼來看一看。
lib.h
定義了動态連結庫的一個函數 show_me_the_money
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SZ4YmMjVjZycjMygzYhNjY5UWZ0ETMyMmMmFmZhVGO08CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
lib.c
包含了lib.h的實際實作
show_me_poor.c
調用了 lib 裡面的函數
把 lib.c 編譯成了一個動态連結庫,也就是 .so 檔案
最終生成檔案集
在編譯的過程中,指定了一個 -fPIC 的參數
其實就是Position Independent Code意,也就是要把這個編譯成一個位址無關代碼
然後,我們再通過gcc編譯 show_me_poor 動态連結了 lib.so 的可執行檔案
在這些操作都完成了之後,我們把 show_me_poor 這個檔案通過objdump出來看一下。
0000000000400540 <show_me_the_money@plt-0x10>:
400540: ff 35 12 05 20 00 push QWORD PTR [rip+0x200512] # 600a58 <_GLOBAL_OFFSET_TABLE_+0x8>
400546: ff 25 14 05 20 00 jmp QWORD PTR [rip+0x200514] # 600a60 <_GLOBAL_OFFSET_TABLE_+0x10>
40054c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0000000000400550 <show_me_the_money@plt>:
400550: ff 25 12 05 20 00 jmp QWORD PTR [rip+0x200512] # 600a68 <_GLOBAL_OFFSET_TABLE_+0x18>
400556: 68 00 00 00 00 push 0x0
40055b: e9 e0 ff ff ff jmp 400540 <_init+0x28>
……
0000000000400676 <main>:
400676: 55 push rbp
400677: 48 89 e5 mov rbp,rsp
40067a: 48 83 ec 10 sub rsp,0x10
40067e: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5
400685: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
400688: 89 c7 mov edi,eax
40068a: e8 c1 fe ff ff call 400550 <show_me_the_money@plt>
40068f: c9 leave
400690: c3 ret
400691: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
400698: 00 00 00
40069b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
我們還是隻關心整個可執行檔案中的一小部分内容
- 在main函數調用show_me_the_money的函數的時候,對應的代碼是這樣的:
這裡後面有一個@plt的關鍵字,代表了我們需要從PLT,也就是程式連結表(Procedure Link Table)裡面找要調用的函數。對應的位址呢,則是400580這個位址。
那當我們把目光挪到上面的 400580 這個位址,你又會看到裡面進行了一次跳轉,
這個跳轉指定的跳轉位址,你可以在後面的注釋裡面可以看到:
這裡的 GLOBAL_OFFSET_TABLE,就是我接下來要說的全局偏移表。
在動态連結對應的共享庫,我們在共享庫的data section裡面,儲存了一張全局偏移表(GOT,Global Offset Table)
雖然共享庫的代碼部分的實體記憶體是共享的,但是資料部分是各個動态連結它的應用程式裡面各加載一份的。
所有需要引用目前共享庫外部的位址的指令,都會查詢GOT,來找到目前運作程式的虛拟記憶體裡的對應位置
而GOT表裡的資料,則是在我們加載一個個共享庫的時候寫進去的。
不同的程序,調用同樣的 lib.so,各自GOT裡面指向最終加載的動态連結庫裡面的虛拟記憶體位址是不同的。
這樣,雖然不同的程式調用的同樣的動态庫,各自的記憶體位址是獨立的,調用的又都是同一個動态庫,但是不需要去修改動态庫裡面的代碼所使用的位址,
而是各個程式各自維護好自己的GOT,能夠找到對應的動态庫就好了
GOT表位于共享庫自己的資料段裡
GOT表在記憶體裡和對應的代碼段位置之間的偏移量,始終是确定的
這樣,共享庫就是位址無關的代碼,對應的各個程式隻需在實體記憶體裡加載同一份代碼
而我們又要通過各個可執行程式在加載時,生成的各不相同的GOT表,找到它需要調用到的外部變量和函數的位址
這是一個典型的、不修改代碼,而是通過修改“位址資料”來進行關聯的辦法
它有點像我們在C語言裡面用函數指針來調用對應的函數,并不是通過預先已經确定好的函數名稱來調用,而是利用當時它在記憶體裡面的動态位址來調用。
4 總結
終于在靜态連結和程式裝載後,利用動态連結把我們的記憶體利用到了極緻
同樣功能的代碼生成的共享庫,我們隻要在記憶體裡面保留一份就好了
這樣
- 不僅能夠做到代碼在開發階段的複用
- 也能做到代碼在運作階段的複用。
實際上,在進行Linux程式開發,一直會用到各種各樣的動态連結庫。
C語言的标準庫就在1MB以上。
撰寫任何一個程式可能都需要用到這個庫,常見的Linux伺服器裡,/usr/bin下面就有上千個可執行檔案。
如果每一個都把标準庫靜态連結進來的,幾GB乃至幾十GB的磁盤空間一下子就用出去了。如果我們服務端的多程序應用要開上千個程序,幾GB的記憶體空間也會一下子就用出去了。這個問題在過去計算機的記憶體較少的時候更加顯著。
通過動态連結這個方式,可以說徹底解決了這個問題。
就像共享單車一樣,如果仔細經營,是一個很有社會價值的事情,但是如果粗暴地把它變成無限制地複制生産,給每個人造一輛,隻會在系統内制造大量無用的垃圾。
已經把程式怎麼從源代碼變成指令、資料,并裝載到記憶體裡面,由CPU一條條執行下去的過程講完了。希望你能有所收獲,對于一個程式是怎麼跑起來的,有了一個初步的認識。
5 推薦閱讀
想要更加深入地了解動态連結,推薦你可以讀一讀
《程式員的自我修養:連結、裝載和庫》的第7章裡面深入地講解了,動态連結裡程式内的資料布局和對應資料的加載關系。
參考
- 深入淺出計算機組成原理