鏈接-動態鏈接

共享庫

共享庫的動機是什麼 ,我們從前面的靜態鏈接的時候學習到了靜態鏈接庫 ,可以知道靜態鏈接庫的缺點如下 :

img

img

這裏有個問題 ,就是每個進程都擁有虛擬空間地址 ,然後共享庫又只會有一個 ,那麼共享庫如何做到給各個進程共享呢? 這個問題我們放在了其他這一個章節

img

動態鏈接和靜態鏈接的一個區別

自定義一個共享庫

img

可以看到我們我們使用了 gcc 的命令生成了位置無關的代碼 ,簡稱 PIC .

動態鏈接的方式

1.加載時動態鏈接

img

img

當發現有一個 .interp 的段(section)的時候就會觸發動態連接. 我們這裏並沒有深入動態鏈接器(ld-linux.so) 的工作原理 ,不過基本也可以纔到它的主要工作就是加載共享庫到內存中去並且映射到各個進程的內存映射區域去 ,然後後面的工作就有點像靜態鏈接一樣, 修改調用的地址了.

2.運行時動態鏈接

img

動態鏈接的的另外一種方式這是通過代碼調用, 再加載到程序中去

PIC

img

從上面的講義中, 我們可以知道引入PIC的目的是 鏈接器無需修改代碼即可將共享庫加載到任意地址運行

PIC 的實現

實現位置無關代碼(PIC)所依賴的理論依據其中之一是:在鏈接階段,鏈接器就已經知道代碼段和數據段之間的偏移。 當鏈接器將若干個目標文件鏈接在一起時,它會將相似段合併(例如,將所有的代碼段合併成一個大的段,段的名稱依然叫代碼段)。 所以,鏈接器是知道每個段的大小以及段與段之間的相對位置的。

來看一個例子,假設代碼段之後緊接着就是數據段,那麼代碼段中的任何一條指令與數據段的開始之間的偏移就是代碼段的大小減去指令到代碼段開始的偏移 —— 當然,這兩個值鏈接器都是知道的。

img

在上圖中,可以看到代碼段的加載地址(這個地址在鏈接階段是不知道的)是0xXXXX0000(X代表任意值),而且數據段緊跟其後,加載地址爲0xXXXXF000。 如果代碼段內偏移0x80處的指令需要訪問數據段中的數據,那麼鏈接器就會知道指令與所需訪問數據之間的相對偏移(在這裏相對偏移是0xEF80),並且將這個相對偏移硬編碼於指令中。

上面的理論依據只有在我們需要相對偏移時纔有用,可是在x86架構上的數據訪問卻需要數據的絕對地址(例如mov指令),那我們怎麼做呢?

如果已知相對地址,然後需要其絕對地址,那麼我們還需要知道的就是指令指針(instruction pointer)的值(因爲依據定義,相對地址是相對於指令位置的地址)。 遺憾的是X86架構沒有直接獲得指令指針的值的指令,不過我們可以利用一個小技巧來獲得,如下面的彙編僞代碼所示:

call tmep 
tmep: 
pop ebx 

解釋如下:

1.CPU執行了call TMPLABEL,所以會將下一條指令(就是pop ebx)的地址壓入棧頂,然後跳到TMPLABEL標籤處。

2.因爲在標籤TMPLABEL處的指令是pop ebx,所以接下來就執行它,它會從棧頂取出一個值存入寄存器ebx。 這樣就可以知道這個值就是這條指令本身的地址,所以此時ebx實際上就包含了指令指針的值。

理解了上面所說的,我們終於可以開始看看位置無關代碼(PIC)是如何在X86架構上實現的了。主要是利用全局偏移表(global offset table)來實現的,全局偏移表簡稱GOT。

img

可以看到GOT 放在了 Data 節的開頭處 , 一個GOT就是一個簡單的指針數組,位於數據段中。 假設代碼段中有一些指令需要訪問數據,那麼它們不會使用絕對地址(因爲這需要重定位操作),而是會引用GOT中的一個項。 因爲GOT位於數據段中,所以鏈接器知道對GOT中項的引用是使用的相對地址。GOT中的項實際就是變量的絕對地址 .

我們可以看到饒了一大圈 ,我們還是的最終解析絕對地址 ,只是說中間多了一層 GOT , 那麼使用 GOT 的好處是什麼呢?

1.如果是代碼段的重定位,那麼鏈接器會爲代碼中每一次的變量引用執行重定位操作,而如果使用GOT的話,只需爲每一個變量執行一次重定位操作。因爲程序中極有可能會對一個變量引用多次,那麼只執行一次重定位操作,勢必會在程序啓動階段節約大量的時間。

