Linux 動態連結與靜態連結原來是這麼回事?
老規矩,先提出幾個問題:
- 為什麼要進行動態連結?
- 如何進行動態連結?
- 什麼是地址無關代碼技術?
- 什麼是延遲綁定技術?
- 如何在程式運行過程中進行明確連結?
為什麼要進行動態連結?
動態連結的出現是為了解決靜態連結的一些缺點:
- 節約記憶體與磁碟空間:如下圖所示,

#Program1和Program2分別包含Program1.o和Program2.o兩個模組,他們都需要Lib.o模組。靜態連結情況下,兩個目標檔案都用到Lib.o這個模組,所以它們同時在連結輸出的可執行檔Program1和program2中有副本,同時執行時,Lib.o在磁碟和記憶體中有兩份副本,當系統中有大量類似Lib.o的多個程式共享目標檔案時,就會浪費很大空間。
- 靜態連結對程式的更新部署和發布很不友善
假如一個模組依賴20個模組,當20個模組其中有一個模組需要更新時,需要將所有的模組找出來重新編譯出一個可執行程式才可以更新成功,每次更新任何一個模組,用戶就需要重新獲得一個非常大的程序,程序如果使用靜態鏈接,那麼通過網絡來更新程序也會非常不便,一旦程序任何位置有一個小改動,都會導致整個程序重新下載。
為了解決靜態鏈接的缺點,所以引入了動態鏈接,動態鏈接的內存分佈如圖,

