1. 程式人生 > >C編譯: 動態連接庫 (.so文件)

C編譯: 動態連接庫 (.so文件)

編譯器 type 進程 span dto load -o 說明 和數

作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉載,也請保留這段聲明。謝謝!

在“紙上談兵: 算法與數據結構”中,我在每一篇都會有一個C程序,用於實現算法和數據結構 (比如棧和相關的操作)。在同一個程序中,還有用於測試的main()函數,結構體定義,函數原型,typedef等等。

這樣的做法非常不“環保”。算法的實際運用和算法的實現混在一起。如果我想要重復使用之前的源程序,必須進行許多改動,並且重新編譯。最好的解決方案是實現模塊化: 只保留純粹的算法實現,分離頭文件,並編譯一個庫(library)。每次需要使用庫的時候(比如使用棧數據結構),就在程序中include頭文件,連接庫。這樣,不需要每次都改動源程序。

我在這裏介紹如何在UNIX環境中創建共享庫 (shared library)。UNIX下,共享庫以so為後綴(shared object)。共享庫與Windows下的DLL類似,是在程序運行時動態連接。多個進程可以連接同一個共享庫。

技術分享

共享庫

本文使用Ubuntu測試,使用gcc作為編譯器。

程序清理

下面程序來自紙上談兵: 棧 (stack),是棧數據結構的C實現:

技術分享
/* By Vamei */
/* use single-linked list to implement stack */
#include <stdio.h>
#include <stdlib.h>

typedef struct node *position;
typedef int ElementTP;

// point to the  head node of the list
typedef struct node *STACK;
 
struct node {
    ElementTP element;
    position next;
};

STACK init_stack(void);
void delete_stack(STACK);
ElementTP top(STACK);
void push(STACK, ElementTP);
ElementTP pop(STACK);
int is_null(STACK);

void main(void)
{
    ElementTP a;
    int i;
    STACK sk;
    sk = init_stack();
    push(sk, 1);
    push(sk, 2);
    push(sk, 8);
    printf("Stack is null? %d\n", is_null(sk));
    for (i=0; i<3; i++) {
        a = pop(sk);
        printf("pop: %d\n", a);
    }

    printf("Stack is null? %d\n", is_null(sk));    
    delete_stack(sk);
}

/*
 * initiate the stack
 * malloc the head node.
 * Head node doesn‘t store valid data
 * head->next is the top node
 */
STACK init_stack(void)
{
    position np;
    STACK    sk;
    np = (position) malloc(sizeof(struct node));
    np->next     = NULL;  // sk->next is the top node
    sk = np; 
    return sk;
}

/* pop out all elements 
 * and then delete head node
 */
void delete_stack(STACK sk)
{
    while(!is_null(sk)) {
        pop(sk);
    }
    free(sk);
}
/* 
 * View the top frame
 */
ElementTP top(STACK sk)
{
    return (sk->next->element);
}

/*
 * push a value into the stack
 */
void push(STACK sk, ElementTP value) 
{
    position np, oldTop;
    oldTop = sk->next;    

    np = (position) malloc(sizeof(struct node));
    np->element  = value;
    np->next     = sk->next;

    sk->next     = np; 
}

/* 
 * pop out the top value
 */
ElementTP pop(STACK sk)
{
    ElementTP element;
    position top, newTop;
    if (is_null(sk)) {
        printf("pop() on an empty stack");
        exit(1);
    } 
    else {
        top      = sk->next;
        element  = top->element;     
        newTop   = top->next;
        sk->next     = newTop;
        free(top);
        return element;
    } 
}

/* check whether a stack is empty*/
int is_null(STACK sk)
{
    return (sk->next == NULL);
}
技術分享

上面的main()部分是用於測試,不屬於功能模塊,在創建庫的時候應該去掉。

程序中的一些聲明,會被重復利用。比如:

技術分享
typedef struct node *position;
typedef int ElementTP;

// point to the  head node of the list
typedef struct node *STACK;
 
struct node {
    ElementTP element;
    position next;
};

STACK init_stack(void);
void delete_stack(STACK);
ElementTP top(STACK);
void push(STACK, ElementTP);
ElementTP pop(STACK);
int is_null(STACK);
技術分享

這一段程序聲明了一些結構體和指針,以及棧操作的函數原型。當我們其他程序中調用庫時 (比如創建一個棧,或者執行pop操作),同樣需要寫這些聲明。我們把這些在實際調用中需要的聲明保存到一個頭文件mystack.h。在實際調用的程序中,可以簡單的include該頭文件,避免了每次都寫這些聲明語句的麻煩。

經過清理後的C程序為mystack.c:

技術分享

/* By Vamei */
/* use single-linked list to implement stack */
#include <stdio.h>
#include <stdlib.h>
#include "mystack.h"


