C 鏈接

鏈接器基礎:

編譯器一般由以下分程序組成:

  • 編譯驅動器(compiler driver):控制程序
  • 預處理器
  • 語法分析器
  • 語義分析器
  • 代碼生成器
  • 彙編器
  • 優化器
  • 鏈接器

編譯器創建一個輸出文件,包含了可重定地址的對象,這些對象是和源文件相對應的數據和機器指令

一個對象文件不是直接可執行的,需要首先被鏈接器處理。鏈接器找到main程序作爲入口,將符號綁定到內存地址,合併所有的對象文件,然後把它們和庫文件聯合在一起生成可執行文件

動態鏈接和靜態鏈接:

PC和更大型的系統相比在鏈接機制上有一個很大的區別。PC只提供很少的被稱爲BIOS例程的基礎I/O服務,並放到固定的存儲地址。而且不屬於任何一個可執行文件的一部分。如果PC程序想要更多的複雜服務,這些服務可以作爲庫提供,而且鏈接器需要把這些庫鏈接到需要的可執行文件上

UNIX系統以前也是這樣,當需要鏈接一個程序的時候,這個庫例程的一個副本就會嵌入到可執行文件中

一個新的更高級的方式被稱爲動態鏈接庫:動態鏈接允許系統提供大量的庫文件提供服務,而且程序不必將這些庫的二進制文件作爲可執行文件的一部分,而是在運行期間尋找這些服務

區別:

  • 靜態鏈接:庫文件的副本被作爲可執行文件的一部分
  • 動態鏈接:可執行文件只有文件名,在運行時加載器使用這個文件名尋找庫文件

將各模塊整合到一起併爲執行做準備有三個過程:鏈接編輯(link-editing)、加載(loading)、運行時鏈接(runtime linking)

  • 靜態鏈接的過程是:鏈接編輯->加載並執行
  • 動態鏈接的過程是:鏈接編輯->加載->動態鏈接並執行

動態鏈接模式在執行的時候,運行時加載器會在main函數調用之前將對應的數據對象放到程序的地址空間中
只有在函數真正被調用的時候才解析外部函數,所以如果不調用外部函數,動態鏈接不會造成額外的消耗

動態鏈接的優點:代價是很小的運行時損耗,因爲一些鏈接器的工作被推遲到運行時執行

  1. 因爲庫只會在需要的時候才被加載到程序中,動態鏈接生成的可執行文件要小得多,節省了磁盤空間和虛擬內存。靜態鏈接如果想要避免將庫文件副本嵌入到可執行文件中就只能把服務放到內核中,這會導致內核膨脹
  2. 所有鏈接到相同庫的可執行文件將在運行時共享一份庫文件的副本。系統內核保證映射到庫文件中的庫可以被所有程序訪問到。這會帶來更好的I/O和交換空間利用率,並且節約物理存儲,提高系統整體吞吐率。如果是靜態鏈接,每個可執行程序都將有一份庫的副本
  3. 動態鏈接提供更好的版本控制,新的庫版本安裝之後,程序就會自動的直接尋找到新的庫,而不必重新執行鏈接過程
  4. 有一個並不經常用到的好處就是,動態鏈接允許用戶選擇運行時執行那個庫文件。可以選擇不同的庫文件版本,比如速度更快的、更節約存儲空間的或者包含了更多的debug信息

動態鏈接庫的問題:
因爲程序是在運行時尋找庫文件的,所以要指定文件名和文件路徑。之後這個庫文件的位置就不能改變了。而且如果在不同的機器上執行,必須有相同的文件路徑,但是如果是系統標準庫文件就不會有這個問題

動態鏈接的主要目的是充分利用ABI的好處,使軟件不必隨着每一次系統更新或庫文件更新而重新編譯

ABI:
我們有這樣的約定:系統爲程序提供接口,這個接口隨着操作系統版本的發佈也是穩定的。程序可以調用接口承諾的服務而不需要考慮他們是怎麼實現的或者底層實現可能會改變。因爲這個程序和服務之間的接口是通過可執行二進制庫文件提供的,所以被稱爲應用程序二進制接口(Application Binary Interface ABI)

以前,應用程序供應商在每次庫文件或操作系統更新的時候都需要重新鏈接他們的軟件。ABI不必這樣,它保證了軟件不會被底層系統軟件的更新影響到

靜態鏈接是過時的方式。靜態鏈接的主要風險是新版本的操作系統可能和綁定到可執行文件的系統庫文件不兼容

命令:

  1. 創建動態鏈接庫的命令:cc -o libxxx.so -G xxx.c
  2. 使用的命令:cc test.c -L/filepath1 -R/filepath2 -lxxx
    1. -L/filepath1作用是告訴鏈接器在鏈接期間去哪裏找庫文件
    2. -R/filepath2作用是告訴鏈接器在運行期間去哪裏找庫文件

