當前位置:
首頁 > 最新 > 從kernel到Android

從kernel到Android

本篇介紹

看kernel還是需要一些編譯知識的,本篇開始介紹下編譯知識,因為編譯特別複雜,要介紹全部顯然不是幾句話可以做到的,只能挑選出編譯中最相關的部分,然後詳細介紹。現在我們介紹的是鏈接和載入。以後也會介紹編譯的其他內容,比如詞法解析,語義解析,編譯優化等。本篇先介紹下鏈接和載入的歷史知識,然後在後面的文章中慢慢深入。

地址綁定

最開始的時候,計算機的編程用的都是彙編。程序員將程序寫到類似於紙的上面,如果程序有地方引用了其他地方的符號,就需要程序員手動翻譯,告訴計算機這個符號在哪裡。如果寫的程序做了修改或調整的話,就需要再次檢查看哪兒需要隨之調整一下。這樣看上去問題就是符號和對應的地址綁定的太早了,程序員在寫程序時就需要考慮到引用的符號地址。後來彙編器出現了,程序員只要一口氣寫程序就行,然後彙編器幫忙將引用的符號綁定到對應的地址上去。這樣就將地址綁定的工作從程序員轉移到了計算機上了。

地址綁定的問題來源於代碼庫的出現,一個程序往往涉及到很多操作,如果這些操作每次都需要實現一次的話,效率低,麻煩,成了真正的重複造輪子。這時候就可以將常用的操作寫成專門的函數,然後將這些函數匯總成函數庫。這樣在寫程序時,有相似的功能,就可以直接使用函數庫裡面的函數就行。這樣就出現了函數庫。實際上程序員先使用的各種各樣的專門函數,後來才使用了彙編器。在彙編器出現前,人們怎樣使用函數呢?在1947年,John Mauchly (一個美國物理學家,和J Presper設計了ENIAC,這個是第一個一般意義上的電子計算機,一些計算機概念,比如存儲程序,子路徑,編程語言都來源他。(ps: 介紹牛人時總會興奮一下))設計了一段機器語言程序,主要可以做兩件事,可以從記錄程序的卡帶上查找程序和可以在裝載程序時重定位子程序位置。這樣的最大好處就是可以讓程序員各自寫自己的程序,感覺對於他們,地址都是從零開始的,只有在主函數鏈接子程序時才會進行地址綁定。

在有操作系統以前,程序可以看成是擁有計算機的全部內存空間,這時候在鏈接時,地址可以綁定到固定的內存地址上。有了操作系統以後,程序必須和操作系統甚至其他程序共享內存空間。這時候只有在程序被裝載到內存中才會知道真正的符號位置。這時候鏈接和裝載就可以分開承擔不一樣的事情了,鏈接負責部分地址綁定和指定每個程序的相對地址,裝載對程序地址進行重定向到真正的內存地址上。隨著硬體重定向和虛擬內存的出現,程序又可以認為是擁有整個內存空間,程序可以被裝載到固定的地址上,由硬體在裝載時進行重定向。還有一個空間的問題,每個程序可能對應n個運行實例,如果每個運行實例都保存對應的一份程序的話,會造成空間浪費。仔細分析程序,可以將程序記性拆分,有些是只讀的,比如可執行代碼,有些是可讀可寫的,比如數據段。這時候如果將程序按讀寫樹行進行拆分(也叫VMA),讓運行實例共享只讀部分,各自擁有一份可寫段就會節省不少空間。再往後就是共享庫的出現了。共享庫的好處就是節省空間,讓運行的程序共享一份公共程序。共享庫最開始是靜態共享庫,在這種形態下,共享庫裡面符號的地址都是固定的,運行的程序從固定的地址去找就可以,可是只要共享庫裡面有所變動,地址就需要變動,程序也需要按新的地址來。後來就出現了動態庫,可以讓程序在運行時,甚至運行中地址綁定。這樣就方便了程序的功能擴展。

鏈接和裝載

鏈接和裝載的區別是什麼?要做區分,首先要知道以下幾個過程:

程序裝載:從磁碟中拷貝程序到內存中的過程,另外還包括分配存儲空間,設置保護位等;

重定向:在程序編譯的時候,每個目標文件的起始地址都是零,往往一個程序會包含多個目標文件(有的是我們看不見的,比如printf等基礎函數,這些都是在基礎庫裡面,不需要我們手動指定的),重定向就是給程序中使用的其他文件的符號指定一個地址,保證不同符號的地址不會重複。

