第 7 章-鏈接

鏈接(Linking)是將各種代碼和數據部分收集起來並組合成爲一個單一文件的過程,這個文件可被加載(或被拷貝)到存儲器並執行。鏈接可以執行於:

  • 編譯時--原代碼被翻譯爲機器代碼時
  • 加載時--程序被加載器(loader)加載到存儲器並執行時
  • 運行時--有應用程序來執行


鏈接使得 分離編譯 成爲可能。我們可以將大型應用程序分解爲一些小模塊,獨立修改和編譯。當我們修改其中一個小模塊時,只需要重新編譯它,並重新鏈接應用,而不必重新編譯其他文件。鏈接通常是由鏈接器默默處理的,學習鏈接知識有助於:

  • 構造大型程序
  • 避免一些危險的編程錯誤
  • 理解語言的作用域是如何實現的
  • 其他重要的系統概念,如加載和運行程序、虛擬存儲器、分頁和存儲器映射
  • 利用共享庫


靜態鏈接

像Unix ld 程序這樣的 靜態連接器 以一組可重定位目標文件和命令行參數作爲輸入,生成一個完全鏈接的可以加載和運行的可執行目標文件作爲輸出。圖7-2概括了驅動程序將源代碼翻譯爲可執行目標文件時的行爲。

爲了構造可執行文件,鏈接器必須完成兩個主要認任務:

  • 符號解析。目標文件定義和引用符號,符號解析的目的是將每個符號引用剛好和一個符號定義聯繫起來。
  • 重定位。編譯器和彙編器生成從地址0開始的代碼和數據節。鏈接器通過把每個符號定義與一個存儲器位置聯繫起來,然後修改所有對這些符號的引用,使得它們指向這個存儲器位置,從而 重定位 這些節。


目標文件

目標文件有三種形式:

  • 可重定位目標文件。包括二進制代碼和數據,可以在鏈接時與其他可重定位目標文件合併起來,創建一個可執行目標文件。
  • 可執行目標文件包括二進制代碼和數據,可以被直接拷貝到存儲器執行。
  • 共享目標文件。一種特殊類型的可重定位目標文件,可以在加載或者運行時被動態的加載到存儲器並鏈接。
圖7-3展示了一個典型的ELF可重定位目標文件的格式。

與靜態庫鏈接

目前爲止,我們都假設鏈接器讀取一組可重定位目標文件,並把它們鏈接起來,成爲一個輸出的可執行文件。實際上,所有的編譯系統都提供一種機制,將所有相關的目標模塊打包成爲一個單獨的文件,稱爲 靜態庫(static library)它可以用作鏈接器的輸入。當鏈接器構造一個輸出的可執行文件時,它只拷貝靜態庫中被應用程序引用的目標模塊。

在Unix系統中,靜態庫以一種稱爲 存檔(archive) 的特殊文件格式存放在磁盤中。存檔文件是一組連接起來的可重定位目標文件的集合,有一個頭部用來描述每個成員目標文件的大小和位置。存檔文件由後綴 .a 標識。下面舉個例子來說明,我們創建一個 libvector.a 的靜態庫。

/* vector.h */
void addvec(int*x, int* y, int* z, int n);
void multvec(int*x, int* y, int* z, int n);
/* addvec.c */
void addvec(int* x, int* y, int* z, int n)
{
    int i;
    for(i=0; i<n; i++){
        z[i] = x[i] + y[i];
    }
}
/* multvec.c */
void multvec(int* x, int* y, int* z, int n)
{
    int i;
    for(i=0; i<n; i++){
        z[i] = x[i] * y[i];
    }
}


爲了創建該庫,我們使用AR工具,具體如下:

  • gcc -c addvec.c multvec.c                           //編譯後得到可重定位目標文件 addvec.o multvec.o 
  • ar rcs libvector.a addvec.o multvec.o          //創建靜態庫 libvector.a
