Linux 上靜態(tài)鏈接庫工作原理
學(xué)習(xí)如何用靜態(tài)鏈接庫將多個 C 目標(biāo)文件結(jié)合到一個單個的可執(zhí)行文件之中。
使用 C 編寫的應(yīng)用程序時,通常有多個源碼文件,但最終你需要編譯成單個的可執(zhí)行文件。
你可以通過兩種方式來完成這項工作:通過創(chuàng)建一個 靜態(tài)static 庫 或 一個 動態(tài)dynamic 庫(也被稱為 共享shared 庫)。從創(chuàng)建和鏈接的方式來看,它們是兩種不同類型的庫。選擇使用哪種方式取決于你的的具體場景。
在 上一篇文章 中,我演示了如何創(chuàng)建一個動態(tài)鏈接的可執(zhí)行文件,這是一種更通用的方法。在這篇文章中,我將說明如何創(chuàng)建一個靜態(tài)鏈接的可執(zhí)行文件。
使用靜態(tài)庫鏈接器
鏈接器linker是一個命令,它將一個程序的多個部分結(jié)合在一起,并為它們重新組織內(nèi)存分配。
鏈接器的功能包括:
- 整合一個程序的所有的部分
- 計算出一個新的內(nèi)存組織結(jié)構(gòu),以便所有的部分組合在一起
- 恢復(fù)內(nèi)存地址,以便程序可以在新的內(nèi)存組織結(jié)構(gòu)下運行
- 解析符號引用
鏈接器通過這些功能,創(chuàng)建了一個名稱為可執(zhí)行文件的一個可運行程序。
靜態(tài)庫是通過復(fù)制一個程序中的所有依賴庫模塊到最終的可執(zhí)行鏡像來創(chuàng)建的。鏈接器將鏈接靜態(tài)庫作為編譯過程的最后一步。可執(zhí)行文件是通過解析外部引用、將庫例程與程序代碼結(jié)合在一起來創(chuàng)建的。
創(chuàng)建目標(biāo)文件
這里是一個靜態(tài)庫的示例以及其鏈接過程。首先,創(chuàng)建帶有這些函數(shù)識別標(biāo)志的頭文件 mymath.h :
int add(int a, int b);
int sub(int a, int b);
int mult(int a, int b);
int divi(int a, int b);
使用這些函數(shù)定義來創(chuàng)建 add.c 、sub.c 、mult.c 和 divi.c 文件。我將把所有的代碼都放置到一個代碼塊中,請將其分為四個文件,如注釋所示:
// add.c
int add(int a, int b){
return (a+b);
}
//sub.c
int sub(int a, int b){
return (a-b);
}
//mult.c
int mult(int a, int b){
return (a*b);
}
//divi.c
int divi(int a, int b){
return (a/b);
}
現(xiàn)在,使用 GCC 來生成目標(biāo)文件 add.o 、sub.o 、mult.o 和 divi.o:
(LCTT 校注:關(guān)于“目標(biāo)文件object file”,有時候也被稱作“對象文件”,對此,存在一些譯法混亂情形,稱之為“目標(biāo)文件”的譯法比較流行,本文采用此譯法。)
$ gcc -c add.c sub.c mult.c divi.c
-c 選項跳過鏈接步驟,而只創(chuàng)建目標(biāo)文件。
創(chuàng)建一個名稱為 libmymath.a 的靜態(tài)庫,接下來,移除目標(biāo)文件,因為它們不再被需要。(注意,使用一個 trash 命令比使用一個 rm 命令更安全。)
$ ar rs libmymath.a add.o sub.o mult.o divi.o
$ trash *.o
$ ls
add.c divi.c libmymath.a mult.c mymath.h sub.c
現(xiàn)在,你已經(jīng)創(chuàng)建了一個名稱為 libmymath 的簡單數(shù)學(xué)示例庫,你可以在 C 代碼中使用它。當(dāng)然,也有非常復(fù)雜的 C 庫,這就是他們這些開發(fā)者來生成最終產(chǎn)品的工藝流程,你和我可以安裝這些庫并在 C 代碼中使用。
接下來,在一些自定義代碼中使用你的數(shù)學(xué)庫,然后鏈接它。
創(chuàng)建一個靜態(tài)鏈接的應(yīng)用程序
假設(shè)你已經(jīng)為數(shù)學(xué)運算編寫了一個命令。創(chuàng)建一個名稱為 mathDemo.c 的文件,并將這些代碼復(fù)制粘貼至其中:
#include <mymath.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x, y;
printf("Enter two numbers\n");
scanf("%d%d",&x,&y);
printf("\n%d + %d = %d", x, y, add(x, y));
printf("\n%d - %d = %d", x, y, sub(x, y));
printf("\n%d * %d = %d", x, y, mult(x, y));
if(y==0){
printf("\nDenominator is zero so can't perform division\n");
exit(0);
}else{
printf("\n%d / %d = %d\n", x, y, divi(x, y));
return 0;
}
}
注意:第一行是一個 include 語句,通過名稱來引用你自己的 libmymath 庫。
針對 mathDemo.c 創(chuàng)建一個名稱為 mathDemo.o 的對象文件:
$ gcc -I . -c mathDemo.c
-I 選項告訴 GCC 搜索在其后列出的頭文件。在這個實例中,你通過單個點(.)來指定當(dāng)前目錄。
鏈接 mathDemo.o 和 libmymath.a 來生成最終的可執(zhí)行文件。這里有兩種方法來向 GCC 告知這一點。
你可以指向文件:
$ gcc -static -o mathDemo mathDemo.o libmymath.a
或者,你可以具體指定庫的路徑及名稱:
$ gcc -static -o mathDemo -L . mathDemo.o -lmymath
在后面的那個示例中,-lmymath 選項告訴鏈接器來鏈接對象文件 mathDemo.o 和對象文件 libmymath.a 來生成最終的可執(zhí)行文件。-L 選項指示鏈接器在下面的參數(shù)中查找?guī)欤愃朴谀闶褂?nbsp;-I 所做的工作)。
分析結(jié)果
使用 file 命令來驗證它是靜態(tài)鏈接的:
$ file mathDemo
mathDemo: ELF 64-bit LSB executable, x86-64...
statically linked, with debug_info, not stripped
使用 ldd 命令,你將會看到該可執(zhí)行文件不是動態(tài)鏈接的:
$ ldd ./mathDemo
not a dynamic executable
你也可以查看 mathDemo 可執(zhí)行文件的大?。?/p>
$ du -h ./mathDemo
932K ./mathDemo
在我 前一篇文章 的示例中,動態(tài)鏈接的可執(zhí)行文件只占有 24K 大小。
運行該命令來看看它的工作內(nèi)容:
$ ./mathDemo
Enter two numbers
10
5
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
看起來令人滿意!
何時使用靜態(tài)鏈接
動態(tài)鏈接可執(zhí)行文件通常優(yōu)于靜態(tài)鏈接可執(zhí)行文件,因為動態(tài)鏈接會保持應(yīng)用程序的組件模塊化。假如一個庫接收到一次關(guān)鍵安全更新,那么它可以很容易地修補,因為它存在于應(yīng)用程序的外部。
當(dāng)你使用靜態(tài)鏈接時,庫的代碼會“隱藏”在你創(chuàng)建的可執(zhí)行文件之中,意味著在庫每次更新時(相信我,你會有更好的東西),僅有的一種修補方法是重新編譯和發(fā)布一個新的可執(zhí)行文件。
不過,如果一個庫的代碼,要么存在于它正在使用的具有相同代碼的可執(zhí)行文件中,要么存在于不會接收到任何更新的專用嵌入式設(shè)備中,那么靜態(tài)連接將是一種可接受的選項。