博客簡介
本篇博客是CSAPP鏈接章節的庫部分總結,目錄如下:
- 鏈接靜態庫
- 鏈接動態庫
如何打包編程者經常使用的功能函數? 在靜態庫提出之前有2種方法:
-
Option 1: 將所有函數寫入一個源代碼文件
-
編程者需要將一個很大的目標文件鏈接到其程序
-
空間和時間性能都低
-
Option 2: 每一個函數形成一個單獨的源文件
-
編程者需將自己的程序顯示地鏈接到相應的二進制文 件
-
更有效,但對於編程者是額外負擔
以上兩種庫處理方式並不能很好處理庫鏈接
鏈接靜態庫
- 將所有相關的目標模塊打包成爲 一個單獨的文件,稱爲靜態庫 (static library) , 它可以用做鏈接器的輸入。當鏈接器構造一個輸出的可執行文件時,它只拷貝靜態庫裏被應用程序引用的目標模塊。
- 相關的函數可以被編譯爲獨立的目標模 塊,然後封裝成一個單獨的靜態庫文件。然後,應用程序可以通過在命令行上指定單獨的文件名 字來使用這些在庫中定義的函數。比如,使用標準 C 庫和數學庫中函數的程序可以用形式如下 的命令行來編譯和鏈接:
unix> gcc main.c /usr/lib/libm.a /usr/lib/libe.a
- 靜態庫以一種稱爲存檔 (archive) 的特殊文件格式存放在磁盤中。存檔文件是一組連接起來的可重定位目標文件的集合,有一個頭部用來描述每個成員目標文件的大小和 位置。存檔文件名由後綴 .a 標識
- 增強了鏈接器,其可以通過在一個或多個存檔中查找符號來解析外 部引用
- 若一個存檔成員文件解析了引用,則可將其連接到可執行文件
libvector.a 靜態庫實例
- (1)假設我們想在一個叫 做 libvector.a 的靜態庫中提供向量例程:
- mulvec.c
/*mulvec.c*/
void mulvec(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++)
z[i]=x[i]*y[i];
}
- addvec.c
/* 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];
}
(2)使用AR工具創建一個庫
linux> gcc -c addvec.c multvec.c
linux> ar rcs libvector.a addvec.o multvec.o
看到已經在目錄下生成了一個libvector.a文件
(2)在頭文件 vector.h 定義libvector.a 中例程的函數原型(一開始我沒用這個也能正確編譯)
/* vector.h */
void multvec(int *x,int *y,int *z,int n);
void addvec(int *x,int *y,int *z,int n);
(3)使用這個庫,編寫main2.c, 調用 addvec 庫例程。
#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;
}
(4)編譯和鏈接輸入文件 main.o 和 libvector.a,執行指令:
gcc -O2 main2.c
gcc -static -o p2 main2.o ./libvector.a
- -static 參數告訴編譯器驅動程序,鏈接器應該構建一個完全鏈接的可執行目標文件,它可以加載到存儲器並運行,在加載時無需更進一步的鏈接。
- 當鏈接器運行時,它判定 addvec.o 定義的 addvec 符號是被 main.o 引用的,所以它拷貝 addvec.o 到可執行文件。因爲程序不引用任何由 multvec.o 定義的符號,所以鏈接器就不會拷貝這個模塊到可執行文件。
- 鏈接器還會拷貝 libc.a 中的 printf.o 模塊,以及許多 C 運行時系統中 的其他模塊。鏈接庫圖形表示如下:
鏈接器如何使用靜態庫來解析引用
上述的靜態庫鏈接過程是怎樣的?此處有一鏈接器使用靜態庫來解析引用的算法:
- 符號解析的階段,鏈接器從左到右按照它們在編譯器驅動程序命令行上出現的順序來掃描可重定位目標文件和存檔文件。
- 維持集合:
① 可重定位目標文 件的集合E:(這個集合中的文件會被合併起來形成可執行文件)
② 一個未解析的符號(即引用了但是尚未定義的符號)集合 U
③ 在前面輸入文件中已定義的符號集合D。
初始時, E、 U和 D 都是空的。
按照命令行順序從左往右掃描,對於命令行上的每個輸人文件f,如果f是一個:
- 目標文件:鏈接器把f添加到 E, 修改 U和 D來反映f中的符號定義和引用,next。
- 存檔文件:鏈接器嘗試匹配U中未解析的符號和由存檔文件成員定義 的符號。如果某個存檔文件成員 m, 定義了一個符號來解析 U 中的一個引用,那麼就將 m 加到 E 中,並且鏈接器修改 U和 D來反映 m 中的符號定義和引用。對存檔文件中所有的成員目標文件都反覆進行這個過程,直到 U和 D都不再發生變化。任何不包含在 E 中的成員目標文件都簡單地被丟棄,next。
- 當鏈接器完成對命令行上輸入文件的掃描後, U是非空的,那麼鏈接器就會輸出一個 錯誤並終止。否則,合併和重定位E中的目標文件,從而構建輸出的可執行文件。
靜態庫的缺陷:
- 文件輸入順序需要程序員特別注意
- 在存儲中的可執行文件中有多個副本 (每一個函數均需要靜態庫文件 )
- 在運行中的可執行文件中存在多個副本
- 即便是對系統庫進行小bug的修復,也需要對使用到這個庫的所有應用顯示地重新鏈接
動態(共享)鏈接庫
共享庫 (shared library) 是致力於解決靜態庫缺陷的一個現代創新產物。共享庫是一個目標模 塊,在運行時,可以加載到任意的存儲器地址,並和一個在存儲器中的程序鏈接起來。這個過程 稱爲動態鏈接 (dynamic linking), 是由一個叫做動態鏈接器 (dynamic linker) 的程序來執行的。
- 在 Unix 系統中通常用 .so 後綴來表示。
- Windows操作系統大量地利用了共享庫,它們稱爲 DLL (動態鏈接庫)
兩種共享方式
- ① 在任何給定的文件系統中,對於一個庫只有一個 .so 文件。所有引用該庫的可執行目標文件共享這個 .so 文件中的代碼和數據,而不是像靜態庫的內容那樣被拷貝和嵌人到引用它們的可執行的文件中。
- ② 其次,在存儲器中,一個共享 庫的 .text 節的一個副本可以被不同的正在運行的進程共享。
創建共享庫並完成鏈接
/*創建共享庫libvector.so*/
gcc -shared -fPIC -o libvector.so addvec.c multvec.c
/*鏈接到程序中*/
unix> gee -o p2 main2.c ./libvector.so
- -fPIC 選項指示編譯器生成與位置 無關的代碼
- -shared 選項指示鏈接器創建一個共享的目標文件
這樣就創建了一個可執行目標文件 p2, 而此文件的形式使得它在運行時可以和 libvector.so 鏈接。基本的思路是當創建可執行文件時,靜態執行一些鏈接,然後在程序加載時,動態完成鏈接過程。
- 當加載器加載和運行可執行文件 p2 時,加載部分鏈接的可執行文件 p2。接着,它注意到 p2 包含一個 .interp 節,這個節包含動態鏈接器的路徑名,動態鏈接器本身就是一個共享目標(比如,在 Linux 系統上的 LO-LINUX.SO)。
- 加載器不再像它通 常那樣將控制傳遞給應用,而是加載和運行這個動態鏈接器。
- 然後,動態鏈接器通過執行下面的重定位完成鏈接任務: ·重定位 libc.so 的文本和數據到某個存儲器段。 ·重定位 libvector.so 的文本和數據到另一個存儲器段。 ·重定位 p2中所有對由 libc.so 和 libvector.so 定義的符號的引用。
- 最後,動態鏈接器將控制傳遞給應用程序。從這個時刻開始,共享庫的位置就固定了,並且在程 序執行的過程中都不會改變
運行時的動態鏈接
除了在運行前動態鏈接庫,還可以用Linux 系統爲動態鏈接器提的一個簡單的接口,允許應用程序在運行時加載和鏈接共享庫
① dlopen:函數加載和鏈接共享庫 filename。用以前帶 RTLD_GLOBAL 選項打開的庫解析filename 中的外部符號。如果當前可執行文件是帶 -rdynamic 選項編譯的,那麼對符號解析而言,它的全局符號也是可用的。 flag 參數必須要麼包括 RTLD_NOW, 該標誌告訴鏈接器立即 解析對外部符號的引用,要麼包括 RTLD_LAZY 標誌,該標誌指示鏈接器推遲符號解析直到執行 來自庫中的代碼。這兩個值中的任意一個都可以和 RTLD_GLOBAL 標誌取或。
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);
返回:若成功則爲指向句柄的指針,若出錯則爲 N譏,L 。
② dlsym:函數的輸入是一個指向前面已經打開共享庫的句柄和一個符號名字,如果該符號存在, 就返回符號的地址,否則返回 NULL。
#include <dlfcn.h>
void *dlsym(void *handle, char *symbol);
返回:若成功則爲指向符號的指針,若出錯則爲 N口一L。
③ dlclose:如果沒有其他共享庫正在使用這個共享庫, dlclose 函數就卸載該共享庫。
#include <dlfcn.h>
int dlclose (void•handle);
返回:若成功則爲 0, 若出錯則爲 -1 。
④ dlerror:函數返回一個字符串,它描述的是調用 dlopen 、 dlsym 或者 dlclose 函數時發生 的最近的錯誤,如果沒有錯誤發生,就返回 NULL。
#include <dlfcn.h>
const cha工 *dlerror(void);
返回:如果前面對 dlopen,dlsym 或 dlclose 的調用失敗, 則爲錯誤消息,如果前面的調用成功,則爲 NULL
一個運行鏈接例子:
#include <stdio.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main() {
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
/* dynamically load the shared lib that contains addvec() */
handle = dlopen("./libvector.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror()); exit(1);
}
/* get a pointer to the addvec() function we just loaded */
addvec = dlsym(handle, "addvec");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
/* Now we can call addvec() just like any other function */
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
/* unload the shared library */
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
- 要編譯這個程序,我們將以下面的方式調用 GCC:
unix> gcc -rdynamic -O2 -o p3 dll.c -ldl