http://blog.csdn.net/wjlkoorey/article/details/7345135
今天我們主要來說說Linux系統下基於動態庫(.so)和靜態(.a)的程式那些陷阱。在這之前,我們需要瞭解一下原始碼到可執行程式之間到底發生了什麼神奇而美妙的事情。
在Linux作業系統中,普遍使用ELF格式作為可執行程式或者程式產生過程中的中間格式。ELF(Executable and Linking Format,可執行串連格式)是UNIX系統實驗室(USL)作為應用程式二進位介面(Application BinaryInterface,ABI)而開發和發布的。工具介面標準委員會(TIS)選擇了正在發展中的ELF標準作為工作在32位Intel體繫上不同作業系統之間可移植的二進位檔案格式。本文不對ELF檔案格式及其組成做太多解釋,以免沖淡本文的主題,大家只要知道這麼個概念就行。以後再詳解Linux中的ELF格式。原始碼到可執行程式的轉換時需要經曆如所示的過程:
l 編譯是指把用進階語言編寫的程式轉換成相應處理器的組合語言程式的過程。從本質上講,編譯是一個文本轉換的過程。對嵌入式系統而言,一般要把用C語言編寫的程式轉換成處理器的彙編代碼。編譯過程包含了C語言的文法解析和彙編碼的產生兩個步驟。編譯一般是逐個檔案進行的,對於每一個C語言編寫的檔案,可能還需要進行預先處理。
l 彙編是從組合語言程式產生目標系統的二進位代碼(機器代碼)的過程。機器代碼的產生和處理器有密切的聯絡。相對於編譯過程的文法解析,彙編的過程相對簡單。這是因為對於一款特定的處理器,其組合語言和二進位的機器代碼是一一對應的。彙編過程的輸入是彙編代碼,這個彙編代碼可能來源於編譯過程的輸出,也可以是直接用組合語言書寫的程式。
l 串連是指將彙編產生的多段機器程式碼群組合成一個可執行程式。一般來說,通過編譯和彙編過程,每一個源檔案將產生一個目標檔案。連接器的作用就是將這些目標檔案組合起來,組合的過程包括了程式碼片段、資料區段等部分的合并,以及添加相應的檔案頭。
GCC是Linux下主要的程式產生工具,它除了編譯器、彙編器、連接器外,還包括一些協助工具輔助。在下面的分析過程中我會教大家這些工具的基本使用方法,Linux的強大之處在於,對於不太懂的命令或函數,有一個很強大的“男人”時刻stand by your side,有什麼不會的就去命令列終端輸入:man [命令名或函數名],然後阿拉神燈就會顯靈了。
對於最後編譯出來的可執行程式,當我們執行它的時候,作業系統又是如何反應的呢?我們先從宏觀上來個總體把握,2所示:
作為UNIX作業系統的一種,Linux的作業系統提供了一系列的介面,這些介面被稱為系統調用(System Call)。在UNIX的理念中,系統調用"提供的是機制,而不是策略"。C語言的庫函數通過調用系統調用來實現,庫函數對上層提供了C語言庫檔案的介面。在應用程式層,通過調用C語言庫函數和系統調用來實現功能。一般來說,應用程式大多使用C語言庫函數實現其功能,較少使用系統調用。
那麼最後的可執行檔到底是什麼樣子呢?前面已經說過,這裡我們不深入分析ELF檔案的格式,只是給出它的一個結構圖和一些簡單的說明,以方便大家理解。
ELF檔案格式包括三種主要的類型:可執行檔、可重新導向檔案、共用庫。
1.可執行檔(應用程式)
可執行檔包含了代碼和資料,是可以直接啟動並執行程式。
2.可重新導向檔案(*.o)
可重新導向檔案又稱為目標檔案,它包含了代碼和資料(這些資料是和其他重定位檔案和共用的object檔案一起串連時使用的)。
*.o檔案參與程式的串連(建立一個程式)和程式的執行(運行一個程式),它提供了一個方便有效方法來用並行的視角看待檔案的內容,這些*.o檔案的活動可以反映出不同的需要。
Linux下,我們可以用gcc -c編譯源檔案時可將其編譯成*.o格式。
3.共用檔案(*.so)
也稱為動態庫檔案,它包含了代碼和資料(這些資料是在串連時候被連接器ld和運行時動態連接器使用的)。動態連接器可能稱為ld.so.1,libc.so.1或者 ld-linux.so.1。我的CentOS6.0系統中該檔案為:/lib/ld-2.12.so
一個ELF檔案從連接器(Linker)的角度看,是一些節的集合;從程式載入器(Loader)的角度看,它是一些段(Segments)的集合。ELF格式的程式和共用庫具有相同的結構,只是段的集合和節的集合上有些不同。
那麼到底什麼是庫呢?
庫從本質上來說是一種可執行代碼的二進位格式,可以被載入記憶體中執行。庫分靜態庫和動態庫兩種。
靜態庫:這類庫的名字一般是libxxx.a,xxx為庫的名字。利用靜態函數庫編譯成的檔案比較大,因為整個函數庫的所有資料都會被整合進目標代碼中,他的優點就顯而易見了,即編譯後的執行程式不需要外部的函數庫支援,因為所有使用的函數都已經被編譯進去了。當然這也會成為他的缺點,因為如果靜態函數庫改變了,那麼你的程式必須重新編譯。
動態庫:這類庫的名字一般是libxxx.M.N.so,同樣的xxx為庫的名字,M是庫的主要版本號,N是庫的副版本號碼。當然也可以不要版本號碼,但名字必須有。相對於靜態函數庫,動態函數庫在編譯的時候並沒有被編譯進目標代碼中,你的程式執行到相關函數時才調用該函數庫裡的相應函數,因此動態函數庫所產生的可執行檔比較小。由於函數庫沒有被整合進你的程式,而是程式運行時動態申請並調用,所以程式的運行環境中必須提供相應的庫。動態函數庫的改變並不影響你的程式,所以動態函數庫的升級比較方便。linux系統有幾個重要的目錄存放相應的函數庫,如/lib
/usr/lib。
當要使用靜態程式庫時,連接器會找出程式所需的函數,然後將它們拷貝到執行檔案,由於這種拷貝是完整的,所以一旦串連成功,靜態程式庫也就不再需要了。然而,對動態庫而言,就不是這樣。動態庫會在執行程式內留下一個標記指明當程式執行時,首先必須載入這個庫。由於動態庫節省空間的,linux下進行串連的預設操作是首先串連動態庫,也就是說,如果同時存在靜態和動態庫,不特別指定的話,將與動態庫相串連。
OK,有了這些知識,接下來大家就可以弄明白我所做的事情是幹什麼了。都說例子是最好老師,我們就從例子入手。
1、靜態連結庫
我們先製作自己的靜態連結庫,然後再使用它。製作靜態連結庫的過程中要用到gcc和ar命令。
準備兩個庫的源碼檔案st1.c和st2.c,用它們來製作庫libmytest.a,如下:
靜態庫檔案libmytest.a已經產生,用file命令查看其屬性,發現它確實是歸檔壓縮檔。用ar -t libmytest.a可以查看一個靜態庫包含了那些obj檔案:
接下來我們就寫個測試程式來調用庫libmytest.a中所提供的兩個介面print1()和print2()。
看到沒,靜態庫的編寫和調用就這麼簡單,學會了吧。這裡gcc的參數-L是告訴編譯器庫檔案的路徑是目前的目錄,-l是告訴編譯器要使用的庫的名字叫mytest。
2、動態庫
靜態庫*.a檔案的存在主要是為了支援較老的a.out格式的可執行檔而存在的。目前用的最多的要數動態庫了。
動態庫的尾碼為*.so。在Linux發行版中大多數的動態庫基本都位於/usr/lib和/lib目錄下。在開發和使用我們自己動態庫之前,請容許我先落裡羅嗦的跟大家嘮叨嘮叨Linux下和動態庫相關的事兒吧。
有時候當我們的應用程式無法運行時,它會提示我們說它找不到什麼樣的庫,或者哪個庫的版本又不合它胃口了等等之類的話。那麼應用程式它是怎麼知道需要哪些庫的呢?我們前面已幾個學了個很棒的命令ldd,用就是用來查看一個檔案到底依賴了那些so庫檔案。
Linux系統中動態連結程式庫的設定檔一般在/etc/ld.so.conf檔案內,它裡面存放的內容是可以被Linux共用的動態聯庫所在的目錄的名字。我的系統中,該檔案的內容如下:
然後/etc/ld.so.conf.d/目錄下存放了很多*.conf檔案,如下:
其中每個conf檔案代表了一種應用的庫配置內容,以mysql為例:
如果您是和我一樣裝的CentOS6.0的系統,那麼細心的讀者可能會發現,在/etc目錄下還存在一個名叫ld.so.cache的檔案。從名字來看,我們知道它肯定是動態連結程式庫的什麼快取檔案。
對,您說的一點沒錯。為了使得動態連結程式庫可以被系統使用,當我們修改了/etc/ld.so.conf或/etc/ld.so.conf.d/目錄下的任何檔案,或者往那些目錄下拷貝了新的動態連結程式庫檔案時,都需要運行一個很重要的命令:ldconfig,該命令位於/sbin目錄下,主要的用途就是負責搜尋/lib和/usr/lib,以及設定檔/etc/ld.so.conf裡所列的目錄下搜尋可用的動態連結程式庫檔案,然後建立處動態載入程式/lib/ld-linux.so.2所需要的串連和(預設)快取檔案/etc/ld.so.cache(此檔案裡儲存著已經排好序的動態連結程式庫名字列表)。
也就是說:當使用者在某個目錄下面建立或拷貝了一個動態連結程式庫,若想使其被系統共用,可以執行一下"ldconfig目錄名"這個命令。此命令的功能在於讓ldconfig將指定目錄下的動態連結程式庫被系統共用起來,即:在快取檔案/etc/ld.so.cache中追加進指定目錄下的共用庫。請注意:如果此目錄不在/lib,/usr/lib及/etc/ld.so.conf檔案所列的目錄裡面,則再次單獨運行ldconfig時,此目錄下的動態連結程式庫可能不被系統共用了。單獨運行ldconfig時,它只會搜尋/lib、/usr/lib以及在/etc/ld.so.conf檔案裡所列的目錄,用它們來重建/etc/ld.so.cache。
因此,等會兒我們自己開發的共用庫就可以將其拷貝到/lib、/etc/lib目錄裡,又或者修改/etc/ld.so.conf檔案將我們自己的庫路徑添加到該檔案中,再執行ldconfig命令。
非了老半天功夫,終於把基礎打好了,猴急的您早已按耐不住激情的想動手嘗試了吧!哈哈。。。OK,說整咱就開整,接下來我就帶領大家一步一步來開發自己的動態庫,然後教大家怎麼去使用它。
我們有一個標頭檔my_so_test.h和三個源檔案test_a.c、test_b.c和test_c.c,將他們製作成一個名為libtest.so的動態連結程式庫檔案:
OK,萬事俱備,只欠東風。如何將這些檔案編譯成一個我們所需要的so檔案呢?可以分兩步來完成,也可以一步到位:
方法一:
1、先產生目標.o檔案:
2、再產生so檔案:
-shared該選項指定產生動態串連庫(讓連接器產生T類型的匯出符號表,有時候也產生弱串連W類型的匯出符號),不用該標誌外部程式無法串連。相當於一個可執行檔。
-fPIC:表示編譯為位置獨立的代碼,不用此選項的話編譯後的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正程式碼片段共用的目的。
方法二:一步到位。
至此,我們製作的動態庫檔案libtest.so就算大功告成了。
接下來,就是如何使用這個動態庫了。動態連結程式庫的使用有兩種方法:既可以在運行時對其進行動態連結,又可以動態載入在程式中是用它們。接下來,我就這兩種方法分別對其介紹。
+++動態庫的使用+++
用法一:動態連結。
使用“-ltest”標記來告訴GCC驅動程式在串連階段引用共用函數庫libtest.so。“-L.”標記告訴GCC函數庫可能位於目前的目錄。否則GNU連接器會尋找標準系統函數目錄。
這裡我們注意,ldd的輸出它說我們的libtest.so它沒找到。還記得我在前面動態連結程式庫一節剛開始時的那堆嘮叨麼,現在你應該很明白了為什麼了吧。因為我們的libtest.so既不在/etc/ld.so.cache裡,又不在/lib、/usr/lib或/etc/ld.so.conf所指定的任何一個目錄中。怎麼辦?還用我告訴你?管你用啥辦法,反正我用的ldconfig
`pwd`搞定的:
執行結果如下:
偶忍不住又要羅嗦一句了,相信俺,我的嘮叨對大家是有好處。我為什麼用這種方法呢?因為我是在給大家示範動態庫的用法,完了之後我就把libtest.so給刪了,然後再重構ld.so.cache,對我的系統不會任何影響。倘若我是開發一款軟體,或者給自己的系統DIY一個非常有用的功能模組,那麼我更傾向於將libtest.so拷貝到/lib、/usr/lib目錄下,或者我還有可能在/usr/local/lib/目錄下建立一檔案夾xxx,將so庫拷貝到那兒去,並在/etc/ld.so.conf.d/目錄下建立一檔案mytest.conf,內容只有一行“/usr/local/lib/xxx/libtest.so”,再執行ldconfig。如果你之前還是不明白怎麼解決那個“not
found”的問題,那麼現在總該明白了吧。
方法二:動態載入。
動態載入是非常靈活的,它依賴於一套Linux提供的標準API來完成。在來源程式裡,你可以很自如的運用API來載入、使用、釋放so庫資源。以下函數在代碼中使用需要包含標頭檔:dlfcn.h
函數原型 |
說明 |
const char *dlerror(void) |
當動態連結程式庫操作函數執行失敗時,dlerror可以返回出錯資訊,傳回值為NULL時表示操作函數執行成功。 |
void *dlopen(const char *filename, int flag) |
用於開啟指定名字(filename)的動態連結程式庫,並返回操作控制代碼。調用失敗時,將返回NULL值,否則返回的是操作控制代碼。 |
void *dlsym(void *handle, char *symbol) |
根據動態連結程式庫操作控制代碼(handle)與符號(symbol),返回符號對應的函數的執行代碼地址。由此地址,可以帶參數執行相應的函數。 |
int dlclose (void *handle) |
用於關閉指定控制代碼的動態連結程式庫,只有當此動態連結程式庫的使用計數為0時,才會真正被系統卸載。2.2在程式中使用動態連結程式庫函數。 |
dlsym(void *handle, char *symbol)
filename:如果名字不以“/”開頭,則非絕對路徑名,將按下列先後順序尋找該檔案。
(1)使用者環境變數中的LD_LIBRARY值;
(2)動態連結緩衝檔案/etc/ld.so.cache
(3)目錄/lib,/usr/lib
flag表示在什麼時候解決未定義的符號(調用)。取值有兩個:
1) RTLD_LAZY : 表明在動態連結程式庫的函數代碼執行時解決。
2) RTLD_NOW :表明在dlopen返回前就解決所有未定義的符號,一旦未解決,dlopen將返回錯誤。
dlsym(void *handle, char *symbol)
dlsym()的用法一般如下:
void(*add)(int x,int y); /*說明一下要調用的動態函數add */
add=dlsym("xxx.so","add"); /* 開啟xxx.so共用庫,取add函數地址 */
add(89,369); /* 帶兩個參數89和369調用add函數 */
看我出招:
執行結果:
使用動態連結程式庫,來源程式中要包含dlfcn.h標頭檔,寫程式時注意dlopen等函數的正確調用,編譯時間要採用-rdynamic選項與-ldl選項(不然編譯無法通過),以產生可調用動態連結程式庫的執行代碼。
OK,通過本文的指導、練習相信各位應該對Linux的庫機制有了些許瞭解,最主要的是會開發使用庫檔案了。由於本人知識所限,文中某些觀點如果不到位或理解有誤的地方還請各位個人不吝賜教。
【轉載請註明出處】