搜尋
首頁系統教程LinuxLinux 動態連結與靜態連結原來是這麼回事?

Linux 動態連結與靜態連結原來是這麼回事?

Feb 05, 2024 pm 05:45 PM
linuxlinux教程linux系統延遲載入linux指令shell腳本嵌入式linux良許linux入門linux學習

老規矩,先提出幾個問題:

  • 為什麼要進行動態連結?
  • 如何進行動態連結?
  • 什麼是地址無關代碼技術?
  • 什麼是延遲綁定技術?
  • 如何在程式運行過程中進行明確連結?

為什麼要進行動態連結?

動態連結的出現是為了解決靜態連結的一些缺點:

  1. 節約記憶體與磁碟空間:如下圖所示,
Linux 动态链接与静态链接原来是这么回事?

#Program1和Program2分別包含Program1.o和Program2.o兩個模組,他們都需要Lib.o模組。靜態連結情況下,兩個目標檔案都用到Lib.o這個模組,所以它們同時在連結輸出的可執行檔Program1和program2中有副本,同時執行時,Lib.o在磁碟和記憶體中有兩份副本,當系統中有大量類似Lib.o的多個程式共享目標檔案時,就會浪費很大空間。

  1. 靜態連結對程式的更新部署和發布很不友善

假如一個模組依賴20個模組,當20個模組其中有一個模組需要更新時,需要將所有的模組找出來重新編譯出一個可執行程式才可以更新成功,每次更新任何一個模組,用戶就需要重新獲得一個非常大的程序,程序如果使用靜態鏈接,那麼通過網絡來更新程序也會非常不便,一旦程序任何位置有一個小改動,都會導致整個程序重新下載。

為了解決靜態鏈接的缺點,所以引入了動態鏈接,動態鏈接的內存分佈如圖,

Linux 动态链接与静态链接原来是这么回事?

多個程式依賴同一個共享目標文件,這個共享目標文件在磁碟和內存中僅有一份,不會產生副本,簡單來講就是不像靜態鏈接一樣對那些組成程序的目標文件進行鏈接,等到程式要運行時才進行鏈接,把鏈接這個過程推遲到運行時才執行。動態連結的方式使得開發過程中各個模組更加獨立,耦合度更小,便於不同的開發者和開發組織之間獨立的進行開發和測試。

如何進行動態連結?

看如下程式碼:

// 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中文網其他相關文章!

陳述
本文轉載於:良许Linux教程网。如有侵權,請聯絡admin@php.cn刪除
Linux軟件的未來:Flatpak和Snap會替換本機桌面應用程序嗎?Linux軟件的未來:Flatpak和Snap會替換本機桌面應用程序嗎?Apr 25, 2025 am 09:10 AM

多年來,Linux軟件分佈依賴於DEB和RPM等本地格式,並深深地根深蒂固。 但是,Flatpak和Snap已經出現,有望成為應用程序包裝的通用方法。 本文考試

Linux和Windows處理設備驅動程序的區別有什麼區別?Linux和Windows處理設備驅動程序的區別有什麼區別?Apr 25, 2025 am 12:13 AM

Linux和Windows在處理設備驅動程序上的差異主要體現在驅動管理的靈活性和開發環境上。 1.Linux採用模塊化設計,驅動可以動態加載和卸載,開發者需深入理解內核機制。 2.Windows依賴微軟生態,驅動需通過WDK開發並簽名認證,開發相對複雜但保證了系統的穩定性和安全性。

比較和對比Linux和Windows的安全模型。比較和對比Linux和Windows的安全模型。Apr 24, 2025 am 12:03 AM

Linux和Windows的安全模型各有優勢。 Linux提供靈活性和可定制性,通過用戶權限、文件系統權限和SELinux/AppArmor實現安全。 Windows則注重用戶友好性,依賴WindowsDefender、UAC、防火牆和BitLocker保障安全。

Linux和Windows之間的硬件兼容性有何不同?Linux和Windows之間的硬件兼容性有何不同?Apr 23, 2025 am 12:15 AM

Linux和Windows在硬件兼容性上不同:Windows有廣泛的驅動程序支持,Linux依賴社區和廠商。解決Linux兼容性問題可通過手動編譯驅動,如克隆RTL8188EU驅動倉庫、編譯和安裝;Windows用戶需管理驅動程序以優化性能。

Linux和Windows之間虛擬化支持有哪些差異?Linux和Windows之間虛擬化支持有哪些差異?Apr 22, 2025 pm 06:09 PM

Linux和Windows在虛擬化支持上的主要區別在於:1)Linux提供KVM和Xen,性能和靈活性突出,適合高定制環境;2)Windows通過Hyper-V支持虛擬化,界面友好,與Microsoft生態系統緊密集成,適合依賴Microsoft軟件的企業。

Linux系統管理員的主要任務是什麼?Linux系統管理員的主要任務是什麼?Apr 19, 2025 am 12:23 AM

Linux系統管理員的主要任務包括系統監控與性能調優、用戶管理、軟件包管理、安全管理與備份、故障排查與解決、性能優化與最佳實踐。 1.使用top、htop等工具監控系統性能,並進行調優。 2.通過useradd等命令管理用戶賬戶和權限。 3.利用apt、yum管理軟件包,確保系統更新和安全。 4.配置防火牆、監控日誌、進行數據備份以確保系統安全。 5.通過日誌分析和工具使用進行故障排查和解決。 6.優化內核參數和應用配置,遵循最佳實踐提升系統性能和穩定性。

很難學習Linux嗎?很難學習Linux嗎?Apr 18, 2025 am 12:23 AM

學習Linux並不難。 1.Linux是一個開源操作系統,基於Unix,廣泛應用於服務器、嵌入式系統和個人電腦。 2.理解文件系統和權限管理是關鍵,文件系統是層次化的,權限包括讀、寫和執行。 3.包管理系統如apt和dnf使得軟件管理方便。 4.進程管理通過ps和top命令實現。 5.從基本命令如mkdir、cd、touch和nano開始學習,再嘗試高級用法如shell腳本和文本處理。 6.常見錯誤如權限問題可以通過sudo和chmod解決。 7.性能優化建議包括使用htop監控資源、清理不必要文件和使用sy

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

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

熱工具

SublimeText3 Mac版

SublimeText3 Mac版

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

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器