多個程式依賴同一個共享目標文件,這個共享目標文件在磁碟和內存中僅有一份,不會產生副本,簡單來講就是不像靜態鏈接一樣對那些組成程序的目標文件進行鏈接,等到程式要運行時才進行鏈接,把鏈接這個過程推遲到運行時才執行。動態連結的方式使得開發過程中各個模組更加獨立,耦合度更小,便於不同的開發者和開發組織之間獨立的進行開發和測試。
如何進行動態連結?
看如下程式碼:
// lib.c #include void func(int i) { printf("func %d \n", i); } // Program.c void func(int i); int main() { func(1); return 0; }
編譯運行過程如下:
$ gcc -fPIC -shared -o lib.so lib.c $ gcc -o test Program.c ./lib.so $ ./test $ func 1
透過-fPIC和-shared可以產生一個動態連結函式庫,再連結到可執行程式就可以正常運作。
透過readelf指令可以查看動態連結庫的segment資訊:
~/test$ readelf -l lib.so Elf file type is DYN (Shared object file) Entry point 0x530 There are 7 program headers, starting at offset 64 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x00000000000006e4 0x00000000000006e4 R E 0x200000 LOAD 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10 0x0000000000000218 0x0000000000000220 RW 0x200000 DYNAMIC 0x0000000000000e20 0x0000000000200e20 0x0000000000200e20 0x00000000000001c0 0x00000000000001c0 RW 0x8 NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8 0x0000000000000024 0x0000000000000024 R 0x4 GNU_EH_FRAME 0x0000000000000644 0x0000000000000644 0x0000000000000644 0x0000000000000024 0x0000000000000024 R 0x4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10 GNU_RELRO 0x0000000000000e10 0x0000000000200e10 0x0000000000200e10 0x00000000000001f0 0x00000000000001f0 R 0x1 Section to Segment mapping: Segment Sections... 00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 01 .init_array .fini_array .dynamic .got .got.plt .data .bss 02 .dynamic 03 .note.gnu.build-id 04 .eh_frame_hdr 05 06 .init_array .fini_array .dynamic .got
可以看見動態連結模組的裝載位址從0開始,0是無效位址,它的裝載位址會在程式執行時再確定,在編譯時是不確定的。
改一下程式:
#// Program.c #include void func(int i); int main() { func(1); sleep(-1); return 0; }
運行讀取maps資訊:
~/test$ ./test & [1] 126 ~/test$ func 1 cat /proc/126/maps 7ff2c59f0000-7ff2c5bd7000 r-xp 00000000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so 7ff2c5bd7000-7ff2c5be0000 ---p 001e7000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so 7ff2c5be0000-7ff2c5dd7000 ---p 000001f0 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so 7ff2c5dd7000-7ff2c5ddb000 r--p 001e7000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so 7ff2c5ddb000-7ff2c5ddd000 rw-p 001eb000 00:00 516391 /lib/x86_64-linux-gnu/libc-2.27.so 7ff2c5ddd000-7ff2c5de1000 rw-p 00000000 00:00 0 7ff2c5df0000-7ff2c5df1000 r-xp 00000000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so 7ff2c5df1000-7ff2c5df2000 ---p 00001000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so 7ff2c5df2000-7ff2c5ff0000 ---p 00000002 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so 7ff2c5ff0000-7ff2c5ff1000 r--p 00000000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so 7ff2c5ff1000-7ff2c5ff2000 rw-p 00001000 00:00 189022 /mnt/d/wzq/wzq/util/test/lib.so 7ff2c6000000-7ff2c6026000 r-xp 00000000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so 7ff2c6026000-7ff2c6027000 r-xp 00026000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so 7ff2c6227000-7ff2c6228000 r--p 00027000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so 7ff2c6228000-7ff2c6229000 rw-p 00028000 00:00 516353 /lib/x86_64-linux-gnu/ld-2.27.so 7ff2c6229000-7ff2c622a000 rw-p 00000000 00:00 0 7ff2c62e0000-7ff2c62e3000 rw-p 00000000 00:00 0 7ff2c62f0000-7ff2c62f2000 rw-p 00000000 00:00 0 7ff2c6400000-7ff2c6401000 r-xp 00000000 00:00 189023 /mnt/d/wzq/wzq/util/test/test 7ff2c6600000-7ff2c6601000 r--p 00000000 00:00 189023 /mnt/d/wzq/wzq/util/test/test 7ff2c6601000-7ff2c6602000 rw-p 00001000 00:00 189023 /mnt/d/wzq/wzq/util/test/test 7fffee96f000-7fffee990000 rw-p 00000000 00:00 0 [heap] 7ffff6417000-7ffff6c17000 rw-p 00000000 00:00 0 [stack] 7ffff729d000-7ffff729e000 r-xp 00000000 00:00 0 [vdso]
可以看到,整个进程虚拟地址空间中,多出了几个文件的映射,lib.so和test一样,它们都是被操作系统用同样的方法映射到进程的虚拟地址空间,只是它们占据的虚拟地址和长度不同.
从maps里可以看见里面还有libc-2.27.so,这是C语言运行库,还有一个ld-2.27.so,这是Linux下的动态链接器,动态链接器和普通共享对象一样被映射到进程的地址空间,在系统开始运行test前,会先把控制权交给动态链接器,动态链接器完成所有的动态链接工作后会把控制权交给test,然后执行test程序。
当链接器将Program.o链接成可执行文件时,这时候链接器必须确定目标文件中所引用的func函数的性质,如果是一个定义于其它静态目标文件中的函数,那么链接器将会按照静态链接的规则,将Program.o的func函数地址进行重定位,如果func是一个定义在某个动态链接共享对象中的函数,那么链接器将会将这个符号的引用标记为一个动态链接的符号,不对它进行地址重定位,将这个过程留在装载时再进行。
动态链接的方式
动态链接有两种方式:装载时重定位和地址无关代码技术。
装载时重定位:
在链接时对所有绝对地址的引用不作重定位,而把这一步推迟到装载时完成,也叫基址重置,每个指令和数据相当于模块装载地址是固定的,系统会分配足够大的空间给装载模块,当装载地址确定后,那指令和数据地址自然也就确定了。
然而动态链接模块被装载映射到虚拟空间,指令被重定位后对于每个进程来讲是不同的,没有办法做到同一份指令被多个进程共享,所以指令对不同的进程来说有不同的副本,还是空间浪费,怎么解决这个问题?使用fPIC方法。
地址无关代码:
指令部分无法在多个进程之间共享,不能节省内存,所以引入了地址无关代码的技术。我们平时编程过程中可能都见过-fPIC的编译选项,这个就代表使用了地址无关代码技术来实现真正的动态链接。
基本思想就是使用GOT(全局偏移表),这是一个指向变量或函数地址的指针数组,当指令要访问变量或者调用函数时,会去GOT中找到相应的地址进行间接跳转访问,每个变量或函数都对应一个地址,链接器在装载模块的时候会查找每个变量和函数的地址,然后填充GOT中的各个项,确保每个指针指向的地址正确。GOT放在数据段,所以它可以在模块装载时被修改,并且每个进程都可以有独立的副本,相互不受影响。
tips
-fpic和-fPIC的区别:它们都是地址无关代码技术,-fpic产生的代码相对较小较快,但是在某些平台会有些限制,所以大多数情况下都是用-fPIC来产生地址无关代码。
-fPIC和-fPIE的区别:一个作用于共享对象,一个作用于可执行文件,一个以地址无关方式编译的可执行文件被称作地址无关可执行文件。
-fpie和-fPIE的区别:类似于-fpic和-fPIC的区别
延迟绑定技术
在程序刚启动时动态链接器会寻找并装载所需要的共享对象,然后进行符号地址寻址重定位等工作,这些工作会减慢程序的启动速度,如果解决?
使用PLT延迟绑定技术,这里会单独有一个叫.PLT的段,ELF将 GOT拆分成两个表.GOT和.GOT.PLT,其中.GOT用来保存全局变量的引用地址,.GOT.PLT用来保存外部函数的地址,每个外部函数在PLT中都有一个对应项,在初始化时不会绑定,而是在函数第一次被用到时才进行绑定,将函数真实地址与对应表项进行绑定,之后就可以进行间接跳转。
显式运行时链接
支持动态链接的系统往往都支持显式运行时链接,也叫运行时加载,让程序自己在运行时控制加载的模块,在需要时加载需要的模块,在不需要时将其卸载。这种运行时加载方式使得程序的模块组织变得很灵活,可以用来实现一些诸如插件、驱动等功能。
通过这四个API可以进行显式运行时链接:
dlopen():打开动态链接库 dlsym():查找符号 dlerror():错误处理 dlclose():关闭动态链接库
参考这段使用代码:
#include #include int main() { void *handle; void (*f)(int); char *error; handle = dlopen("./lib.so", RTLD_NOW); if (handle == NULL) { printf("handle null \n"); return -1; } f = dlsym(handle, "func"); do { if ((error = dlerror()) != NULL) { printf("error\n"); break; } f(100); } while (0); dlclose(handle); return 0; }
编译运行:
$ gcc -o test program.c -ldl $ ./test func 100
总结
为什么要进行动态链接?
为了解决静态链接浪费空间和更新困难的缺点。
動態連結的方式?
裝載時重定位和位址無關程式碼技術。
位址無關程式碼技術原理?
透過GOT段實現間接跳躍。
延遲載入技術原理?
對外部函數符號透過PLT段實現延遲綁定及間接跳轉。
如果進行明確運行時連結?
透過
以上是Linux 動態連結與靜態連結原來是這麼回事?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