爲了使用該庫,我們編寫一個應用如下:
/*  main.c */
#include<stdio.h>
#include"vector.h"

int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];

int main()
{
    addvec(x,y,z,2);
    printf("z = [%d %d]\n", z[0], z[1]);
    return 0;
}

爲了創建這個可執行文件,我們還要編譯和鏈接輸入文件 main.o 和 libvector.a:
  • gcc -c main.c
  • gcc -static -o main main.o ./libvector.a

圖7-7概括了鏈接器的行爲。-static 參數告訴編譯器驅動程序,鏈接器應該構建一個完全鏈接的可執行目標文件,它可以加載到存儲器並運行,在加載時無需更進一步的鏈接。當鏈接器運行時,它判定 addvec.o 定義的 addvec 符號是被 main.o 引用的,所以它拷貝 addvec.o 到可執行文件。因爲程序不引用任何由 multvec.o 定義的符號,所以鏈接器不會拷貝這個模塊到可執行文件。鏈接器還會拷貝 libc.a 中的 printf.o 模塊,以及許多 c 運行時系統中的其他模塊。


可執行目標文件

我們已經看到鏈接器是如何將多個目標模塊合併成一個可執行目標文件的。我們的 C 程序,開始時是一組 ASCII 文本文件,已經被轉化爲一個二進制文件,且這個二進制文件包含加載程序到存儲器並運行它所需的所有信息。圖 7-11 概括了一個典型的 ELF可執行文件中的各類信息。



可執行目標文件的格式類似於可重定位目標文件的格式。ELF頭部描述文件的總體格式。它還包括程序的 入口點(entry point),也就是程序運行時要執行的第一條指令的地址。

要運行可執行目標文件 p,可以在 Unix 外殼的命令行輸入它的名字:

  •    ./p
因爲 p 不是一個內置的外殼命令,所以外殼會認爲p是一個可執行目標文件,通過調用某個駐留在存儲器中稱爲加載器(loader)的操作系統代碼來運行它。任何 Unix 程序都可以通過調用 execve 函數來調用加載器。
加載器將可執行目標文件中的代碼和數據從磁盤拷貝到存儲器中,然後通過跳轉到程序的第一條指令或者 入口點(entry point)來運行該程序。 這個將程序拷貝到存儲器並運行的過程叫做 加載(loading)


動態鏈接共享庫

上面提到的靜態庫解決了許多關於如何讓大量相關函數對應用程序可用的問題,然而靜態庫有一些明顯的缺點:

  • 需要定期維護和更新
  • 如果需要用庫的最新版本,需要以某種方式瞭解該庫的更新情況,然後顯示的將程序與更新了的庫重新鏈接
  • 幾乎每個c程序都是用標準 I/O 函數,如 printf 和 scanf。在運行時,這些函數的代碼會被複制到每個運行進程的文本段中,這是對存儲器系統資源的極大浪費。
共享庫 是致力於解決靜態庫缺陷的一個創新產物。共享庫 是一個目標模塊,在運行時,可以加載到任意的存儲器地址,並和一個在存儲器中的程序鏈接起來。這個過程成爲動態鏈接。是由一個叫做動態鏈接器 的程序來執行的。
共享庫也稱 共享目標,在 Unix 系統中通常以 .so 後綴來表示。微軟的操作系統大量利用了共享庫,稱爲動態鏈接庫DLL。

共享庫是以兩種不同的方式來共享的:
  • 在任何給定的文件系統中,對於一個庫只有一個 .so 文件, 所有引用該庫的可執行目標文件共享這個 .so 文件中的代碼和數據。而不是像靜態庫的內容那樣被拷貝和嵌入到引用它們的可執行文件中。
  • 在存儲器中,一個共享庫的 .text 節的一個副本可以被不同的正在運行的進程共享。
