UNIX再学习 -- 静态库与共享库
生活随笔
收集整理的這篇文章主要介紹了
UNIX再学习 -- 静态库与共享库
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、庫
本質上來說庫是一種可執行代碼的二進制形式,可以被操作系統載入內存執行。由于Windows和linux本質不同,因此二者庫的二進制是不兼容的。庫有兩種:靜態庫(.a、.lib)和共享庫也稱動態庫(.so、.dll)。回顧下,我們之前講gcc編譯過程可分為四個階段:預處理->>編譯->>匯編->>鏈接。而所謂的靜態、動態就指的是鏈接階段。
注意,可執行代碼的二進制形式,即ELF格式。以后有時間會詳細介紹下它。
參看:可執行文件(ELF)格式的理解
參看:C語言再學習-- readelf、objdump、nm使用詳解
二、靜態庫
1、靜態庫介紹
靜態庫指將所有相關的目標文件打包成為一個單獨的文件,即靜態庫文件,其缺省擴展名是 ?.a 。鏈接靜態庫就是將庫中被調用的代碼復制到調用模塊中。靜態庫占用空間大,庫中代碼一旦修改必須重新鏈接。使用靜態庫的代碼在運行時無需依賴庫,且執行效率高。 靜態庫命名規范,必須是"lib[your_library_name].a":lib為前綴,中間是靜態庫名,擴展名為.a。例如:libadd.a
以下源碼文件將用于下面的講解: //add.c 加法運算函數 #include "add.h" int add_int (int ia, int ib) {return ia + ib; }//add.h 頭文件 #ifndef ADD_H//防止頭文件被多次包含 #define ADD_H #include <stdio.h> //聲明一個函數 int add_int(int ia,int ib); #endif//main.c 主函數 #include "add.h" int main() {printf("計算兩個整數的和是:%d\n",add_int(20,40));return 0; }
2、靜態庫的創建和使用
(1)只編譯不鏈接,生成目標文件
gcc -c add.c main.c 生成目標文件 add.o main.o(2)使用 ar -r 命令創建靜態庫文件 ?(創建)
ar -r lib庫名 .a 目標文件ar -r libadd.a add.o 創建靜態庫文件 libadd.a
(3)鏈接測試程序和庫文件 ?(使用)
主要有三種方法: 1)直接連接 gcc main.o libadd.a -o add 生成可執行文件 add2)通過編譯器選項進行間接鏈接 (重點) gcc/cc main.o -l 庫名 -L 庫文件所在的路徑gcc main.o -l add -L . -o add 生成可執行文件 add3)配置環境變量 LIBRARY_PSTH 進行連接 export LIBRARY_PATH=$LIBRARY_PATH:.
gcc/cc main.o -l 庫名?
未配置環境變量之前編譯錯誤: gcc main.o -l add /usr/bin/ld: cannot find -ladd collect2: ld 返回 1 配置環境變量 export LIBRARY_PATH=$LIBRARY_PATH:. gcc main.o -l add -o add 生成可執行文件 add
3、講解
(1)首先gcc編譯過程,之前有講參看:C語言再學習 -- GCC編譯過程?在此就不重復了。不過需要知道為什么只編譯不鏈接,生成目標文件。前面已經講到了,gcc編譯過程可分為四個階段:預處理->>編譯->>匯編->>鏈接。而所謂的靜態、動態就指的是鏈接階段。所以它需要在匯編階段生成目標文件,然后鏈接靜態庫/動態庫生成可執行文件。 (2)ar命令 感興趣的可以 ?man ar The GNU ar program creates, modifies, and extracts from archives. An archive is a single file holding a collection of other filesin a structure that makes it possible to retrieve the original individual files (called members of the archive). The original files' contents, mode (permissions), timestamp, owner, and group are preserved in the archive, and can be restored onextraction. ar [選項] <靜態庫文件> <目標文件列表> -r ? ? 將目標文件插入到靜態庫中,已存在則更新 -q ? ?將目標文件追加到靜態庫尾 -d ? ?從靜態庫中刪除目標文件 -t ? ? 列表顯示靜態庫中的目標文件 -x ? ? 將靜態庫展開為目標文件 (3)鏈接 gcc編譯器,連接程序選項說明:-L dir:將dir所指出的目錄加到“函數庫搜索列表”中,dir 為庫文件所在的路徑
-llib: 鏈接lib庫,lib 為庫名
-I name: 連接時,加載名字為name的函數庫。該庫位于系統預設的目錄或者由-L選項確定的目錄下。實際的庫名是libname(后綴為.a或.so) (4)配置環境變量 上篇文章已經專門講了,參看:Unix再學習 -- 環境變量?需要注意的是,靜態庫屬于編譯鏈接階段,所以如此配置?export LIBRARY_PATH=$LIBRARY_PATH:. ?而它的意思是,靜態庫文件在當前目錄下查找,配置的環境變量對當前用戶臨時有效。
三、共享庫
1、共享庫介紹
共享庫和靜態庫最大的不同就是,鏈接共享庫并不需要將庫中被調用的代碼復制到調用模塊中,相反被嵌入到調用模塊中的僅僅是被調用代碼在共享庫中的相對地址。如果共享庫中的代碼同時為多個進程所用,共享庫的實例在整個內存空間中僅需一份,這正是共享的意義所在。共享庫占用空間小,即使修改了庫中的代碼,只要接口保持不變,無需重新鏈接。使用共享庫的代碼在運行時需要依賴庫,執行效率略低。而共享庫的缺省擴展名是: .so 共享庫命名規范,必須是"lib[your_library_name].so":lib為前綴,中間是共享庫名,擴展名為 .so例如:libadd.so
2、共享庫的創建和使用
(1)只編譯不鏈接,生成目標文件
gcc -c -fpic add.c main.c 生成目標文件 add.o main.o(2)創建共享庫文件 (創建)
gcc/cc -shared xxx.o -o lib庫名.sogcc -shared add.o -o libadd.so 生成共享庫文件 libadd.so
(3)鏈接測試程序和庫文件 (使用)
主要有三種方法:1)直接鏈接gcc main.o libadd.so -o add 生成可執行文件 add2)通過編譯器選項進行間接鏈接 (重點)gcc/cc main.o -l 庫名 -L 庫文件所在的路徑gcc main.o -l add -L . -o add 生成可執行文件 add3)配置環境變量?LIBRARY_PSTH 和?LD_LIBRARY_PSTH 進行連接、運行export LIBRARY_PATH=$LIBRARY_PATH:.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
gcc/cc main.o -l 庫名未配置環境變量 LIBRARY_PATH 之前編譯錯誤: gcc main.o -l add /usr/bin/ld: cannot find -ladd collect2: ld 返回 1 未配置環境變量 LD_LIBRARY_PATH 之前運行錯誤: ./add: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory 配置環境變量 export LIBRARY_PATH=$LIBRARY_PATH:. gcc main.o -l add -o add 生成可執行文件 add 配置環境變量 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. 運行 ./add 成功
3、講解
(1)gcc編譯過程,同靜態庫一樣。不過gcc編譯里面使用了選項 -fpic 需要講一下。 PIC (Position Independent Code,告訴編譯器產生與位置無關代碼) 調用代碼通過相對地址標識被調用代碼的位置,模塊中的指令與該模塊被加載到內存中的位置無關。 (通俗點就是在可執行程序裝載它們的時候,它們可以放在可執行程序的內存里的任何地方。) -fPIC:大模式,生成代碼比較大,運行速度比較慢,所有平臺都支持。 -fpic :小模式,生成代碼比較小,運行速度比較快,僅部分平臺支持。 我們可以比較下未使用 -fpic 和使用后生成的目標文件大小。可以看出,使用 fpic 生成的目標文件,多了.group 和 .text? 詳細可參看:GCC參數的官方介紹 -fpic Generate position-independent code (PIC) suitable for use in a shared library, if supported for the target machine. Such code accesses all constant addresses through a global offset table (GOT). The dynamic loader resolves the GOT entries when the program starts (the dynamic loader is not part of GCC; it is part of the operating system). If the GOT size for the linked executable exceeds a machine-specific maximum size, you get an error message from the linker indicating that -fpic does not work; in that case, recompile with -fPIC instead. (These maximums are 8k on the SPARC, 28k on AArch64 and 32k on the m68k and RS/6000. The x86 has no such limit.)Position-independent code requires special support, and therefore works only on certain machines. For the x86, GCC supports PIC for System V but not for the Sun 386i. Code generated for the IBM RS/6000 is always position-independent.When this flag is set, the macros __pic__ and __PIC__ are defined to 1.-fPIC If supported for the target machine, emit position-independent code, suitable for dynamic linking and avoiding any limit on the size of the global offset table. This option makes a difference on AArch64, m68k, PowerPC and SPARC.Position-independent code requires special support, and therefore works only on certain machines.When this flag is set, the macros __pic__ and __PIC__ are defined to 2.(2)gcc編譯 -shared選項 產生共享庫文件? -shared Produce a shared object which can then be linked with other objects to form an executable. Not all systems support this option.(3)配置環境變量
通過?未配置環境變量 LIBRARY_PATH 之前編譯錯誤、未配置環境變量 LD_LIBRARY_PATH 之前運行錯誤 可以明顯驗證,它們的作用了。 LIBRARY_PATH:Linux gcc編譯鏈接時的共享庫搜索路徑。
LIBRARY_PATH:執行二進制文件時的共享庫搜索路徑。
在可執行程序的鏈接階段,并不將所調用函數的二進制代碼復制到可執行程序中,而只是將該函數在共享庫中的地址嵌入到調用模塊中,因此運行時需要依賴共享庫。 (4)gcc缺省鏈接共享庫,可通過 -static 選項強制鏈接靜態庫。 參看:GCC -static常見問題
-static On systems that support dynamic linking, this prevents linking with the shared libraries. On other systems, this option has no effect.在GCC中,會優先使用shard library. 為了確保使用的是靜態庫,則使用此選項。例如:
鏈接時有-static選項,卻鏈接了共享庫,則會報錯 # gcc -static main.o libadd.so -o add /usr/bin/ld: attempted static link of dynamic object `libadd.so' collect2: ld 返回 1
四、靜態庫與共享庫比較
(1)靜態庫在程序編譯時會被連接到目標代碼中,程序運行時將不再需要該靜態庫。當程序與靜態庫連接時,庫中目標文件所含的所有將被程序使用的函數的機器碼被copy到最終的可執行文件中。這就會導致最終生成的可執行代碼量相對變多,相當于編譯器將代碼補充完整了,這樣運行起來相對就快些。不過會有個缺點: 占用磁盤和內存空間。靜態庫會被添加到和它連接的每個程序中,而且這些程序運行時,都會被加載到內存中,無形中又多消耗了更多的內存空間。(2)動態庫在程序編譯時并不會被連接到目標代碼中,而是在程序運行是才被載入,因此在程序運行時還需要動態庫存在。與共享庫連接的可執行文件只包含它需要的函數的引用表,而不是所有的函數代碼,只有在程序執行時,那些需要的函數代碼才被拷貝到內存中。這樣就使可執行文件比較小,節省磁盤空間,更進一步,操作系統使用虛擬內存,使得一份共享庫駐留在內存中被多個程序使用,也同時節約了內存。不過由于運行時要去鏈接庫會花費一定的時間,執行速度相對會慢一些。
(3)總的來說靜態庫是犧牲了空間效率,換取了時間效率,共享庫是犧牲了時間效率換取了空間效率,沒有好與壞的區別,只看具體需要了。
(4)另外,一個程序編好后,有時需要做一些修改和優化,如果我們要修改的剛好是庫函數的話,在接口不變的前提下,使用共享庫的程序只需要將共享庫重新編譯就可以了,而使用靜態庫的程序則需要將靜態庫重新編譯好后,將程序再重新編譯一遍。例如,將 add.c改為乘法運算://乘法運算 #include "add.h"int add_int(int ia,int ib) {return ia*ib; } 共享庫操作是:生成目標文件add.o: gcc -c -fpic add.c 生成共享庫文件libadd.so: gcc -shared add.o -o libadd.so 不需要再編譯生成可執行文件 直接執行./add 輸出結果: 計算兩個整數的和是:800 靜態庫操作是:生成目標文件add.o: gcc -c add.c 生成靜態庫文件libadd.a: ar -r libadd.a add.o 重新編譯生成可執行文件add: gcc main.o libadd.a -o add 執行./add 輸出結果: 計算兩個整數的和是:800
五、動態庫的顯式調用
參看:C++靜態庫與動態庫#include <dlfcn.h>,提供了下面幾個接口:(1)加載共享庫?dlopenvoid * dlopen( const char * pathname, int mode )函數功能:以指定模式打開指定的動態連接庫文件,并返回一個句柄給調用進程。打開模式:RTLD_LAZY ?暫緩決定,等有需要時再解出符號?
RTLD_NOW 立即決定,返回前解除所有未決定的符號。?返回值:?
打開錯誤返回NULL,成功,返回庫引用?
編譯時候要加入 -ldl (指定dl庫)?感興趣的可以 man dlopen One of the following two values must be included in flag:RTLD_LAZYPerform lazy binding. Only resolve symbols as the code that references them is executed. If the symbol is never referenced,then it is never resolved. (Lazy binding is only performed for function references; references to variables are always imme‐diately bound when the library is loaded.)RTLD_NOWIf this value is specified, or the environment variable LD_BIND_NOW is set to a nonempty string, all undefined symbols in thelibrary are resolved before dlopen() returns. If this cannot be done, an error is returned.(2)獲取函數地址 dlsymvoid* dlsym(void* handle,const char* symbol)函數功能:dlsym根據動態鏈接庫操作句柄(handle)與符號(symbol),返回符號對應的地址。使用這個函數不但可以獲取函數地址,也可以獲取變量地址。handle是由dlopen打開動態鏈接庫后返回的指針,symbol就是要求獲取的函數或全局變量的名稱。
(3)卸載共享庫?dlcloseint dlclose (void *handle)函數功能:dlclose用于關閉指定句柄的動態鏈接庫,只有當此動態鏈接庫的使用計數為0時,才會真正被系統卸載。
(4)獲取錯誤信息?dlerrorconst char *dlerror(void)函數功能:當動態鏈接庫操作函數執行失敗時,dlerror可以返回出錯信息,返回值為NULL時表示操作函數執行成功。
舉個栗子:生成共享庫文件:gcc -shared add.o -o libadd.so ?將上面?main.c 改為如下,使用動態度顯示調用函數,調用 add_int 函數:#include <stdio.h> #include <stdlib.h> #include <dlfcn.h> //動態鏈接庫路徑 #define LIB_ADD_PATH "./libadd.so" //函數指針 typedef int (*ADD_FUNC) (int, int); int main (void) { void *handle; char *error; ADD_FUNC add_func = NULL; //打開動態鏈接庫 handle = dlopen (LIB_ADD_PATH, RTLD_LAZY); if (!handle) { fprintf (stderr, "%s\n", dlerror ()); exit (EXIT_FAILURE); } //清除之前存在的錯誤 dlerror (); //獲取一個函數 add_func = (ADD_FUNC)dlsym (handle, "add_int"); if (!add_func){fprintf (stderr, "%s\n", dlerror ()); exit (EXIT_FAILURE); }printf ("計算兩個整數的和是:%d\n", add_func (20, 40)); if(dlclose (handle)){fprintf (stderr, "%s\n", dlerror ()); exit (EXIT_FAILURE); }return 0; } 編譯選項如下:gcc -rdynamic -o add main.c -ldl
執行 ./add 輸出結果: 計算兩個整數的和是:60
六、庫相關命令
(1)nm命令
參看:C語言再學習-- readelf、objdump、nm使用詳解 其中,nm 命令可以打印庫中所涉及到的所有符號,既可以用以靜態庫也可以用以共享庫。 //用于共享庫 # nm libadd.so 00001f28 a _DYNAMIC 00001ff4 a _GLOBAL_OFFSET_TABLE_w _Jv_RegisterClasses 00001f18 d __CTOR_END__ 00001f14 d __CTOR_LIST__ 00001f20 d __DTOR_END__ 00001f1c d __DTOR_LIST__ 000004ec r __FRAME_END__ 00001f24 d __JCR_END__ 00001f24 d __JCR_LIST__ 0000200c A __bss_startw __cxa_finalize@@GLIBC_2.1.3 00000420 t __do_global_ctors_aux 00000350 t __do_global_dtors_aux 00002008 d __dso_handlew __gmon_start__ 00000407 t __i686.get_pc_thunk.bx 0000200c A _edata 00002014 A _end 00000458 T _fini 000002f0 T _init 0000040c T add_int 0000200c b completed.6159 00002010 b dtor_idx.6161 000003d0 t frame_dummy//用于靜態庫 nm libadd.a add.o: 00000000 T add_intnm列出的符號有很多,常見的有三種:1) 一種是在庫中被調用,但并沒有在庫中定義(表明需要其他庫支持),用U表示;
2) 一種是庫中定義的函數,用T表示,這是最常見的;
3) 一種是所謂的弱態”符號,它們雖然在庫中被定義,但是可能被其他庫中的同名符號覆蓋,用W表示。
2、ldd命令
ldd命令,可以查看一個可執行程序依賴的共享庫。 //查看靜態庫,錯誤 #ldd libadd.a 不是動態可執行文件//查看共享庫 # ldd libadd.so linux-gate.so.1 => (0xb7772000)libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75b6000)/lib/ld-linux.so.2 (0xb7773000)3、strip 命令
去除目標文件、可執行文件、靜態庫和共享庫中的符號表、調試信息等。# strip add參看:linux下strip的用法strip經常用來去除目標文件中的一些符號表、調試符號表信息,以減小程序的大小,在rpmbuild包的最后就用到。
用法:strip <選項> 輸入文件
從文件中刪除符號和節
?選項為:
-I --input-target=<bfdname> Assume input file is in format <bfdname>-O --output-target=<bfdname> Create an output file in format <bfdname>-F --target=<bfdname> Set both input and output format to <bfdname>-p --preserve-dates Copy modified/access timestamps to the output-R --remove-section=<name> Remove section <name> from the output-s --strip-all Remove all symbol and relocation information-g -S -d --strip-debug Remove all debugging symbols & sections--strip-unneeded Remove all symbols not needed by relocations--only-keep-debug Strip everything but the debug information-N --strip-symbol=<name> Do not copy symbol <name>-K --keep-symbol=<name> Do not strip symbol <name>--keep-file-symbols Do not strip file symbol(s)-w --wildcard Permit wildcard in symbol comparison-x --discard-all Remove all non-global symbols-X --discard-locals Remove any compiler-generated symbols-v --verbose List all object files modified-V --version Display this program's version number-h --help Display this output--info List object formats & architectures supported-o <file> Place stripped output into <file>strip: 支持的目標: elf32-i386 a.out-i386-linux efi-app-ia32 elf32-little elf32-big elf64-alpha ecoff-littlealpha elf64-little elf64-big elf32-littlearm elf32-bigarm elf32-hppa-linux elf32-hppa elf64-ia64-little elf64-ia64-big efi-app-ia64 elf32-m68k a.out-m68k-linux elf32-powerpc aixcoff-rs6000 elf32-powerpcle ppcboot elf64-powerpc elf64-powerpcle aixcoff64-rs6000 elf32-s390 elf64-s390 elf32-sparc a.out-sparc-linux elf64-sparc a.out-sunos-big elf64-x86-64 pe-i386 pei-i386 srec symbolsrec tekhex binary ihex trad-core目標文件分為:可重定位文件、可執行文件、共享文件
strip的默認選項會去除.symbol節的內容以及.debug節的內容,因此盡量只對可執行文件執行strip而不要對靜態庫或動態庫等目標文件strip。
測試:生成目標文件 add.o main.o: gcc -c add.c main.c 生成靜態庫文件 libadd.a: ar -r libadd.a add.o 生成可執行文件 add_a: gcc main.o libadd.a -o add_a 生成目標文件 add.o main.o: gcc -c -fpic add.c main.c 生成共享庫文件 libadd.so: gcc -shared add.o -o libadd.so 生成可執行文件add_so: gcc main.o libadd.so -o add_so 做備份,使用strip指令 strip add_a add_so libadd.a libadd.so main.o add.o # ls -l 總用量 84 -rwxr-xr-x 1 root root 5516 Mar 20 10:38 add_a -rwxr-xr-x 1 root root 7206 Mar 20 10:33 add_a_bak -rw-r--r-- 1 root root 80 Mar 17 14:23 add.c -rw-r--r-- 1 root root 147 Mar 17 14:23 add.h -rw-r--r-- 1 root root 596 Mar 20 10:38 add.o -rw-r--r-- 1 root root 860 Mar 20 10:38 add.o_bak -rwxr-xr-x 1 root root 5520 Mar 20 10:38 add_so -rwxr-xr-x 1 root root 7188 Mar 20 10:34 add_so_bak -rw-r--r-- 1 root root 728 Mar 20 10:38 libadd.a -rw-r--r-- 1 root root 1004 Mar 20 10:34 libadd.a_bak -rwxr-xr-x 1 root root 5356 Mar 20 10:38 libadd.so -rwxr-xr-x 1 root root 6654 Mar 20 10:34 libadd.so_bak -rw-r--r-- 1 root root 123 Mar 20 10:20 main.c -rw-r--r-- 1 root root 876 Mar 20 10:38 main.o -rw-r--r-- 1 root root 1416 Mar 20 10:34 main.o_bak
選項簡釋:
The -fPIC flag directs the compiler to generate position independent code section).
The -shared flag directs the linker to create a shared object file.
可見無論是靜態庫 (libadd.a) 還是動態庫 (libadd.so) 還是可執行文件(add_a、add_so),去掉一些符號信息后都減小了很多,但如果這時再鏈接這兩個庫的話是編不過的,因此,如果不是指定特殊的 strip 選項的話,還是盡量不要對庫文件 strip,只對鏈接后的可執行文件 strip 就可以了 (如果也不調試) 。
簡而言之,strip 和 -fpic 正好相反 ,一個是去除符號信息,一個為添加符號信息。4、ldconfig 命令
用專門的配置文件管理共享庫的搜索路徑。事先將共享庫的路徑信息寫入 /etc/ld.so.conf 配置文件中。執行 ldconfig 命令,將 /etc/ld.so.conf 配置文件轉換為 /etc/ld.so.cache 緩沖文件,并將后者加載到系統內存中,借以提高共享庫的搜索和加載速度。每次系統啟動時都會自動執行 ldconfig 命令。如果修改了共享庫配置文件 /etc/ld.so.conf,則需要手動執行 ldconfig 命令,更新緩沖文件并重新加載到系統內存。5、ls -l 命令
ls -l命令,查看庫大小。 # ls -l libadd.* -rw-r--r-- 1 root root 1004 Mar 17 15:24 libadd.a -rwxr-xr-x 1 root root 6654 Mar 17 15:24 libadd.so 查看屬性,也可以得到:七、Windows下靜態庫和動態庫操作
參看:程序員的自我修養--鏈接、裝載與庫(高清帶完整書簽版).pdf? 在此不作講解。八、感言
花了兩天半總結完,靜態庫和動態庫。一開始真沒覺得能講這么多東西,越發感到對之前培訓時淺嘗輒止的慚愧。 踏實一點吧,深入學習,才是關鍵!總結
以上是生活随笔為你收集整理的UNIX再学习 -- 静态库与共享库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: UNIX再学习 -- 环境变量
- 下一篇: UNIX再学习 -- 错误和警告