《程式設計師的自我修養 鏈結 裝載與庫》 鏈結

2022-07-13 17:39:12 字數 3682 閱讀 1525

對於平常的應用程式開發,我們很少需要關注編譯和鏈結過程,因為通常的開發環境都是流行的整合開發環境(ide),比如visual studio、myeclipse等。這樣的ide一般都將編譯和鏈結的過程一步完成,通常將這種編譯和鏈結合併在一起的過程稱為構建,即使使用命令列來編譯乙個源**檔案,簡單的一句」gcc hello.c」命令就包含了非常複雜的過程。然而,正是因為整合開發環境的強大,很多系統軟體的執行機制與機理被掩蓋,其程式的很多莫名其妙的錯誤讓我們無所適從,面對程式執行時種種效能瓶頸我們束手無策。我們看到的是這些問題的現象,但是卻很難看清本質,所有這些問題的本質就是軟體執行背後的機理及支撐軟體執行的各種平台和工具,如果能深入了解這些機制,那麼解決這些問題就能夠游刃有餘,收放自如了。

現在我們通過乙個c語言的經典例子,來具體了解一下這些機制:

#include int main()
在linux下只需要乙個簡單的命令(假設源**檔名為hello.c):

$ gcc –e hello.c –o hello.i
或者,

$ cpp hello.c > hello.i
預編譯過程主要處理源**檔案中以」#」開頭的預編譯指令。比如」#include」、」#define」等,主要處理規則如下:

編譯過程就是把預處理完的檔案進行一系列詞法分析、語法分析、語義分析及優化後生產相應的彙編**檔案,此過程是整個程式構建的核心部分,也是最複雜的部分之一。其編譯過程相當於如下命令:

$ gcc –s hello.i –o hello.s
gcc是好多後台程式的包裝,它會根據不同的引數要求去呼叫預編譯程式cc1、彙編器as、鏈結器ld。

彙編器是將彙編**轉變成機器可以執行的指令,每乙個彙編語句幾乎對應一條機器令。所以彙編器的彙編過程相對於編譯器來講比較簡單,它沒複雜的語法,也沒有語義,也不需要做指令優化,只是根據彙編指令和機器指令的對照表一一翻譯就可以了。其彙編過程相當於如下命令:

as hello.s –o hello.o
或者,

gcc –c hello.s –o hello.o
或者使用gcc命令從c源**檔案開始,經過預編譯、編譯和彙編直接輸出目標檔案:

gcc –c hello.c –o hello.o
鏈結通常是乙個讓人比較費解的過程,為什麼彙編器不直接輸出可執行檔案而是輸出乙個目標檔案呢?為什麼要鏈結?下面讓我們來看看怎麼樣呼叫ld才可以產生乙個能夠正常執行的hello world程式:

$ ld –static /usr/lib/crt1.o /usr/lib/crti.o

/usr/lib/gcc/i486-linux-gnu/4.3.1/crtbegint.o

-l/usr/lib/gcc/i486-linux-gnu/4.3.1 –l/usr/lib/ -l/lib hello.o –start-group

-lgcc –lgcc_eh –lc –end-group /usr/lib/gcc/i486-linux-gnu/4.3.1/crtend.o

/usr/lib/ctrn.o

如果把所有的路徑都省略掉,那麼上面的命令:

ld –static crti.o crtbegint.o hello.o –start-group -lgcc –lgcc_eh –lc –end-group crtend.o ctrn.o
可以看到,我們需要將一大堆檔案鏈結起來才可以得到「a.out」,即最終的可執行檔案。看到這麼複雜的命令,你可能會問,這些.o檔案是什麼?它們做什麼用的?–lgcc_eh –lc –end-group這些又是些什麼引數?為什麼要使用它們?為什麼要將它們和hello.o鏈結起來才可能得到可執行檔案?等等。

現在我們一起在探索他們的條案吧。

在現代軟體開發過程中,軟體的規模往往都很大,動輒數百萬行**,如果都放在乙個模組肯定是無法想象的。所以現代的大型軟體往往擁有成千上萬個模組,這些模組之間相互依賴又相對獨立。這種層次化及模組化儲存和組織源**有很多好處,比如**更容易閱讀、理解、重用,每個模組可以單獨開發、編譯、測試,改變部分**不需要編譯整個程式等。

在乙個程式被分割成多個模組以後,這些模組之間最後如何組合形成乙個單一的程式是須解決的問題。模組之間如何組合的問題可以歸結為模組之間如何通訊的問題,最常見的屬於靜態語言的c/c++模擬之間通訊有兩種方式:

