練習29:庫和連結
原文:Exercise 29: Libraries And Linking
譯者:飛龍
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
的程式,它可以載入這個庫並執行其中的程式、
#include <stdio.h>
#include <ctype.h>
#include "dbg.h"
int print_a_message(const char *msg)
{
printf("A STRING: %s
", msg);
return 0;
}
int uppercase(const char *msg)
{
int i = 0;
// BUG: termination problems
for(i = 0; msg[i] != ` `; i++) {
printf("%c", toupper(msg[i]));
}
printf("
");
return 0;
}
int lowercase(const char *msg)
{
int i = 0;
// BUG: termination problems
for(i = 0; msg[i] != ` `; i++) {
printf("%c", tolower(msg[i]));
}
printf("
");
return 0;
}
int fail_on_purpose(const char *msg)
{
return 1;
}
這裡面沒什麼神奇之處。其中故意留了一些bug,看你是否注意到了。你會在隨後修復它們。
我們打算使用dlopen
,dlsym
,和dlclose
函式來處理上面的函式。
#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`並隨後執行它。下面的程式碼中你可以學到如何手動構建:
# 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
迴圈來檢查` `
的結構,修改它們使這些函式總是接收字串長度,並在函式內部使用。 -
使用專案框架目錄,並且為這個練習建立新的專案。將
libex29.c
放入src/
目錄,修改Makefile
使它能夠構建build/libex29.so
。 -
將
ex29.c
改為tests/ex29_tests.c
,使它做為單元測試執行。使它能夠正常工作,意思是你需要修改它讓它載入build/libex29.so
檔案,並且執行上面我手寫的測試。 -
閱讀
man dlopen
文件,並且查詢所有有關函式。嘗試dlopen
的其它選項,比如RTLD_NOW
。