-K pic編譯選項:
作用是爲庫產生位置獨立的代碼

位置獨立意味着所有的全局數據讀寫都是通過額外的一次間接引用,這樣在想要爲數據換位置的時候就只需要改變全局偏移表中的一個值

類似的,所有的函數調用都是通過間接引用程序鏈接表中的地址實現的,這段文本就可以通過改變偏移表輕易的遷移。所以當代碼在運行時被映射的時候,運行時鏈接器就可以把這段代碼放到任意有空間的位置,而這段代碼本身不必改變

默認情況下,編譯器不會執行PICode選項,因爲額外的指針解引用會導致運行時速度稍微變慢。但是如果不使用PICode,生成的代碼會綁定到固定的地址,這對可執行程序而言是無所謂的,但是對於一個共享的庫效率會降低。因爲運行時每次全局的引用都需要通過頁修改來恢復,也就導致這個頁不可共享。雖然運行時鏈接器總會恢復頁引用,但是如果是位置獨立的代碼這項工作會簡單很多

經驗法則是總是使用PICode

位置獨立的代碼對於共享的庫文件是非常有用的,因爲每次獲取都是把它映射到虛擬地址中(但是保持相同的物理地址)

一個相關的術語是純代碼,純代碼意味着可執行文件中只含有代碼而沒有靜態的或初始化數據。所以在被任意程序執行的時候都不需要被修改就可以執行,它從棧或者其他的代碼段取數據。純代碼段是可以共享的。如果使用PICode,使用純代碼是最好的

關於鏈接庫文件的五點:

  1. 動態鏈接庫的命名:libname.so;靜態鏈接庫的命名:libname.a,name是文件名
  2. 通過-lname選項告知編譯器鏈接到哪個文件。如庫文件名是 libthread.so 那麼編譯選項就是 -lthread——去掉"lib"並加上"l"
  3. 編譯器會到特定的目錄下去尋找庫文件
    1. 編譯命令 -Lpathname 會告訴編譯器去哪個目錄下尋找庫文件
    2. 環境變量LD_LIBRARY_PATH和LD_RUN_PATH也被用來提供這個信息
    3. 但是使用環境變量被官方認爲是不好的,因爲安全性、性能和編譯/執行獨立性等問題
  4. 通過包含的頭文件確定使用了那些庫
    1. 每個頭文件代表了一個必須包含的庫文件。
    2. 問題:
      1. 頭文件名和庫文件名通常不一致
      2. 一個庫文件可能包含了可以滿足多個頭文件原型聲明的例程。比如,<string.h><stdio.h><time.h>都由libc.so提供
    3. 如何將一個符號匹配到庫文件:如果程序有一個符號未定義的錯誤,到庫文件目錄下執行:
      1. % foreach i (lib?*)  
        ? echo $i  
        ? nm $i | grep symble | grep -v UNDEF  
        ? end

        找到缺失的庫文件,並在編譯時加上對它的動態鏈接 

  5. 從靜態鏈接庫中提取符號將比從動態鏈接庫中收到更多的限制
    1. 靜態鏈接和動態鏈接在語義上有一個很大的區別:
      1. 動態鏈接,所有庫的符號都會加載到輸出文件的虛擬地址空間中,在鏈接過程中所有的符號對所有其他文件都是可見的
      2. 靜態鏈接只會在靜態庫文件加載的時候在裏面尋找加載器目前的未定義符號
    2. 也就是說,由於靜態庫在編譯命令中是從左到右解析的,所以編譯命令中的順序會造成很大的不同。如果相同的符號在不同的文件中有不同的定義,那麼編譯命令中這兩個文件的不同相對順序就會產生不同的結果。如果編譯命令中自己的代碼是在靜態庫文件之後,那麼在解析靜態庫文件的時候就不會有任何未定義的符號,就不會解析任何東西
    3. 而math庫,由於要儘可能的提高效率,被設計爲靜態庫,如果採用這樣的語句:cc -lm main.c,就會產生錯誤,應使用:cc main.c -lm

代碼植入(Interpositioning):

指通過使用相同的函數名,用戶用自己定義的函數代替庫函數。問題是,不僅用戶的代碼會調用用戶實現的版本,系統例程也會調用用戶自己實現的版本,而且編譯器不會對重新定義庫函數產生任何錯誤

舉例:
用戶實現:
main(){
    mktemp();
    getwd();
}
C庫:
mktemp(){...}
getwd(){...mktemp()...}

如果用戶實現了自己的mktemp()定義,那麼用戶代碼中的main()和C庫中的geted()都會調用用戶實現的版本

出現過的一個BUG就是C庫的mktemp()需要一個參數,而用戶實現的版本需要三個。但是C庫中的getwd()調用的時候是按照C庫的版本調用的,只會傳遞一個參數。但是用戶版本會嘗試進行三個參數的解析,這會根據棧中的值產生out of memory錯誤

 

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