Linux學習筆記 三 linux下的連結庫以及實現
1、連結庫概述
Linux下得庫有動態與靜態兩種,動態通常用.so為字尾,靜態用.a為字尾。面對比一下兩者:
靜態連結庫:當要使用時,聯結器會找出程式所需的函式,然後將它們拷貝到執行檔案,由於這種拷貝是完整的,所以一旦連線成功,靜態程式庫也就不再需要了。
動態庫而言:某個程式在執行中要呼叫某個動態連結庫函式的時候,作業系統首先會檢視所有正在執行的程式,看在記憶體裡是否已有此庫函式的拷貝了。如果有,則讓其共享那一個拷貝;只有沒有才連結載入。在程式執行的時候,被呼叫的動態連結庫函式被安置在記憶體的某個地方,所有呼叫它的程式將指向這個程式碼段。因此,這些程式碼必須使用相對地址,而不是絕對地址。在編譯的時候,我們需要告訴編譯器,這些物件檔案是用來做動態連結庫的,所以要用地址無關程式碼(Position Independent Code (PIC))
動態連結庫的載入方式有兩種:隱式載入和顯示載入。
注意:linux下進行連線的預設操作是首先連線動態庫,也就是說,如果同時存在靜態和動態庫,不特別指定的話,將與動態庫相連線(見本文第四部分)。
2、靜態連結庫
下面就通過實際的例子來向大家演示一下,該怎樣編譯和使用靜態和動態連結庫:
2.1 編輯測試檔案
二個檔案:add.c、 sub.c、add.h 、sub.h 和 main.c
/*add.h */ #ifndef _ADD_H_ #define _ADD_H_ int add(int a, int b); #endif ------------------------------------------------------------------------------------------------- /*add.c*/ #include "add.h" int add(int a, int b) { return a+b; } ------------------------------------------------------------------------------------------------- /*sub.h*/ #ifndef _SUB_H_ #define _SUB_H_ int sub(int a, int b); #endif ------------------------------------------------------------------------------------------------- /*sub.c*/ #include "add.h" int sub(int a, int b) { return a-b; } ------------------------------------------------------------------------------------------------- /*main.c*/ #include <stdio.h> #include "add.h" #include "sub.h" int main(void) { printf("1 + 2 =%d\n", add(1, 2)); printf("1 - 2 =%d\n", sub(1, 2)); return 0; }
-------------------------------------------------------------------------------------------------
2.2 將.c 編譯生成 .o檔案
gcc -c add.c
gcc -c sub.c
生成的檔案:sub.o ,add.o
無論是靜態庫檔案還是動態庫檔案,都是由 .o 檔案建立的。
2.3 由 .o 檔案建立.a靜態庫
ar crlibmymath.a sub.o add.o
ar:靜態函式庫建立的命令 -c :create的意思 -r :replace的意思,表示當前插入的模組名已經在庫中存在,則替換同名的模組。如果若干模組中有一個模組在庫中不存在,ar顯示一個錯誤資訊,並不替換其他同名的模組。預設的情況下,新的成員增加在庫德結尾處。 |
庫檔案的命名規範是以lib開頭(字首),緊接著是靜態庫名,以 .a 為字尾名。
2.4 在程式中使用靜態庫
gcc -o main main.c -L. –lmymath
-L 指定函式庫查詢的位置,注意L後面還有'.',表示在當前目錄下查詢 -l則指定函式庫名,其中的lib和.a(.so)省略。 注意:-L是指定查詢位置,-l指定需要操作的庫名。 |
靜態庫製作完了,如何使用它內部的函式呢?只需要在使用到這些公用函式的源程式中包含這些公用函式的原型宣告,然後在用gcc命令生成目標檔案時指明靜態庫名(是mymath 而不是libmymath.a ),gcc將會從靜態庫中將公用函式連線到目標檔案中。注意,gcc會在靜態庫名前加上字首lib,然後追加副檔名.a得到的靜態庫檔名來查詢靜態庫檔案。在程式main.c中,我們包含了靜態庫的標頭檔案add.h和sub.h,然後在主程式main中直接呼叫公用函式add()和sub()即可。
2.5 生成目標程式main,然後執行。
./main
1 + 2 = 3
1 - 2 = -1
3、動態庫(隱式連結)
3.1 由 .o建立.so動態庫
動態庫檔名命名規範和靜態庫檔名命名規範類似,也是在動態庫名增加字首lib,但其副檔名為.so。例如:我們將建立的動態庫名為mymath,則動態庫檔名就是libmamath.so。用gcc來建立動態庫。在系統提示符下鍵入以下命令得到動態庫檔案libmamath.so。
gcc -fPIC-o add.o -c add.c
gcc -fPIC-o sub.o -c sub.c
gcc -shared-o libmamath.so add.o sub.o
或者:
gcc –c –o add.oadd.c
gcc –c –o sub.osub.c
gcc -shared -fPCI-o libmyhello.so add.o sub.o
這裡:
-fpic:產生程式碼位置無關程式碼 -shared :生成共享庫 |
3.2 隱式方式使用動態庫
在程式中隱式使用動態庫和使用靜態庫完全一樣,也是在使用到這些公用函式的源程式中包含這些公用函式的原型宣告,然後在用gcc命令生成目標檔案時指明動態庫名進行編譯。我們先執行gcc命令生成目標檔案,再執行它看看結果。
gcc -o main main.c -L. -lmymath
./main
./main: error while loading shared libraries:libmymath.so: cannot open shared object file: No such file or directory
出錯了!!!
快看看錯誤提示,原來是找不到動態庫檔案libmyhello.so。程式在執行時,會在/usr/lib和/lib等目錄中查詢需要的動態庫檔案。若找到,則載入動態庫,否則將提示類似上述錯誤而終止程式執行。
動態庫的搜尋路徑搜尋的先後順序是:
1.編譯目的碼時指定的動態庫搜尋路徑;
2.環境變數LD_LIBRARY_PATH指定的動態庫搜尋路徑;
3.配置檔案/etc/ld.so.conf中指定的動態庫搜尋路徑;//只需在在該檔案中追加一行庫所在的完整路徑如"/root/test/conf/lib"即可,然後ldconfig是修改生效。
4.預設的動態庫搜尋路徑/lib;
5.預設的動態庫搜尋路徑/usr/lib。
為此解決方法:
1. 我們將檔案libmyhello.so複製到目錄/usr/lib中:
mv libmyhello.so/usr/lib/
2. 將libmyhello.so拷貝到可執行檔案main的同一目錄下。
再次執行:./main
1 + 2 = 3
1 - 2 = -1
成功了!這也進一步說明了動態庫在程式執行時是需要的。
3.3 動態庫的初始化和解析
Windows下的動態庫載入,解除安裝都會有初始化函式以及解除安裝函式來完成庫的初始化以及資源回收,linux當然也可以實現,這些初始化函式主要包含兩個部分:動態庫的構造和解構函式機制、動態庫的全域性變數初始化工作。
(1)動態庫的構造和解構函式機制
在Linux中,提供了一個機制:在載入和解除安裝動態庫時,可以編寫一些函式,處理一些相應的事物,我們稱這些函式為動態庫的構造和解構函式,其程式碼格式如下:
void __attribute__ ((constructor)) my_init(void); // my_init為自定義的建構函式名
void __attribute__ ((destructor)) my_fini(void); //my_fini為自定義的解構函式名
在編譯共享庫時,不能使用"-nonstartfiles"或"-nostdlib"選項,否則構建與解構函式將不能正常執行(除非你採取一定措施)。
注意,建構函式的引數必須為空,返回值也必須為空。
舉個例子,動態庫檔案a.c的程式碼如下:
void __attribute__((constructor)) my_init(void)
{
printf("init library\n");
}
編譯成動態庫:
gcc -fPIC -shared a.c -o liba.so
主程式main.c如下:
#include<stdlib.h>
#include<stdio.h>
int main()
{
pause();
return 0;
}
編譯:
gcc -L./ -la main.c -o main.bin
執行main.bin程式:
也就是說,在執行main時,載入完liba.so後,自動執行liba.so的初始化函式。
(2)全域性變數初始化
①先看如下例子:
//檔名:b1.c
#include<stdlib.h>
#include<stdio.h>
int reti()
{
printf("reti\n");
return 10;
}
int g1=reti(); // g1是個全域性變數。
使用GCC對其進行編譯:
gcc -fPIC -shared b1.c -o libb.so
編譯錯誤!使用G++對其進行編譯:
g++ -fPIC -shared b1.c -o libb.so
編譯成功!可見GCC和G++對於這種全域性變數初始化的方法,支援力度是不一樣的。
//主程式
//檔名:main.c
#include <stdlib.h>
#include <stdio.h>
int main()
{
pause();
return 0;
}
編譯執行檔案:
gcc -L./ -lb main.c -o main.bin
執行main.bin:
這說明,程序在載入libb.so後,為了初始化全域性變數g1,其會執行reti來初始化g1。
②再來看一個C++的例子:
//檔名:b2.cpp
class Myclass
{
public:
Myclass();
int i;
};
Myclass::Myclass()
{
printf("constructMyclass\n");
};
Myclass g1;
編譯動態庫:
g++ -fPIC -shared b2.cpp-o libb.so
在動態庫libb.so中,聲明瞭一個型別為Myclass的全域性變數g1。
//主程式
//檔名:main.cpp
#include <stdlib.h>
#include <stdio.h>
#include<unistd.h>
int main()
{
pause();
return 0;
}
編譯執行檔案:
g++ -L./ -lb main.cpp -o main.bin
執行main.bin:
這說明,程序在載入liba.so後,為了初始化全域性變數g1,程式在進入main函式前將會執行Myclass的建構函式。
4、動態連結庫(顯式連結)
4.1 重要的dlfcn.h標頭檔案
LINUX下使用動態連結庫,源程式需要包含dlfcn.h標頭檔案,此檔案定義了呼叫動態連結庫的函式的原型。下面詳細說明一下這些函式。
函式dlerror:
原型為: const char *dlerror(void);
當動態連結庫操作函式執行失敗時,dlerror可以返回出錯資訊,返回值為NULL時表示操作函式執行成功。
函式dlopen:開啟指定的動態連結庫檔案
原型為: void *dlopen (const char *filename, int flag);
dlopen用於開啟指定名字(filename)的動態連結庫,並返回操作控制代碼。
filename: 如果名字不以/開頭,則非絕對路徑名,將按下列先後順序查詢該檔案:
(1) 使用者環境變數中的LD_LIBRARY值;
(2) 動態連結緩衝檔案/etc/ld.so.cache
(3) 目錄/lib,/usr/lib
flag表示在什麼時候解決未定義的符號(呼叫)。取值有兩個:
1) RTLD_LAZY : 表明在動態連結庫的函式程式碼執行時解決。
2) RTLD_NOW : 表明在dlopen返回前就解決所有未定義的符號,一旦未解決,dlopen將返回錯誤。
dlopen呼叫失敗時,將返回NULL值,否則返回的是操作控制代碼。
函式dlsym : 取函式執行地址
原型為: void *dlsym(void *handle, char *symbol);
dlsym根據動態連結庫操作控制代碼(handle)與符號(symbol),返回符號對應的函式的執行程式碼地址。由此地址,可以帶引數執行相應的函式。
如程式程式碼: void (*add)(int x,int y); /* 說明一下要呼叫的動態函式add */
add=dlsym("xxx.so","add"); /* 開啟xxx.so共享庫,取add函式地址 */
add(89,369); /* 帶兩個引數89和369呼叫add函式 */
函式dlclose : 關閉動態連結庫
原型為: int dlclose (void *handle);
dlclose用於關閉指定控制代碼的動態連結庫,只有當此動態連結庫的使用計數為0時,才會真正被系統解除安裝。
4.2 顯載入示動態連結庫的例項
在下面這個例項中將通過動態載入libmymath.so連結庫,來呼叫add()和sub()兩個函式。
/*main.c*/
#include <stdio.h>
#include <dlfcn.h>
int main(void)
{
void*dp=dlopen("libmymath.so",RTLD_LAZY);
if(NULL==dp)
{
printf("開啟動態連結庫時失敗!");
return1;
}
//定義函式指標
int(*fn_add)(int,int)=NULL;
int(*fn_sub)(int,int)=NULL;
fn_add=dlsym(dp,"add");
fn_sub=dlsym(dp,"sub");
if(NULL==fn_add|| NULL==fn_sub)
{
printf("在動態連結庫中尋找函式失敗!");
return1;
}
printf("1+ 2 = %d\n", fn_add(1, 2));
printf("1- 2 = %d\n", fn_sub(1, 2));
dlclose(dp);
return0;
}
將libmymath.so和main.c放在同一個目錄下,執行如下命令:
gcc -rdynamic -s -o main.bin main.c -ldl
-rdynamic選項以指定輸出檔案為動態連結的方式 -s指定刪除目標檔案中的符號表, -ldl則指示裝配程式ld需要裝載dl函式庫。 |
最後執行main.bin的結果同上。
4.3 Windows下和Linux下顯示載入動態連結庫的比較
Windows下動態連結庫以“.dll”為字尾,而Linux下得動態連結庫是以”.so”為字尾的。
函式功能 |
Windows下 |
Linux下 |
開啟載入動態連結庫 |
LoadLibrary |
dlopen |
獲取動態連結庫中的函式地址 |
GetProcAddress |
dlsym |
關閉動態連結庫 |
FreeLibrary |
dlclose |
在使用時應包含的標頭檔案 |
Winbase.h(include Windows.h) |
dlfcn.h |
5、特殊情況
我們回過頭看看,發現使用靜態庫和隱式方式使用動態庫時編譯成目標程式使用的gcc命令完全一樣,那當靜態庫和動態庫同名時,gcc命令會使用哪個庫檔案呢?抱著對問題必究到底的心情,來試試看。先刪除除.c和.h外的所有檔案,恢復成我們剛剛編輯完舉例程式狀態。
gcc -c add.c
gcc -c sub.c
ar crlibmymath.a sub.o add.o
gcc -shared -fPCI -olibmyhello.so sub.o add.o
現在目錄有兩個同名的庫檔案(動態庫檔案和靜態庫檔案同名):
libmymath.a 、 libmymath.so
編譯執行程式:
gcc -o main main.c -L. -lmymath
./main
./main: error while loading shared libraries:libmymath.so: cannot open shared object file: No such file or directory
從程式./main執行的結果中很容易知道,當Linux靜態庫和Linux動態庫同名時, gcc命令將優先使用動態庫。如果強制使用靜態庫則需要加-static選項支援,即:
gcc-static -o main main.c -L. -lmymath
連結靜態庫的可執行程式明顯比連結動態庫的可執行檔案大。
6、檢視庫中的符號
1、使用nm命令可以打印出庫中涉及到的所有符號。庫既可以是靜態庫也可以是動態的。
常見的三種符號:
①在庫中被呼叫,但沒有在庫中定義(表明需要其他庫支援),用U表示
②在庫中定義的函式,用T表示
③“弱態”符號,他們雖然在庫中被定義但是可能被其他庫中同名的符號覆蓋,用W表示。
2、用ldd命令可以檢視一個可執行程式依賴的共享庫。
7、Linux下so匯出指定函式
Linux下編譯so匯出原始檔裡面的指定函式:
1、在檔案裡面最前面加上:#defineDLL_PUBLIC __attribute__((visibility("default")))
2、在檔案裡面需要匯出的函式前加上:extern "C" DLL_PUBLIC
3、Linux下動態庫(so)編譯時預設不匯出,在Makefile中需要新增:-fvisibility=hidden