編譯鏈接過程
關於編譯鏈接過程的文章其實有很多,我就不重複造輪子了,直接分享一位大佬的文章作爲開頭了:文章傳送門
編譯的補充
看完大佬的文章後應該對編譯連接用戶程序的過程有了一個較爲完整的認識,但是咧,咱們不是編寫用戶程序而是編寫內核程序,雖然兩者本質是一樣的但在一些細節上是完全不同的,下面就對編寫ZOS內核時不同於編寫用戶程序的地方進行補充說明,如果有不全或者錯誤的地方請留言指出。
關於編譯補充的內容主要是對編譯參數的解釋,如下是我們所使用的編譯參數:
CFLAGS=-c -m32 -ggdb -gstabs+ -fno-stack-protector -fno-builtin -fno-strict-aliasing -O0 -Wall -fno-pic -nostdinc -I include
1.參數-c
這個參數是告訴gcc對於源文件只編譯不鏈接,這樣當我們完成編譯後就會得到*.o文件,這些未經鏈接的文件叫object文件,最後將所有object文件一鏈接就得到了我們的內核。
2.參數-m32
這是告訴gcc將源文件編譯爲32位指令的object文件,這是因爲ZOS是在x86即因特爾IA32架構上運行的內核。
3.參數-ggdb
這是告訴gcc在編譯時將調試信息加入object文件,方便之後調試內核。
4.參數-gstabs+
忘了這是幹嘛的了。。。好像也是調試用的。
5.參數-fno-stack-protector
這是告訴gcc不要棧保護器,這本來是編譯用戶程序時用以保護用戶棧的,但是對於編譯內核好像沒啥幫助,甚至會造成奇怪的BUG,所以既然我們自己編寫內核,那麼我們就應該對內核中所有的數據結構瞭如指掌,不要讓別人修修改改的。
6.參數-fno-builtin
忘了,找到再加。
7.參數-fno-strict-aliasing
這是爲了防止gcc在編譯時進行地址對齊操作,對於用戶程序能提高程序性能,但是對於內核程序則會造成匪夷所思的錯誤,比如你定義了一個7字節的結構體,結果編譯後你會發現它怎麼變成了一個8字節的結構體。
8.參數-O0
這是告訴gcc在編譯時不要優化代碼,這是因爲gcc的優化有時會把一些很有用但是很蠢的代碼給優化掉(比如用於延時的循環),畢竟優化策略是大佬用豐富的經驗總結的,而我們還遠未達到大佬的編程水平。
9.參數-Wall
這是告訴gcc顯示編譯過程中的所有警告,以前我編譯用戶程序都是沒編譯錯誤就行了,根本不看編譯警告,但是現在不行了,我們應該儘量保證內核編譯後沒有任何錯誤和警告信息,畢竟警告就意味着一個定時炸彈,指不定什麼時候就造成什麼BUG呢。
10.參數-fno-pic
這是告訴gcc編譯時不生成位置無關代碼,位置無關代碼是給用戶程序(尤其是動態鏈接庫)用的,因爲用戶程序不知道自己將被加載到內存的哪個位置,但是我們對內核即將加載的位置是胸有成竹的,這個胸有成竹主要體現在鏈接腳本的編寫上,所以就不勞駕gcc生成位置無關代碼了。
11.參數-nostdinc
這是告訴gcc在處理#include命令時不要用標準的頭文件進行處理,這是因爲標準頭文件(比如耳熟能詳的stdio.h)是操作系統給用戶程序準備的,而我們寫的就是操作系統內核,所以我們用到的頭文件就必須全部自己編寫,以免與gcc的標準頭文件產生衝突。
12.參數-I include
這是告訴gcc在處理#include命令時在include這個目錄中查找頭文件,具體目錄名是什麼完全可以由自己決定,不過一般都叫include。
鏈接的補充
對於鏈接過程,編寫內核和編寫用戶程序是不同的,這一點在HelloWorld工程中的鏈接腳本中就完全體現出來,這個鏈接腳本需要我們自己編寫,而鏈接用戶程序則不用,直接用gcc默認的鏈接腳本就可以了,現在我們來看看兩個鏈接腳本有什麼不同,其中的區別就是自己必須手寫鏈接腳本的原因。
gcc默認的鏈接腳本(操作系統:64位deepin,gcc版本:7.3.0):
代碼段 小部件
不知道你有沒有看懂gcc默認的鏈接腳本,反正我是連看都沒看,相比之下咱們HelloWorld內核所使用的鏈接腳本長這樣:
ENTRY(_start)
SECTIONS
{
. = 0x100000;
.text :
{
*(.text);
. = ALIGN(4K);
}
.data :
{
*(.data);
. = ALIGN(4K);
}
}
很明顯是小巫見大巫鴨,但是不管鏈接腳本多複雜,它的作用就是告訴鏈接器應該將可執行程序中每個節放在哪個位置,像是一張可執行程序的藍圖,鏈接器會根據這張藍圖正確擺放各個節直至生成最後的可執行程序。下面我們以HelloWorld的藍圖來說明鏈接腳本的編寫方法。
1.ENTRY(_start)
顧名思義,這條腳本的意思就是生成的可執行程序從哪裏開始執行。這個好理解吧,不管是GRUB加載內核還是操作系統加載用戶程序,加載完都得跳轉到入口處執行嘛,這一句就是告訴鏈接器當前的可執行程序入口地址,更加詳細的入口地址知識在編寫進程管理時會詳細敘述的。
2.SECTIONS{}
這條腳本花括號中包含的就是各種節,其中節的先後順序就是生成後可執行程序中節的先後順序。
3.“."(小圓點)
這個小圓點表示小圓點所處位置的虛擬地址,要理解這個小圓點必須理解虛擬地址和加載地址,這兩個概念在內存管理模塊的編寫中會詳細解釋,而現在你就把它當做一個32位的無符號整型變量就可以了。對應使用的ALIGN()命令表示對齊,小圓點在經過ALIGN(n)命令賦值後,它的值會保證被n所整除,也就是說虛擬地址向n對齊。
4..text{}和.data{}
這兩個腳本意思差不多,這裏以.text{}爲例,它表示生成後的可執行程序中.text節所包含的內容,.data{}同理。
5.*(.text)和*(.data)
這兩個腳本意思也差不多,這裏也以*(.text)爲例,它表示所有需要放進最終可執行程序中的.text節,就是代碼節,*(.data)同理。
有了上述理解後我們來看目前ZOS所使用的鏈接腳本:
ENTRY(_start)
SECTIONS
{
. = 0x100000;
.init.text :
{
*(.init.text);
. = ALIGN(4K);
}
. += 0xc0000000;
.text :AT(ADDR(.text) - 0xc0000000)
{
*(.text);
. = ALIGN(4K);
}
PROVIDE(etext = .);
.rodata :AT(ADDR(.rodata) - 0xc0000000)
{
*(.rodata);
. = ALIGN(4K);
}
PROVIDE(data = .);
.data :AT(ADDR(.data) - 0xc0000000)
{
*(.data);
. = ALIGN(4K);
}
PROVIDE(edata = .);
.bss :AT(ADDR(.bss) - 0xc0000000)
{
*(COMMON);
*(.bss);
. = ALIGN(4K);
}
.stab :AT(ADDR(.stab) - 0xc0000000)
{
*(.stab);
. = ALIGN(4K);
}
.stabstr :AT(ADDR(.stabstr) - 0xc0000000)
{
*(.stabstr);
. = ALIGN(4K);
}
PROVIDE(end = .);
}
這個是寫這篇附錄時正常使用的鏈接腳本,可能以後還會進行補充。可以發現其中又多了不少腳本指令。
1.PROVIDE( name = val );
這條指令表示該鏈接腳本向鏈接器提供一個新的變量,變量名位name,值爲val(可以是常量也可以是變量),在C代碼中需要通過extern來引用這個變量。
2.AT()
這條指令的位置固定,在本內核中是跟在“節名:"之後,表示這個節在內存中的加載地址,更詳細的敘述在內存管理模塊中進行。
3.ADDR(節名)
這條指令表示該節的虛擬地址。
關於鏈接的補充就這麼多吧,以後再有以後再補充吧。