# 練習29:庫和鏈接
> 原文:[Exercise 29: Libraries And Linking](http://c.learncodethehardway.org/book/ex29.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
C語言編程的核心能力之一就是鏈接OS所提供的庫。鏈接是一種為你的程序天機額外特性的方法,這些特性有其它人在系統中創建并打包。你已經使用了一些自動包含的標準庫,但是我打算對哭的不同類型和它們的作用做個解釋。
首先,庫在每個語言中都沒有良好的設計。我不知道為什么,但是似乎語言的設計者都將鏈接視為不是特別重要的東西。它們通常令人混亂,難以使用,不能正確進行版本控制,并以不同的方式鏈接到各種地方。
C沒有什么不同,但是C中的庫和鏈接是Unix操作系統的組件,并且可執行的格式在一些年前就設計好了。學習C如何鏈接庫有助于理解OS如何工作,以及它如何運行你的程序。
C中的庫有兩種基本類型:
靜態
你可以使用`ar`和`ranlib`來構建它,就像上個練習中的`libYOUR_LIBRARY.a`那樣(Windows下后綴為`.lib`)。這種庫可以當做一系列`.o`對象文件和函數的容器,以及當你構建程序時,可以當做是一個大型的`.o`文件。
動態
它們通常以`.so`(Linux)或`.dll`(Windows)結尾。在OSX中,差不多有一百萬種后綴,取決于版本和編寫它的人。嚴格來講,OSX中的`.dylib`,`.bundle`和`framework`與前面這個三個沒什么不同。這些文件都被構建好并且放置到指定的地方。當你運行程序時,OS會動態加載這些文件并且“憑空”鏈接到你的程序中。
我傾向于對于小型或中性項目使用靜態的庫,因為它們易于使用,并且工作在在更多操作系統上。我也喜歡將所有代碼當如靜態庫中,之后鏈接它來執行單元測試,或者鏈接到所需的程序中。
動態庫適用于大型系統,其中空間十分有限,或者大量程序都使用相同的功能。這種情況下不應該為每個程序的共同特性靜態鏈接所有代碼,而是應該將它放到動態庫中,這樣它僅僅會為所有程序加載一份。
在上一個練習中,我講解了如何構建靜態庫(`.a`),我會在本書的剩余部分用到它。這個練習中我打算向你展示如何構建一個簡單的`.so`庫,并且如何使用Unix系統的`dlopen`動態加載它。我會手動執行它,以便你可以理解每件實際發生的事情。之后,附加題這部分會使用c項目框架來創建它。
## 動態加載動態庫
我創建了兩個源文件里完成它。一個用于侯建`libex29.so`庫,另一個是個叫做`ex29`的程序,它可以加載這個庫并運行其中的程序、
```c
#include <stdio.h>
#include <ctype.h>
#include "dbg.h"
int print_a_message(const char *msg)
{
printf("A STRING: %s\n", msg);
return 0;
}
int uppercase(const char *msg)
{
int i = 0;
// BUG: \0 termination problems
for(i = 0; msg[i] != '\0'; i++) {
printf("%c", toupper(msg[i]));
}
printf("\n");
return 0;
}
int lowercase(const char *msg)
{
int i = 0;
// BUG: \0 termination problems
for(i = 0; msg[i] != '\0'; i++) {
printf("%c", tolower(msg[i]));
}
printf("\n");
return 0;
}
int fail_on_purpose(const char *msg)
{
return 1;
}
```
這里面沒什么神奇之處。其中故意留了一些bug,看你是否注意到了。你會在隨后修復它們。
我們打算使用`dlopen`,`dlsym`,和`dlclose`函數來處理上面的函數。
```c
#include <stdio.h>
#include "dbg.h"
#include <dlfcn.h>
typedef int (*lib_function)(const char *data);
int main(int argc, char *argv[])
{
int rc = 0;
check(argc == 4, "USAGE: ex29 libex29.so function data");
char *lib_file = argv[1];
char *func_to_run = argv[2];
char *data = argv[3];
void *lib = dlopen(lib_file, RTLD_NOW);
check(lib != NULL, "Failed to open the library %s: %s", lib_file, dlerror());
lib_function func = dlsym(lib, func_to_run);
check(func != NULL, "Did not find %s function in the library %s: %s", func_to_run, lib_file, dlerror());
rc = func(data);
check(rc == 0, "Function %s return %d for data: %s", func_to_run, rc, data);
rc = dlclose(lib);
check(rc == 0, "Failed to close %s", lib_file);
return 0;
error:
return 1;
}
```
我現在會拆分這個程序,便于你理解這一小段代碼其中的原理。
ex29.c:5
我在隨后使用這個函數指針定義,來調用庫中的函數。這沒什么新東西,確保你理解了它的作用。
ex29.c:17
在為一個小型程序做必要的初始化后,我使用了`dlopen`函數來加載由`lib_file`表示的庫。這個函數返回一個句柄,我們隨后會用到它,就像來打開文件那樣。
ex29.c:18
如果出現錯誤,我執行了通常的檢查并退出,但是要注意最后我使用了`dlerror`來查明發生了什么錯誤。
ex29.c:20
我使用了`dlsym`來獲取`lib`中的函數,通過它的字面名稱`func_to_run`。它是最強大的部分,因為我動態獲取了一個函數指針,基于我從命令行`argv`獲得的字符串。
ex29.c:23
接著我調用`func`函數,獲得返回值并檢查。
ex29.c:26
最后,我像關閉文件那樣關閉了庫。通常你需要在程序的整個運行時保持它們打開,所以關閉操作并不非常實用,我只是在這里演示它。
> 譯者注:由于能夠使用系統調用加載,動態庫可以被多種語言的程序調用,而靜態庫只能被C及兼容C的程序調用。
## 你會看到什么
既然你已經知道這些文件做什么了,下面是我的shell會話,用于構建`libex29.so`和`ex29`并隨后運行它。下面的代碼中你可以學到如何手動構建:
```shell
# compile the lib file and make the .so
# you may need -fPIC here on some platforms. add that if you get an error
$ cc -c libex29.c -o libex29.o
$ cc -shared -o libex29.so libex29.o
# make the loader program
$ cc -Wall -g -DNDEBUG ex29.c -ldl -o ex29
# try it out with some things that work
$ ex29 ./libex29.so print_a_message "hello there"
-bash: ex29: command not found
$ ./ex29 ./libex29.so print_a_message "hello there"
A STRING: hello there
$ ./ex29 ./libex29.so uppercase "hello there"
HELLO THERE
$ ./ex29 ./libex29.so lowercase "HELLO tHeRe"
hello there
$ ./ex29 ./libex29.so fail_on_purpose "i fail"
[ERROR] (ex29.c:23: errno: None) Function fail_on_purpose return 1 for data: i fail
# try to give it bad args
$ ./ex29 ./libex29.so fail_on_purpose
[ERROR] (ex29.c:11: errno: None) USAGE: ex29 libex29.so function data
# try calling a function that is not there
$ ./ex29 ./libex29.so adfasfasdf asdfadff
[ERROR] (ex29.c:20: errno: None) Did not find adfasfasdf
function in the library libex29.so: dlsym(0x1076009b0, adfasfasdf): symbol not found
# try loading a .so that is not there
$ ./ex29 ./libex.so adfasfasdf asdfadfas
[ERROR] (ex29.c:17: errno: No such file or directory) Failed to open
the library libex.so: dlopen(libex.so, 2): image not found
$
```
需要注意,你可能需要在不同OS、不同OS的不同版本,以及不同OS的不同版本的不同編譯器上執行構建,則需要修改構建共享庫的方式。如果我構建`libex29.so`的方式在你的平臺上不起作用,請告訴我,我會為其它平臺添加一些注解。
> 譯者注:到處編寫、到處調試、到處編譯、到處發布。--vczh
‍
> 注
> 有時候你會通常運行`cc -Wall -g -DNDEBUG -ldl ex29.c -o ex29`,并且認為它能夠正常工作,但是沒有。在一些平臺上,參數的順序會影響到它是否生效,這也沒什么理由。在Debian或者Ubuntu中你需要執行`cc -Wall -g -DNDEBUG ex29.c -ldl -o ex29`。它是唯一的方式,所以雖然我在這里使用了OSX,但是以后如果你鏈接動態庫的時候它找不到某個函數,要試著自己解決問題。
> 這里面比較麻煩的事情是,實際平臺的不同會影響到命令參數的順序。將`-ldl`放到某個位置沒有理由與其它位置不同。他只是一個選項,還需要了解這些簡直是太氣人了。
## 如何使它崩潰
打開`lbex29.so`,并且使用能夠處理二進制的編輯器編輯它。修改一些字節,然后關閉。看看你是否能使用`dlopen`函數來打開它,即使你修改了它。
## 附加題
+ 你注意到我在`libex29.c`中寫的不良代碼了嗎?我使用了一個`for`循環來檢查`'\0'`的結尾,修改它們使這些函數總是接收字符串長度,并在函數內部使用。
+ 使用項目框架目錄,并且為這個練習創建新的項目。將`libex29.c`放入`src/`目錄,修改`Makefile`使它能夠構建`build/libex29.so`。
+ 將`ex29.c`改為`tests/ex29_tests.c`,使它做為單元測試執行。使它能夠正常工作,意思是你需要修改它讓它加載`build/libex29.so`文件,并且運行上面我手寫的測試。
+ 閱讀`man dlopen`文檔,并且查詢所有有關函數。嘗試`dlopen`的其它選項,比如`RTLD_NOW`。
- 笨辦法學C 中文版
- 前言
- 導言:C的笛卡爾之夢
- 練習0:準備
- 練習1:啟用編譯器
- 練習2:用Make來代替Python
- 練習3:格式化輸出
- 練習4:Valgrind 介紹
- 練習5:一個C程序的結構
- 練習6:變量類型
- 練習7:更多變量和一些算術
- 練習8:大小和數組
- 練習9:數組和字符串
- 練習10:字符串數組和循環
- 練習11:While循環和布爾表達式
- 練習12:If,Else If,Else
- 練習13:Switch語句
- 練習14:編寫并使用函數
- 練習15:指針,可怕的指針
- 練習16:結構體和指向它們的指針
- 練習17:堆和棧的內存分配
- 練習18:函數指針
- 練習19:一個簡單的對象系統
- 練習20:Zed的強大的調試宏
- 練習21:高級數據類型和控制結構
- 練習22:棧、作用域和全局
- 練習23:認識達夫設備
- 練習24:輸入輸出和文件
- 練習25:變參函數
- 練習26:編寫第一個真正的程序
- 練習27:創造性和防御性編程
- 練習28:Makefile 進階
- 練習29:庫和鏈接
- 練習30:自動化測試
- 練習31:代碼調試
- 練習32:雙向鏈表
- 練習33:鏈表算法
- 練習34:動態數組
- 練習35:排序和搜索
- 練習36:更安全的字符串
- 練習37:哈希表
- 練習38:哈希算法
- 練習39:字符串算法
- 練習40:二叉搜索樹
- 練習41:將 Cachegrind 和 Callgrind 用于性能調優
- 練習42:棧和隊列
- 練習43:一個簡單的統計引擎
- 練習44:環形緩沖區
- 練習45:一個簡單的TCP/IP客戶端
- 練習46:三叉搜索樹
- 練習47:一個快速的URL路由
- 后記:“解構 K&R C” 已死