函式訪問須知道目標函式的位址,變數訪問也須知道目標變數的位址,所以,這兩種方式都可以歸結為一種方式,那就是模組間符號的引用。模組間依靠符號來通訊類似於拼圖版,定義符號的模組多出一塊區域,引用該符號的模組剛好少了那一塊區域,兩者一拼接剛好完美組合。這個模組的拼接過程就是鏈結。鏈結的主要內容就是把各個模組之間相互引用的部分都處理好,使得各個模組之間都能夠正確的銜接,也就是把一些指令對其他符號位址的引用加以修正。鏈結過程主要包括了位址和空間分配(address and storage allocation)、符號決議(symbol resolution)和重定位(relocation)等這些步驟。

現在我們舉例解釋一下編譯和鏈結的概念。比如我們在程式模組main.c中使用另外乙個模組func.c中的函式foo()。我們在main.c模組中每一處呼叫foo()的時候都必須確切知道foo()這個函式的位址,但是由於每個模組都是單獨編譯的,在編譯器編譯main.c的時候,它並不知道foo()函式的位址,所以它暫時把這些呼叫foo()的指令的目標位址擱置,等待最後鏈結的時候由鏈結器去將這些指令的目標位址修正。如果沒有鏈結器,須要我們手工把每個呼叫foo()的指令進行修正,則填入正確的foo()函式位址。當func.c模組被重新編譯,foo()函式的位址有可能改變時,那麼我們在main.c中所有使用到foo()的位址的指令將要全部重新調整,這些繁瑣的工作將成為程式設計師的噩夢。使用鏈結器,我們可以直接引用其他模組的函式和全域性變數而無須知道它們的位址,因為鏈結器在鏈結的時候,會根據你所引用的符號foo(),自動去相應的func.c模組查詢foo()位址,然後將main.c模組中所有引用到foo()的指令重新修正,讓它們的目標位址為真正的foo()的指令重新修正,讓它們的目標位址為真正的foo()函式位址。這也就是靜態鏈結的最基本的過程和作用。

在鏈結過程中,對其他定義在目標檔案中的函式呼叫的指令須要被重新調整,對使用其他定義在其他目標檔案的變數來說,也存在同樣的問題。讓我們結合具體的cpu指令來了解這個過程。假設我們有個全域性變數叫做var,它在目標檔案a裡面。我們在目標檔案b裡面要訪問這個全域性變數,比如我們在目標檔案b裡面有這麼一條指令:

mov1 $0x2a, var
這條指令就是給這個var變數賦值0x2a,相當於c語言裡面的語句var = 42。然後我們編譯目標檔案b,得到這條指令機器碼,如圖:

由於在編譯檔案b的時候,編譯器並不知道變數var的目標位址,所以編譯器在沒法確定位址的情況下,將這條mov指令的目標位址置為0,等待鏈結器在將目標檔案a和b鏈結起來的時候,再將其修正。我們假定a和b鏈結後,變數var的位址確定下來為0x1000,那麼鏈結器將會把這個指令的目標位址部分修改成0x1000。這個位址修正的過程也被叫做重定位(relocation),每乙個要被修正的地方叫乙個重定位入口(relocation entry)。重定位所做的就是給程式中每個這樣的絕對位址引用的位置「打補丁」,使它們指向正確的位址。

《程式設計師的自我修養 鏈結 裝載與庫》

先不說別的,就單看書名就知道是什麼意思了。作者的意思是想 演員的自我修養 的作者 斯坦尼斯拉夫斯基 致敬。老斯的那本書我沒看過。但我看這本書的意思就是培養程式設計師的基本素質。你說啥叫基本素質?那就是你能夠了解你編寫的程式的任何乙個執行的細節。就拿乙個簡單的 hello world 來說,它是如何執...

程式設計師的自我修養 鏈結 裝載與庫

第一次接觸 程式設計師的自我修養 的時候,的確懷有一種疑惑的態度的。因為潛意識告訴我 在計算機這一行,更強調的是實踐動手,而 修養的顯然不屬於動手操作類,至少不是太適合我的需求。但是,當我以一種隨意的心態翻閱的時候,我才發現我的判斷是多麼的幼稚!這是一本深入淺出 通俗易懂的權威教材,特別是當我了解到...

Notes 《程式設計師的自我修養 鏈結 裝載與庫》

記錄下每章的知識點,便於以後對著這份知識圖譜,複習和重組。掌握硬體中的核心部件 cpu 記憶體 i o控制晶元 了解cpu核心頻率提公升過程中硬體構架的演進 從bus,到pci isa,再到pci express 搶占式cpu分配方式 cpu由作業系統統一分配,因為cpu分配給每個process的時...