VS Code 系統要求:操作系統:Windows 10 及以上、macOS 10.12 及以上、Linux 發行版處理器:最低 1.6 GHz,推薦 2.0 GHz 及以上內存:最低 512 MB,推薦 4 GB 及以上存儲空間:最低 250 MB,推薦 1 GB 及以上其他要求:穩定網絡連接,Xorg/Wayland(Linux)

Linux系統的五個基本組件是:1.內核,2.系統庫,3.系統實用程序,4.圖形用戶界面,5.應用程序。內核管理硬件資源,系統庫提供預編譯函數,系統實用程序用於系統管理,GUI提供可視化交互,應用程序利用這些組件實現功能。

雖然 Notepad 無法直接運行 Java 代碼,但可以通過借助其他工具實現:使用命令行編譯器 (javac) 編譯代碼,生成字節碼文件 (filename.class)。使用 Java 解釋器 (java) 解釋字節碼,執行代碼並輸出結果。

VS Code擴展安裝失敗的原因可能包括:網絡不穩定、權限不足、系統兼容性問題、VS Code版本過舊、殺毒軟件或防火牆干擾。通過檢查網絡連接、權限、日誌文件、更新VS Code、禁用安全軟件以及重啟VS Code或計算機,可以逐步排查和解決問題。

要查看 Git 倉庫地址,請執行以下步驟:1. 打開命令行並導航到倉庫目錄;2. 運行 "git remote -v" 命令;3. 查看輸出中的倉庫名稱及其相應的地址。

VS Code 可以在 Mac 上使用。它具有強大的擴展功能、Git 集成、終端和調試器,同時還提供了豐富的設置選項。但是,對於特別大型項目或專業性較強的開發,VS Code 可能會有性能或功能限制。

Visual Studio Code (VSCode) 是一款跨平台、開源且免費的代碼編輯器,由微軟開發。它以輕量、可擴展性和對眾多編程語言的支持而著稱。要安裝 VSCode,請訪問官方網站下載並運行安裝程序。使用 VSCode 時,可以創建新項目、編輯代碼、調試代碼、導航項目、擴展 VSCode 和管理設置。 VSCode 適用於 Windows、macOS 和 Linux,支持多種編程語言,並通過 Marketplace 提供各種擴展。它的優勢包括輕量、可擴展性、廣泛的語言支持、豐富的功能和版

vscode 內置終端是一個開發工具,允許在編輯器內運行命令和腳本,以簡化開發流程。如何使用 vscode 終端:通過快捷鍵 (Ctrl/Cmd ) 打開終端。輸入命令或運行腳本。使用熱鍵 (如 Ctrl L 清除終端)。更改工作目錄 (如 cd 命令)。高級功能包括調試模式、代碼片段自動補全和交互式命令歷史。
