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