笨辦法學C 練習29:庫和連結

飛龍發表於2019-05-12

練習29:庫和連結

原文:Exercise 29: Libraries And Linking

譯者:飛龍

C語言程式設計的核心能力之一就是連結OS所提供的庫。連結是一種為你的程式天機額外特性的方法,這些特性有其它人在系統中建立並打包。你已經使用了一些自動包含的標準庫,但是我打算對哭的不同型別和它們的作用做個解釋。

首先,庫在每個語言中都沒有良好的設計。我不知道為什麼,但是似乎語言的設計者都將連結視為不是特別重要的東西。它們通常令人混亂,難以使用,不能正確進行版本控制,並以不同的方式連結到各種地方。

C沒有什麼不同,但是C中的庫和連結是Unix作業系統的元件,並且可執行的格式在一些年前就設計好了。學習C如何連結庫有助於理解OS如何工作,以及它如何執行你的程式。

C中的庫有兩種基本型別:

靜態

你可以使用arranlib來構建它,就像上個練習中的libYOUR_LIBRARY.a那樣(Windows下字尾為.lib)。這種庫可以當做一系列.o物件檔案和函式的容器,以及當你構建程式時,可以當做是一個大型的.o檔案。

動態

它們通常以.so(Linux)或.dll(Windows)結尾。在OSX中,差不多有一百萬種字尾,取決於版本和編寫它的人。嚴格來講,OSX中的.dylib.bundleframework與前面這個三個沒什麼不同。這些檔案都被構建好並且放置到指定的地方。當你執行程式時,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,看你是否注意到了。你會在隨後修復它們。

我們打算使用dlopendlsym,和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

相關文章