圖7-15 概括了 main.c 程序的動態鏈接過程。爲構造向量運算程序的共享庫 libector.so,我們調用編譯器,給鏈接器如下指令:
  • gcc -shared -fPIC -o libvector.so addvec.c multvec.c   // 生成共享庫 libector.so
-fPIC 指編譯器生成於位置無關的代碼;-shared 指示鏈接器生成一個共享的目標文件

然後我們將這個庫鏈接到 main.c 示例程序中:
  • gcc -o main main.c ./libvector.so
這樣就創建了一個可執行目標文件 main,而此文件的形式使得它在運行時可以和 libvector.so 鏈接。
基本思路是當創建可執行文件時,靜態執行一些鏈接,然後在程序加載時,動態完成鏈接過程

此時:沒有任何 libvector.so 的代碼和數據節真的被拷貝到可執行文件 main 中。反之,鏈接器拷貝了一些重定位和符號表信息,它們使得運行時可以解析對 libvector.so 中代碼和數據的引用。

當加載器加載和運行可執行文件 main 時,加載 部分鏈接 的可執行文件 main。接着 它注意到 main 包含一個 .interp 節,這個節包含動態鏈接器的路徑名,動態鏈接器本身就是一個共享目標。加載器此時不再像通常那樣將控制傳遞給應用,而是加載和運行這個動態鏈接器。

然後,動態鏈接器通過執行下面的重定位完成鏈接任務:
  • 重定位 libc.so 的文本和數據到某個存儲器段。
  • 重定位 libector.so 的文本和數據到另一個存儲器段。
  • 重定位 main 中所有對由 libc.so 和 libvector.so 定義的符號的引用。
最後,動態鏈接器將控制傳遞給應用程序。從這個時刻開始,共享庫的位置就固定了,並且在程序執行的過程中都不會改變。


從應用程序中加載和鏈接共享庫

到目前爲止,我們已經知道了在應用程序執行之前,即應用程序被加載時,動態鏈接器加載和鏈接共享庫的情景。然而,應用程序還可能在它運行時要求動態鏈接器加載和鏈接任意共享庫,而無需在編譯時鏈接那些庫到應用中。

動態鏈接是一項強大有用的技術。下面是一些實際的例子:

  • 分發軟件。微軟windows應用開發者常常利用共享庫來分發軟件更新。更新共享庫後,下一次運行應用程序,應用將自動鏈接和加載新的共享庫。
  • 構建高性能Web服務器。生成web服務器的動態內容。

鏈接可以在鏈接時由靜態鏈接器來完成,也可以在加載和運行時由動態鏈接器來完成。

鏈接器處理成爲目標文件的二進制文件,它有三種不同的形式:

  • 可重定位的
  • 可執行的
  • 共享的 (一種特殊的可重定位的)
可重定位的目標文件由靜態鏈接器合併成一個可執行的目標文件,它可以加載到存儲器中並執行。
共享目標文件是在運行時由動態鏈接器鏈接和加載的,或者隱含的在調用程序被加載和開始執行時,或者根據需要在程序調用dlopen庫的函數時。

鏈接器的兩個主要任務是 符號解析 和 重定位。符號解析將目標文件中的每個全局符號都綁定到一個唯一的定義,而重定位確定每個符號的最終存儲器地址,並修改對那些目標的引用。

靜態鏈接器是由像GCC這樣的編譯驅動程序調用的。它們將多個可重定位目標文件合併成一個單獨的可執行文件。多個目標文件可以定義相同的符號,而鏈接器用來悄悄解析這些多重定義的規則可能在用戶程序中引入微妙錯誤。

加載器將可執行文件的內容映射到存儲器,並運行這個程序。鏈接器還可能生成部分鏈接的可執行目標文件,這樣的文件中有對定義在共享庫中的程序和數據的未解析的引用。在加載時,加載器將部分鏈接的可執行文件映射到存儲器,然後調用動態鏈接器,它通過加載共享庫和重定位程序中的引用來完成鏈接任務。


參考

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