鏈接庫概述
Linux下得庫有動態與靜態兩種,動態通常用.so為后綴,靜態用.a為后綴。面對比一下兩者:
-
靜態鏈接庫:當要使用時,連接器會找出程序所需的函數,然后將它們拷貝到執行文件,由於這種拷貝是完整的,所以一旦連接成功,靜態程序庫也就不再需要了。
-
動態庫:某個程序在運行中要調用某個動態鏈接庫函數的時候,操作系統首先會查看所有正在運行的程序,看在內存里是否已有此庫函數的拷貝了。如果有,則讓其共享那一個拷貝;只有沒有才鏈接載入。在程序運行的時候,被調用的動態鏈接庫函數被安置在內存的某個地方,所有調用它的程序將指向這個代碼段。因此,這些代碼必須使用相對地址,而不是絕對地址。在編譯的時候,我們需要告訴編譯器,這些對象文件是用來做動態鏈接庫的,所以要用地址無關代碼(Position Independent Code (PIC))。
動態鏈接庫的加載方式有兩種:隱式加載和顯示加載。
linux下進行連接的缺省操作是首先連接動態庫,也就是說,如果同時存在靜態和動態庫,不特別指定的話,將與動態庫相連接。
靜態鏈接庫
編輯測試文件
二個文件: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;
}
將.c 編譯生成 .o文件
gcc -c add.c
gcc -c sub.c
生成的文件:sub.o ,add.o
無論是靜態庫文件還是動態庫文件,都是由 .o 文件創建的。
由 .o 文件創建.a靜態庫
ar crlibmymath.a sub.o add.o
-
ar:靜態函數庫創建的命令
-c :create的意思
-r :replace的意思,表示當前插入的模塊名已經在庫中存在,則替換同名的模塊。如果若干模塊中有一個模塊在庫中不存在,ar顯示一個錯誤信息,並不替換其他同名的模塊。默認的情況下,新的成員增加在庫的結尾處。
庫文件的命名規范是以lib開頭(前綴),緊接着是靜態庫名,以 .a 為后綴名。
在程序中使用靜態庫
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()即可。
生成目標程序main,然后運行。
./main
1 + 2 = 3
1 - 2 = -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 :生成共享庫
隱式方式使用動態庫
在程序中隱式使用動態庫和使用靜態庫完全一樣,也是在使用到這些公用函數的源程序中包含這些公用函數的原型聲明,然后在用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。
-
-
為此解決方法:
-
- 我們將文件libmyhello.so復制到目錄/usr/lib中:
-
mv libmyhello.so/usr/lib/
- 2. 將libmyhello.so拷貝到可執行文件main的同一目錄下。
再次運行:
./main
1 + 2 = 3
1 - 2 = -1
成功了!這也進一步說明了動態庫在程序運行時是需要的。
動態庫的初始化和解析
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的構造函數。
動態鏈接庫(顯式鏈接)
重要的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表示在什么時候解決未定義的符號(調用)。取值有兩個:
-
RTLD_LAZY : 表明在動態鏈接庫的函數代碼執行時解決。
-
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時,才會真正被系統卸載。
顯加載示動態鏈接庫的實例
在下面這個實例中將通過動態加載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的結果同上。
Windows下和Linux下顯示加載動態鏈接庫的比較
Windows下動態鏈接庫以“.dll”為后綴,而Linux下得動態鏈接庫是以”.so”為后綴的。
特殊情況
我們回過頭看看,發現使用靜態庫和隱式方式使用動態庫時編譯成目標程序使用的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
鏈接靜態庫的可執行程序明顯比鏈接動態庫的可執行文件大。
查看庫中的符號
使用nm命令可以打印出庫中涉及到的所有符號。庫既可以是靜態庫也可以是動態的。
常見的三種符號:
①在庫中被調用,但沒有在庫中定義(表明需要其他庫支持),用U表示
②在庫中定義的函數,用T表示
③“弱態”符號,他們雖然在庫中被定義但是可能被其他庫中同名的符號覆蓋,用W表示。
用ldd命令可以查看一個可執行程序依賴的共享庫。
Linux下so導出指定函數
Linux下編譯so導出源文件里面的指定函數:
1、在文件里面最前面加上:#defineDLL_PUBLIC attribute((visibility("default")))
2、在文件里面需要導出的函數前加上:extern "C" DLL_PUBLIC
3、Linux下動態庫(so)編譯時默認不導出,在Makefile中需要添加:-fvisibility=hidden