符號解析:一個程序使用了不在該文件裡面定義的符號時,就需要在其他文件裡面查找符號,然後將位置告訴程序。

鏈接和裝載有些地方是重複的,比如重定向,不過比較合理的劃分是負責程序裝載的就是裝載, 負責符號解析的就是鏈接。

鏈接

鏈接就是讀取輸入的文件,命令行,鏈接控制腳本等,輸出一個可執行文件,還有符號表等的過程。如下圖所示

一般鏈接可以分為兩個過程,第一個過程是掃描輸入文件各個段的大小,還有創建一個表格,包含了每個輸入文件使用的其他地方定義的符號,還有自己定義的可以讓別人使用的符號。第二個過程就是使用第一個過程的結果,對使用的符號用數字地址代替,也就是重定向的過程。並將結果輸出到結果文件中。

可以演示下鏈接過程:

首先我們寫了兩個函數文件:

先進行預處理a.c gcc -E a.c -o a.i

這時候得到了a.i文件,預處理其實就是將a.c裡面的頭文件內容包含進來,替換掉包含的頭文件,也就是預處理後,a.i裡面不會包含任何頭文件,這就預處理結束了。

對a.i進行編譯 gcc -S a.i -o a.s, 得到的a.s文件就是彙編程序了。

對a.s 進行彙編,gcc -c a.s -o a.o,就得到了目標文件a.o,a.o已經是機器碼的形式了。

對b.c進行預處理 gcc -E b.c -o b.i

對b.i進行編譯 gcc -S b.i -o b.s

對b.s進行彙編 gcc -c b.s -o b.o

對a.o,b.o進行鏈接 gcc a.o b.o -o a.out,得到a.out文件

這時候的a.out就是可執行文件了,這就完成了鏈接過程。

我們可以通過使用鏈接腳本完成更多的自定義,可以指定欄位的鏈接順序,合併相似段的規則,也可以自定義段,然後將變數放到自定義的段裡面等等。

我們現在看下a.o,b.o,a.out,用來進一步看下鏈接過程

看下a.o的內容 objdump -x -D a.o,這兒只看下部分內容。

首先是段信息,可以看到a.o裡面有那些段,.text就是代碼段,.data就是數據段,.bss就是Block Started by Symbol,方便記憶的話,可以叫做better save space ,在代碼裡面定義了一些未初始化的全局變數,因為默認是0,就放到了bss裡面,這樣在目標文件中可以不分配空間,載入到內存後在分配空間。Size就是各個欄位的大小,VMA就是虛擬內存地址,可以看到.text是從零開始的。

符號表,從這兒可以看到目標文件裡面包含了哪些符號,引用了哪些符號。這樣如果在編譯過程中遇到unknown reference等的錯誤後,就可以看下鏈接的庫裡面有沒有需要的符號。也可以使用nm命令來看。

代碼段的彙編結果。可以看到開始都是保存棧基址,然後讓棧基址等於棧指針。可以看到在調用fun函數時,地址是0.

看下b.o的信息。objdump -x -D b.o

這兒內容與a.o含義一樣

這兒就是fun對應的彙編程序了,可以看到地址4那行,mov %0x16, %eax,函數結果就通過eax返回了。

現在看下a.out

可以看到VMA已經變成實際需要裝載到內存的地址了。不過這個地址當然是虛擬地址,並不是內存的物理地址。

下面是對應的彙編代碼,可以看到callq調用函數的地址已經被糾正過來了,變成了1e,這是相對地址,可以用下條指令的地址加上1e就可以得到目標函數的地址。400538+001e = 400556。看下400556的地方,果然就是fun函數的地址了。也可以看到返回值是怎樣傳遞的。

本篇總結

鏈接和載入內容特別多,現在只能算介紹了下相關的歷史故事和相關的概念。後面的內容會比較難,不過從現在來看,應該有種感覺,如果掌握這些內容的話,對一個程序員寫代碼,調試代碼會大有幫助。最後留一個問題,對於python等的解釋型語言,鏈接和載入是充當怎樣的角色的?

一個小福利,介紹一個在線的繪圖網站:https://www.processon.com/diagrams


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 溫水煮豆漿 的精彩文章:

TAG:溫水煮豆漿 |