2.因爲數據段是可寫的,並且在進程間是不共享的,所以在數據段執行重定位操作並沒有什麼傷害。再者,將重定位操作從代碼段移至數據段,就可以將代碼段設置成可讀的,並且可以在多個進程間共享。

PIC-延遲加載

延遲加載 ,就和我們學 spring bean的時候的懶加載 ,等到需要用到的時候我再加載這個 bean , 之前沒有用到我就先不進行加載 . 具體的做法是用到了一個叫PLT 的結構

延遲加載過程

過程如下 :

img

左圖 展示了 GOT 和 PLT 如何協同工作,在 addvec 被第一次調用時,延遲解析它的運行時地址:

  • 第 1 步。不直接調用 addvec,程序調用進入 PLT[2],這是 addvec 的 PLT 條目。
  • 第 2 步。第一條 PLT 指令通過 GOT[4] 進行間接跳轉。因爲每個 GOT 條目初始時都指向它對應的 PLT 條目的第二條指令,這個間接跳轉只是簡單地把控制傳送回 PLT[2] 中的下一條指令。
  • 第 3 步。在把 addvec 的 ID(0x1)壓入棧中之後,PLT[2] 跳轉到 PLT[0]。
  • 第 4 步。PLT[0] 通過 GOT[1] 間接地把動態鏈接器的一個參數壓入棧中,然後通過 GOT[2] 間接跳轉進動態鏈接器中。動態鏈接器使用兩個棧條目來確定 addvec 的運行時位置,用這個地址重寫 GOT[4],再把控制傳遞給 addvec。

右圖 給出的是後續再調用 addvec 時的控制流:

  • 第 1 步。和前面一樣,控制傳遞到 PLT[2]。
  • 第 2 步。不過這次通過 GOT[4] 的間接跳轉會將控制直接轉移到 addvec。

補充

講義中提到的
img

PIC 的引用的四種情況實際上就是上面我們講到過程 , 只是講義把成4種情況來講.

img

img

img

img

img

思想都是一樣了, 利用了 在鏈接階段,鏈接器就已經知道代碼段和數據段之間的偏移。 當鏈接器將若干個目標文件鏈接在一起時,它會將相似段合併(例如,將所有的代碼段合併成一個大的段,段的名稱依然叫代碼段)。 所以,鏈接器是知道每個段的大小以及段與段之間的相對位置的。

其他

共享庫是如何加載到各個進程的虛擬空間的呢?
虛擬空間地址中有一塊區域是專門存放內存映射的 :

img

內存映射段(mmap)
內核將硬盤文件的內容直接映射到內存, 任何應用程序都可通過Linux的mmap()系統調用或Windows的CreateFileMapping()/MapViewOfFile()請求這種映射。內存映射是一種方便高效的文件I/O方式, 因而被用於裝載動態共享庫。用戶也可創建匿名內存映射,該映射沒有對應的文件, 可用於存放程序數據。在 Linux中,若通過malloc()請求一大塊內存,C運行庫將創建一個匿名內存映射,而不使用堆內存。”大塊” 意味着比閾值 MMAP_THRESHOLD還大,缺省爲128KB,可通過mallopt()調整。

   該區域用於映射可執行文件用到的動態鏈接庫。在Linux 2.4版本中,若可執行文件依賴共享庫,則系統會爲這些動態庫在從0x40000000開始的地址分配相應空間,並在程序裝載時將其載入到該空間。在Linux 2.6內核中,共享庫的起始地址被往上移動至更靠近棧區的位置。

   從進程地址空間的佈局可以看到,在有共享庫的情況下,留給堆的可用空間還有兩處:一處是從.bss段到0x40000000,約不到1GB的空間;另一處是從共享庫到棧之間的空間,約不到2GB。這兩塊空間大小取決於棧、共享庫的大小和數量。這樣來看,是否應用程序可申請的最大堆空間只有2GB?事實上,這與Linux內核版本有關。在上面給出的進程地址空間經典佈局圖中,共享庫的裝載地址爲0x40000000,這實際上是Linux kernel 2.6版本之前的情況了,在2.6版本里,共享庫的裝載地址已經被挪到靠近棧的位置,即位於0xBFxxxxxx附近,因此,此時的堆範圍就不會被共享庫分割成2個“碎片”,故kernel 2.6的32位Linux系統中,malloc申請的最大內存理論值在2.9GB左右。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章