當(dāng)前位置:首頁 > 嵌入式培訓(xùn) > 嵌入式學(xué)習(xí) > 講師博文 > 簡析靜態(tài)庫與動態(tài)庫
一、庫的簡介
當(dāng)今程序員的程序開發(fā)流程與50年前對比可謂是發(fā)生了翻天覆地的變化:50年前,那些“上古時期”的大神們沒有簡便的可視化操作系統(tǒng),沒有詳 盡的API文檔,沒有方便的面向?qū)ο笳Z言(面向過程語言剛剛興起),甚至連一些當(dāng)今程序員認為某些“天生的”功能(例如C語言的printf函數(shù))都 沒有。50年過去了,當(dāng)今的程序員們可能無法體會過去的大神們編程的艱辛,因為有一種工具包的存在,使得編程大大簡化,讓程序開發(fā)者更多地去 注重程序的邏輯性而不是一些“細枝末節(jié)”。這種工具包就是庫。
庫(library)是一種可執(zhí)行代碼的二進制形式,通常把一些常用的函數(shù)制作成各種函數(shù)庫,然后被系統(tǒng)載入內(nèi)存中運行。庫是許多前輩大神們已 經(jīng)寫好的程序,程序開發(fā)者可以直接來調(diào)用這些功能程序來完成相應(yīng)功能,從而簡化了程序的開發(fā)工作。而且如果不同的應(yīng)用程序調(diào)用同樣的庫,那 么內(nèi)存內(nèi)只需有一份該庫的實例即可,節(jié)省了存儲空間。庫內(nèi)一般都是各種標準程序、子程序、相關(guān)文件以及目錄等的集合,內(nèi)置一些經(jīng)常用的程序 。主要有三種:
標準子程序:例如三角函數(shù)、反三角函數(shù)等
標準程序:例如解常微分方程等
服務(wù)性程序:例如輸入、輸出、磁盤操作、調(diào)試等。
以熟悉的C語言stdio庫為例。stdio庫意為標準輸入輸出庫(standard input & output),該庫內(nèi)集成的是用于控制輸入、輸出、輸出錯誤的相關(guān) 功能函數(shù),例如我們熟悉的fopen()、fclose()、fread()、fwrite()、putchar()、getchar()、printf()、scanf()等函數(shù)都集成在該庫內(nèi)。從C89版 本開始,一般C語言編譯器都會自帶stdio庫,只需我們在程序中包含頭文件stdio.h即可調(diào)用庫內(nèi)的功能函數(shù)。這樣就大大簡化了程序的開發(fā)工作。
Linux系統(tǒng)下的庫分為靜態(tài)庫與動態(tài)庫兩種。二者的不同點主要體現(xiàn)在載入時間的不同(見附圖1)。靜態(tài)庫在程序編譯時的鏈接階段被鏈接到目標代 碼中,運行程序時將不再需要靜態(tài)庫。編譯后的可執(zhí)行程序體積較大。動態(tài)庫在程序編譯時并不會馬上鏈接到目標代碼中,而是在執(zhí)行階段才被程序 載入,因此編譯后的可執(zhí)行程序體積較小,但是需要系統(tǒng)動態(tài)庫存在。
二、靜態(tài)庫簡介與制作
靜態(tài)庫在程序編譯的“鏈接”階段生效。在編譯過程中,若需要加載靜態(tài)庫,則在鏈接階段,編譯器會拷貝一份完整的庫函數(shù)代碼,整合到當(dāng)前正在 編譯的程序中,這樣在編譯完成后庫就被整合到了程序內(nèi)部。這種加載庫的方式稱為“靜態(tài)庫”。由于靜態(tài)庫與程序整合在一起,因此程序體積較大 ,在程序運行時無需二次加載所需的庫,不過庫的更新也變得困難。而且,由于靜態(tài)庫是采取“拷貝”的方式來加載庫,因此無法實現(xiàn)不同進程間的 庫的共享。
那么如何制作一個靜態(tài)庫呢?
在Linux系統(tǒng)中,我們可以使用ar工具制作一個靜態(tài)庫。ar是類似gcc的一個GNU工具包內(nèi)的工具,作用是建立、修改、提取歸檔文件。歸檔文件是包含 多個文件內(nèi)容的一個大文件,被包含文件的原始內(nèi)容、權(quán)限、時間戳、所有者等屬性都保存于歸檔文件中,并且可以通過“提取”來還原該文件。
下面我將制作一個名為libmyhello.a的靜態(tài)庫。
(注意:在Linux系統(tǒng)中,庫文件的文件名一般為libXXX.a或libXXX.so,其中l(wèi)ib表示這是一個庫,.a表示靜態(tài)庫,.so表示動態(tài)庫,XXX為庫名。在 Windows系統(tǒng)中以不同的文件后綴名區(qū)分靜態(tài)庫與動態(tài)庫,其中.lib文件為靜態(tài)庫,.dll文件為動態(tài)庫。)
第一步:準備3個文件:hello.h、hello.c、test.c。其中hello.h和hello.c用于制作靜態(tài)庫,test.c是測試程序主函數(shù)。
第二步:將hello.c編譯生成目標文件hello.o
gcchello.c -c -o hello.o
第三步:使用ar將hello.o制作成靜態(tài)庫
arcrslibmyhello.ahello.o
第三步的參數(shù)解釋:
⒈c:表示無提示方式創(chuàng)建文件包
⒉r:在文件包中替代文件
⒊s:強制重新生成文件包的符號表
此時就生成了文件名為libmyhello.a(庫名為myhello)的靜態(tài)庫。下一步就可以將該靜態(tài)庫鏈接到程序中了。
第四步:編譯test.c,將剛制作的靜態(tài)庫加載至程序內(nèi)
gcctest.c -L. -lmyhello -o hello
其中參數(shù)-L的意思是添加所需增加的庫的路徑,-L.表示增加庫的路徑為當(dāng)前路徑。參數(shù)-l的意思是在鏈接階段尋找該庫并鏈接至程序中。
經(jīng)過以上四步,我們就成功制作了一個靜態(tài)庫并將它成功地添加到程序中。執(zhí)行程序hello即可看到結(jié)果。
并且若我們刪除庫(即libmyhello.a文件),再次執(zhí)行該程序仍然可以得到正確的結(jié)果。這是因為靜態(tài)庫在鏈接階段已經(jīng)和程序整合到一起,即使原 始庫文件不存在,程序依然可以成功執(zhí)行。
三、動態(tài)庫(共享庫)的簡介與制作
靜態(tài)庫在使用過程中有許多的缺點,包括但不限于:庫與程序整合到一起,這樣會使得程序占用空間變大;如果庫需要更新,則需要重新編譯;由于 加載庫是采取拷貝的方式,這樣程序與程序之間沒有實現(xiàn)庫的共享……。
基于以上幾點,我們發(fā)明了動態(tài)庫。與靜態(tài)庫不同的是,動態(tài)庫在鏈接階段并沒有真正的整合到程序內(nèi)部,而是保留了庫的一個“線索”,當(dāng)我們執(zhí) 行該程序時,程序會按照這條“線索”與當(dāng)前系統(tǒng)的環(huán)境變量尋找?guī)斓恼嬲谖恢貌⒓虞d。這樣做的好處是將庫與程序人為分離,這樣便于庫的更 新與維護,同時多個程序間只需保留一份庫的實例即可,無需拷貝庫而浪費內(nèi)存。不過這樣做的缺點就是程序?qū)討B(tài)庫有依賴性,即程序無法脫離庫 而獨立運行。
(如果有玩過(尤其是盜版)游戲的同學(xué),一定遇到過“缺少XXX.dll文件”的問題從而導(dǎo)致游戲無法正確運行。.dll文件即Windows系統(tǒng)下的動態(tài)庫 文件,缺少該文件即使游戲能夠正確地安裝到電腦上,也會因缺少相應(yīng)庫文件而無法執(zhí)行。)
那么如何制作一個動態(tài)庫呢?
我們可以使用gcc直接制作一個自己的動態(tài)庫。
第一步:需要準備3個文件:hello.h、hello.c、test.c。其中hello.h和hello.c用于制作動態(tài)庫,test.c是測試程序主函數(shù)。(代碼與上面相同,略 )
第二步:使用gcc編譯生成動態(tài)庫。
gcchello.c -fPIC -c -o hello.o
gcchello.o -shared -o libmyhello.so
(或者直接寫成一步:gcchello.c -fPIC -shared -o libmyhello.so)
第二步的參數(shù)解釋:
⒈ -fPIC(或-fpic):表示編譯為位置獨立的代碼。位置獨立的代碼即位置無關(guān)代碼,在可執(zhí)行程序加載的時候可以存放在內(nèi)存內(nèi)的任何位置 。若不使用該選項,則編譯后的代碼是位置相關(guān)的代碼,在可執(zhí)行程序加載時仍然是通過代碼拷貝的方式來滿足不同的進程的需要,并沒有實現(xiàn)真正 意義上的位置共享。
⒉ -shared:指定生成動態(tài)鏈接庫。
此時就生成了文件名為libmyhello.so(庫名為myhello)的動態(tài)庫。下一步就可以將該動態(tài)庫鏈接到程序中了。
第三步:編譯test.c,將剛制作的動態(tài)庫加載至程序內(nèi)。
gcctest.c -L. -lmyhello -o hello
此時就生成了可執(zhí)行程序hello。不過,當(dāng)我們執(zhí)行該程序的時候,會發(fā)生錯誤:
錯誤信息為“當(dāng)加載共享庫的時候,無法找到libmyhello.so的位置:沒有該文件或目錄”。
上文說過,動態(tài)庫在鏈接階段并沒有真正地整合到程序中,而是保留了一個指向該庫的“線索”。當(dāng)程序在加載該動態(tài)庫的時候,需要依照線索找到 動態(tài)庫所在的位置。對于Linux系統(tǒng)而言,在可執(zhí)行程序加載動態(tài)庫的時候,不僅要知道該庫的名字,還需要知道其絕對路徑。因此,我們需要再聲明 動態(tài)庫的絕對位置,這樣才能正確地加載動態(tài)庫。
我們可以使用ldd指令查看程序加載庫的情況。在執(zhí)行hello程序的時候,系統(tǒng)無法找到libmyhello.so的絕對路徑,因此無法加載庫。
第四步:定位自己制作的動態(tài)庫。
要想讓自己制作的動態(tài)庫生效,我們需要了解正常情況下系統(tǒng)是如何加載一個動態(tài)庫的。以我們熟悉的stdio庫為例,系統(tǒng)在加載標準輸入輸出庫時有 以下幾個步驟:(見附圖2)
⒈執(zhí)行./hello指令,終端解釋該指令,終端指示應(yīng)加載動態(tài)庫stdio,尋找存放動態(tài)庫的配置文件。
⒉存放動態(tài)庫的配置文件默認目錄為/etc/ld.so.conf.d/以及下屬的眾多子目錄內(nèi)的配置文件。配置文件指示該庫的絕對路徑在/usr/lib或/lib下。
⒊去往/usr/lib或/lib,將存儲的stdio庫加載到程序hello中。
因此我們有三種方法讓自己制作的動態(tài)庫生效:
⒈把自己制作的庫拷貝到/usr/lib和/lib下。
⒉在LD_LIBRARY_PATH環(huán)境變量中添加自己制作的庫所在的位置。
⒊添加/etc/ld.so.conf.d/XXX.conf文件(XXX需要自己命名),把庫所在的路徑添加到文件末尾并執(zhí)行l(wèi)dconfig刷新。
(注意以下三種方法的異同,以及每種方法執(zhí)行時ldd指令所顯示的區(qū)別。)
第一種:將庫拷貝到/usr/lib和/lib下。
sudocp libmyhello.so /usr/lib
sudocp libmyhello.so /lib
此時再執(zhí)行./hello即可得到正確的顯示結(jié)果。
第二種:修改LD_LIBRARY_PATH環(huán)境變量
sudo vim /etc/bash.bashrc
在文件后,添加:
export
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/linux/dongtaiku
保存退出,重啟終端,此時再執(zhí)行./hello即可得到正確的顯示結(jié)果。
第三種:添加/etc/ld.so.conf.d/XXX.conf文件
sudo vim /etc/ld.so.conf.d/my.conf
在文件內(nèi)添加動態(tài)庫的目錄:
/home/linux/dongtaiku
保存退出,執(zhí)行l(wèi)dconfig使設(shè)置生效。
sudoldconfig
此時再執(zhí)行./hello即可得到正確的顯示結(jié)果。
以上就是讓動態(tài)庫生效的三種方法。
四、結(jié)語
庫的存在,使得我們的編程過程大大簡化,程序員可以將更多的精力放在程序的邏輯上。并且,庫的產(chǎn)生直接改變了人們對于編程語言的認識,可以 說間接促進了當(dāng)代許多面向?qū)ο笳Z言的誕生。當(dāng)代的許多語言,例如c++、java、c#、javascript包括近大火的Node.js,都是集合了大量的工具庫 ,庫內(nèi)包含了許多種編程時可能調(diào)用的功能。這樣大大地方便了程序開發(fā)人員。
后留一道思考題給各位:當(dāng)靜態(tài)庫與動態(tài)庫的庫名重名時,系統(tǒng)在加載庫時是以哪個庫為準呢?例如有靜態(tài)庫libmyhello.a和動態(tài)庫libmyhello.so ,在編譯時,都執(zhí)行:
gcctest.c –L. –lmyhello –o hello
這時系統(tǒng)以哪個庫為準呢?