程式的連結和裝入及Linux下動態連結的實現

jackie_gnu發表於2011-11-09

 https://www.ibm.com/developerworks/cn/linux/l-dynlink/

簡介: 程式的連結和裝入存在著多種方法,而如今最為流行的當屬動態連結、動態裝入方法。本文首先回顧了連結器和裝入器的基本工作原理及這一技術的發展歷史,然後通過實際的例子剖析了Linux系統下動態連結的實現。瞭解底層關鍵技術的實現細節對系統分析和設計人員無疑是必須的,尤其當我們在面對實時系統,需要對程式執行時的時空效率有著精確的度量和把握時,這種知識更顯重要。

 

連結器和裝入器的基本工作原理

一個程式要想在記憶體中執行,除了編譯之外還要經過連結和裝入這兩個步驟。從程式設計師的角度來看,引入這兩個步驟帶來的好處就是可以直接在程式中使用printf和errno這種有意義的函式名和變數名,而不用明確指明printf和errno在標準C庫中的地址。當然,為了將程式設計師從早期直接使用地址程式設計的夢魘中解救出來,編譯器和彙編器在這當中做出了革命性的貢獻。編譯器和彙編器的出現使得程式設計師可以在程式中使用更具意義的符號來為函式和變數命名,這樣使得程式在正確性和可讀性等方面都得到了極大的提高。但是隨著C語言這種支援分別編譯的程式設計語言的流行,一個完整的程式往往被分割為若干個獨立的部分並行開發,而各個模組間通過函式介面或全域性變數進行通訊。這就帶來了一個問題,編譯器只能在一個模組內部完成符號名到地址的轉換工作,不同模組間的符號解析由誰來做呢?比如前面所舉的例子,呼叫printf的使用者程式和實現了printf的標準C庫顯然就是兩個不同的模組。實際上,這個工作是由連結器來完成的。

為了解決不同模組間的連結問題,連結器主要有兩個工作要做――符號解析和重定位:

符號解析:當一個模組使用了在該模組中沒有定義過的函式或全域性變數時,編譯器生成的符號表會標記出所有這樣的函式或全域性變數,而連結器的責任就是要到別的模組中去查詢它們的定義,如果沒有找到合適的定義或者找到的合適的定義不唯一,符號解析都無法正常完成。

重定位:編譯器在編譯生成目標檔案時,通常都使用從零開始的相對地址。然而,在連結過程中,連結器將從一個指定的地址開始,根據輸入的目標檔案的順序以段為單位將它們一個接一個的拼裝起來。除了目標檔案的拼裝之外,在重定位的過程中還完成了兩個任務:一是生成最終的符號表;二是對程式碼段中的某些位置進行修改,所有需要修改的位置都由編譯器生成的重定位表指出。

 

可執行程式生成後,下一步就是將其裝入記憶體執行。Linux下的編譯器(C語言)是cc1,彙編器是as,連結器是ld,但是並沒有一個實際的程式對應裝入器這個概念。實際上,將可執行程式裝入記憶體執行的功能是由execve(2)這一系統呼叫實現的。簡單來講,程式的裝入主要包含以下幾個步驟:

  • 讀入可執行檔案的頭部資訊以確定其檔案格式及地址空間的大小;
  • 以段的形式劃分地址空間;
  • 將可執行程式讀入地址空間中的各個段,建立虛實地址間的對映關係;
  • 將bbs段清零;
  • 建立堆疊段;
  • 建立程式引數、環境變數等程式執行過程中所需的資訊;
  • 啟動執行。

 

相關文章