【深入理解計算機系統筆記】Linux 下 程序的鏈接過程

我們第一個hello程序的生命週期是從一個人工編輯的文本文件hello.c 開始的,然後經過翻譯形成一個可執行文件,這之後通過加載器將可執行目標文件的代碼和數據從磁盤複製到內存中,然後通過跳轉到程序的第一條指令或入口點來運行該程序。
其中翻譯過程又可以分爲預處理,編譯,彙編,鏈接這四個步驟。如下圖所示:
這裏寫圖片描述
預處理階段根據以#字符開頭來讀取系統頭文件,把它直接插入到程序文本中,得到 hello.i 文本文件,文本文件是用ASCII編碼的。
編譯階段將文本文件hello.i 翻譯成文本文件hello.s ,它包含一個彙編語言的程序。
彙編階段將hello.s翻譯成機器語言指令。把這些指令打包成一個可重定位的目標程序hello.o,它是一個二進制文件。
鏈接階段。閱讀了第七章,主要對鏈接階段詳細說明。

鏈接階段

鏈接是將各種代碼和數據片段收集並組合成爲一個單一文件的過程,這個形成的文件可以被加載到內存中運行。鏈接可分爲編譯時鏈接,加載時鏈接,運行時鏈接。前兩種是靜態鏈接,後兩種是動態鏈接。我個人對鏈接的理解就是將各種代碼和數據“分類整理”,確定無誤後分配運行時地址,合併成一個文件。
靜態鏈接有兩個任務:符號解析和重定位。

符號解析

目標文件中有定義的符號,有引用的符號,每一個符號可能是一個函數,一個全局變量,或者是一個static聲明的靜態變量。符號解析的目的就是將每個符號的引用正好和一個符號的定義關聯起來。
先來看看可重定位目標文件的組成: 可重定位目標文件由ELF 頭節頭部表以及他們中間的節構成,這些節中有:

  • .text : 已經編譯程序的機器代碼;
  • .rodata: 只讀數據,比如printf中的格式串和開關語句的跳轉表;
  • . data: 已初始化的全局和靜態變量。局部變量是在運行時被保存在棧中。不在可重定位目標文件中出現;
  • . bss: 未初始化的全局和靜態變量,以及所有被初始化未0 的全局和靜態變量。這個節不佔據實際的空間,只是一個佔位符。劃分已初始化和未初始化的變量是爲了空間效率:沒有初始化的變量就沒有必要佔據任何實際的磁盤空間。運行時,在內存中分配這些變量,初始值爲0
  • . symtab: 一個符號表,存放在程序中定義和引用的函數和全局變量信息。每個可重定位目標文件在 .symtab 中都有一個符號表。和編譯器的符號表不同, .symtab 符號不包括局部變量的條目。
    有三種不同的符號:
    1)由本模塊定義的並能夠被其他模塊引用的全局符號——非靜態函數和全局變量
    2)由其他模塊定義並能被本模塊引用的全局符號,稱爲外部符號——對應於其他模塊定義的非靜態函數和全局變量
    3) 只能被本模塊定義和引用的局部符號——帶static屬性的函數和全局C變量。
    編譯器只允許每個模塊中每個局部符號有一個定義,靜態局部變量也會有本地鏈接器符號,編譯器還要保證他們有唯一的名字。對於不是在昂前模塊中定義的符號,會假設該符號是在其他某個模塊中定義的,生成一個鏈接器符號條目,並把它交給鏈接器處理。當鏈接器在其他輸入模塊中都找不到這個被引用的符號的定義,就輸出一條錯誤信息終止。
    還有其他的節,不做介紹。

重定位

一旦鏈接器完成了符號解析,就把代碼中每個符號引用和正好一個符號的定義關聯起來。就開始了重定位步驟,在這個步驟中,將合併輸入模塊,併爲每個符號分配運行時地址。
重定位由兩部分構成:重定位節和符號定義;重定位節中的符號引用。

靜態鏈接和動態鏈接

首先要了解靜態庫和動態庫:

  • 靜態庫
    一般爲 .a文件,作爲鏈接器的輸入。比如printf函數需要引用一個目標模塊,通常將這些模塊提前打包,鏈接器就把這個目標模塊複製進程序中。利用靜態函數庫編譯成的文件比較大,因爲整個 函數庫的所有數據都會被整合進目標代碼中,他的優點就顯而易見了,即鏈接後的執行程序不需要外部的函數庫支持,因爲所有使用的函數都已經被複制進去了。當然這也會成爲他的缺點,因爲如果靜態函數庫改變了,那麼你的程序必須重新編譯。
  • 動態庫(共享庫)
    一般爲 .so文件, 相對於靜態函數庫,動態函數庫在編譯的時候 並沒有被編譯進目標代碼中,你的程序執行到相關函數時才調用該函數庫裏的相應函數,因此動態函數庫所產生的可執行文件比較小。由於函數庫沒有被整合進你的程序,而是程序運行時動態的申請並調用,所以程序的運行環境中必須提供相應的庫。動態函數庫的改變並不影響你的程序,所以動態函數庫的升級比較方便。

靜態鏈接

鏈接器將函數的代碼從其所在地(目標文件或靜態鏈接庫中)拷貝到最終的可執行程序中。這樣該程序在被執行時這些代碼將被裝入到該進程的虛擬地址空間中。靜態鏈接庫實際上是一個目標文件的集合,其中的每個文件含有庫中的一個或者一組相關函數的代碼。

動態鏈接

在此種方式下,函數的定義在動態鏈接庫或共享對象的目標文件中。在編譯的鏈接階段,動態鏈接庫只提供符號表和其他少量信息用於保證所有符號引用都有定義,保證編譯順利通過。動態鏈接器(ld-Linux.so)鏈接程序在運行過程中根據記錄的共享對象的符號定義來動態加載共享庫,然後完成重定位。在此可執行文件被執行時,動態鏈接庫的全部內容將被映射到運行時相應進程的虛地址空間。動態鏈接程序將根據可執行程序中記錄的信息找到相應的函數代碼。

發佈了30 篇原創文章 · 獲贊 23 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章