/* * initiate the stack * malloc the head node. * Head node doesn‘t store valid data * head->next is the top node */ STACK init_stack(void) { position np; STACK sk; np = (position) malloc(sizeof(struct node)); np->next = NULL; // sk->next is the top node sk = np; return sk; } /* pop out all elements * and then delete head node */ void delete_stack(STACK sk) { while(!is_null(sk)) { pop(sk); } free(sk); } /* * View the top frame */ ElementTP top(STACK sk) { return (sk->next->element); } /* * push a value into the stack */ void push(STACK sk, ElementTP value) { position np, oldTop; oldTop = sk->next; np = (position) malloc(sizeof(struct node)); np->element = value; np->next = sk->next; sk->next = np; } /* * pop out the top value */ ElementTP pop(STACK sk) { ElementTP element; position top, newTop; if (is_null(sk)) { printf("pop() on an empty stack"); exit(1); } else { top = sk->next; element = top->element; newTop = top->next; sk->next = newTop; free(top); return element; } } /* check whether a stack is empty*/ int is_null(STACK sk) { return (sk->next == NULL); }
技術分享

#include "..."; 語句將首先在工作目錄尋找相應文件。如果使用gcc時,增加-I選項,將在-I提供的路徑中尋找。

制作.so文件

我們的目標是制作共享庫,即.so文件。

首先,編譯stack.c:

$gcc -c -fPIC -o mystack.o mystack.c

-c表示只編譯(compile),而不連接。-o選項用於說明輸出(output)文件名。gcc將生成一個目標(object)文件mystack.o。

註意-fPIC選項。PIC指Position Independent Code。共享庫要求有此選項,以便實現動態連接(dynamic linking)。

生成共享庫:

$gcc -shared -o libmystack.so mystack.o

庫文件以lib開始。共享庫文件以.so為後綴。-shared表示生成一個共享庫。

這樣,共享庫就完成了。.so文件和.h文件都位於當前工作路徑(.)。

使用共享庫

我們編寫一個test.c,來實際調用共享庫:

技術分享
#include <stdio.h>
#include "mystack.h"

/*
* call functions in mystack library
*/ void main(void) { ElementTP a; int i; STACK sk; sk = init_stack(); push(sk, 1); push(sk, 2); push(sk, 8); printf("Stack is null? %d\n", is_null(sk)); for (i=0; i<3; i++) { a = pop(sk); printf("pop: %d\n", a); } printf("Stack is null? %d\n", is_null(sk)); delete_stack(sk); }
技術分享

註意,我們在程序的一開始include了mystack.h。

編譯上述程序。編譯器需要知道.h文件位置。

  • 對於#include "...",編譯器會在當前路徑搜索.h文件。你也可以使用-I選項提供額外的搜索路徑,比如-I/home/vamei/test。
  • 對於#include <...>,編譯器會在默認include搜索路徑中尋找。

編譯器還需要知道我們用了哪個庫文件,在gcc中:

  • 使用-l選項說明庫文件的名字。這裏,我們將使用-lmystack (即libmystack庫文件)。
  • 使用-L選項說明庫文件所在的路徑。這裏,我們使用-L. (即.路徑)。

如果沒有提供-L選項,gcc將在默認庫文件搜索路徑中尋找。

你可以使用下面的命令,來獲知自己電腦上的include默認搜索路徑:

$`gcc -print-prog-name=cc1` -v

獲知庫默認搜索路徑:

$gcc -print-search-dirs

我們所需的.h和.so文件都在當前路徑,因此,使用如下命令編譯:

$gcc -o test test.c -lmystack -L.

將生成test可執行文件。

使用

$./test

執行程序

運行程序

盡管我們成功編譯了test可執行文件,但很有可能不能執行。一個可能是權限問題。我們需要有執行該文件的權限,見Linux文件管理背景知識

另一個情況是:

./test: error while loading shared libraries: libmystack.so: cannot open shared object file: No such file or directory

這是因為操作系統無法找到庫。libmystack.so位於當前路徑,位於庫文件的默認路徑之外。盡管我們在編譯時(compile time)提供了.so文件的位置,但這個信息並沒有寫入test可執行文件(runtime)。可以使用下面命令測試:

$ldd test

ldd用於顯示可執行文件所依賴的庫。顯示:

    linux-vdso.so.1 =>  (0x00007fff31dff000)
    libmystack.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fca30de7000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fca311cb000)

這說明test可執行文件無法找到它所需的libmystack.so庫文件。

為了解決上面的問題,我們可以將.so文件放入默認搜索路徑中。但有時,特別是多用戶環境下,我們不享有在默認搜索路徑寫入的權限。

一個解決方案是設置LD_LIBRARY_PATH環境變量。比如:

$export LD_LIBRARY_PATH=.

這樣,可執行文件執行時,操作系統將在先在LD_LIBRARY_PATH下搜索庫文件,再到默認路徑中搜索。環境變量的壞處是,它會影響所有的可執行程序。如果我們在編譯其他程序時,如果我們不小心,很可能導致其他可執行文件無法運行。因此,LD_LIBRARY_PATH環境變量多用於測試。

另一個解決方案,即提供-rpath選項,將搜索路徑信息寫入test文件(rpath代表runtime path)。這樣就不需要設置環境變量。這樣做的壞處是,如果庫文件移動位置,我們需要重新編譯test。使用如下命令編譯test.c:

$gcc -g -o test test.c -lmystack -L. -Wl,-rpath=.

-Wl表示,-rpath選項是傳遞給連接器(linker)。

test順利執行的結果為:

Stack is null? 0
pop: 8
pop: 2
pop: 1
Stack is null? 1

C編譯: 動態連接庫 (.so文件)