第五章 對鏈接的思考
靜態庫(archive) .a文件
動態庫(shared object) .so文件
編譯器包括預處理器(preprocessor)、語法和語義檢查器(synatatic and semantic checker)、代碼生成器(code generator)、
彙編程序(assembler)、優化器(optimizer)、鏈接器(linker)
如果函數庫的一份拷貝是可執行文件的物理組成部分,那麼這就是靜態鏈接;
如果可執行文件只是包含了文件名,讓載入器在運行時能夠尋找程序需要的函數庫,那麼我們稱之爲動態鏈接。
收集模塊準備執行的三個階段的規範名稱是鏈接、編輯(link-editing)、載入(loading)、運行時鏈接(runtime linking)
靜態鏈接的模塊被鏈接編輯並且被載入以便運行。
動態鏈接的模塊被鏈接編輯後載入,並在運行時進行鏈接以便運行。程序執行時,
在main()函數被調用前,運行時載入器把共享的數據對象載入到進程的地址空間。外部函數被真正調用之前,運行時載入器並不解析它們。
所以即使鏈接了函數庫,如果沒有實際調用,也不會帶來額外開銷。
動態鏈接的優點:
可執行文件的體積非常小
可以更加有效的利用磁盤空間
動態鏈接通過以下2個方面提高性能:
1、動態鏈接可執行文件比功能相同的靜態鏈接可執行文件體積小。它能夠節省磁盤空間和虛擬內存,
因爲函數庫只有在需要時才映射到內存中。以前,避免把函數庫的拷貝綁定到每個可執行文件的唯一方法就是把服務
置於內核中而不是函數庫中,這就帶來可怕的“內核膨脹”。
2、所有動態鏈接到某個特定函數庫的可執行文件在運行時共享該函數庫的一個單獨拷貝。操作系統內核保證映射到內存中
的函數庫可以被所有使用它們的內存共享。這就提供更好的I/O和交換空間利用率。
函數庫被多次鏈接的時候,靜態鏈接將佔用更多的物理內存。
動態鏈接使得函數庫的版本升級更爲容易。新的函數庫可以隨時發佈,只要安裝在系統中,舊的程序就能夠自動獲得新版本函數庫的優點,
而無需從新鏈接。
動態鏈接允許用戶用戶在運行時選擇需要的函數庫。用戶可以根據自己的喜好,在程序執行用一個庫文件取代取代另一個庫文件
動態鏈接是一種just-in-time的鏈接,這意味着程序在運行時必須要找到它所需要的函數庫。鏈接器把庫文件名或者文件路徑植入可執行文件
來完成這一點。這意味着函數庫的路徑不能隨意移動。
-L/home/linden -R/home/linden -L和-R分別告訴鏈接器在鏈接時和運行時從哪個目錄尋找需要的函數庫。
·與提取動態庫的符號相比,靜態庫中的符號提取的方法限制更嚴
archive(靜態庫)和shared object(動態庫)的動作不同。
在動態鏈接中,所有的庫符號進入輸出文件的虛擬地址空間中,所有的符號對於鏈接在一起的所有文件都是可見的。
相反,對於靜態鏈接,在處理archive時,它只是在archive時,它只是在archive中查找載入器當時所知道的未定義符號。
簡而言之,在編譯器命令行中各個靜態鏈接庫的出現順序是非常重要的。鏈接器會被“函數庫是在哪裏找到的”,“它是以什麼次序出現”
之類的問題,搞得手忙腳亂,以爲符號是從左到右進行解析的。如果是相同的符號在不同函數庫中,將會有不同的解釋。
同名庫的鏈接優先問題
同一個庫如果同時存在動態庫和靜態庫,優先鏈接動態庫,除非使用--static強制使用靜態庫。
第6章 運動時數據結構(運動的詩章)
1、 a.out assembler output(彙編程序輸出)的縮寫。
這是由於歷史原因,之前是所有的源文件連接在一起,然後進行彙編,而彙編的輸出保存在a.out中
實際上a.out是由鏈接器輸出。
缺省使用a.out這個名字是UNIX“沒什麼理由,但我們就是這樣做的”思維的一種體現
2、目標文件和可執行文件可以有幾種不同的格式。但是所有不同格式都是段(segments)。
就目標文件而言,它們是二進制文件中簡單的區域,裏面保存中和某種特定類型相關的所有信息。
section是ELF文件的最小組織單位(ELF:Extension Linker Format,可擴展鏈接器格式)
一個段(segment)一般包含幾個section。
!!不要跟InterX86中段的概念混淆。!!
在UNIX中,段表示一個二進制文件相關的內容塊。
當在一個可執行文件中運行size命令時,它會告訴你這個文件中的三個段(文本段、數據段、bss段)
%echo; echo "text data bss total" ;size a.out
text data bss total
1548 4236 4004 = 9788
爲何a.out要以段的形式組織呢?
段可以方便地映射到鏈接器在運行時可以直接載入的對象。
載入器只是去文件中每個段的映像,並直接將它們放入內存中。
段在正在執行的程序中(即進程)中是一塊內存區域。每個區域都有特定的目的。
文本段包含程序指令,直接拷貝到內存中運行。(該段屬性爲 read-and-execute only)
數據段包含經過初始化的全局和靜態變量以及它們的值。
BSS段的大小從可執行文件中得到,然後鏈接器得到這個大小的內存塊。
堆棧段,用於保存局部變量、臨時數據、傳遞到函數中的參數等。
同時,堆空間(heap)用於動態內存的分配。
“C語言運行時的函數非常少,且個個短小精悍。相反的例子是C++。”
跟蹤調用鏈是C語言提供的服務之一。
哪些函數調用了哪些函數?當下一個return語句執行後,控制將返回何處?
怎麼解決上述的2個問題呢?經典機制:堆棧中的過程活動記錄
過程活動記錄:一種數據結構,用於支持過程調用,並記錄調用結束後返回調用點所需要的全部信息。
局部變量(local varibales) |
參數(argument) |
靜態鏈接(static link) |
指向先前結構的指針 |
返回地址(return address) |
對堆棧怎麼實現函數的調用的描述也同時解釋了爲什麼不能從函數返回一個指向該函數局部變量的指針。
char * favorite_fruit()
{
char deciduous[] = "apple";
return deciduous;
}
·當進入該函數時,自動變量decious在堆棧中分配。當函數結束後,變量所佔用的堆棧空間被回收,可能在任何時候被覆蓋。這樣,指針就失去有效性,(引用不存在的東西)
成爲一個懸垂指針(danging pointer)。如果想返回一個指向在函數內部定義的變量的指針時,要把那個變量聲明爲static。這樣就能保證變量保存在數據段中而不是堆棧中。
·auto在實際中用不到。(但是在C++11中, auto表示自動匹配類型,是一種重要應用)