讀Kernel感悟-Linux核心啟動-從hello world說起
核心是從哪裡開始執行的呢?幾乎任何一本Linux核心原始碼分析的書都會給出詳細的答案。不過,我試圖從一個不同的角度(一個初學者的角度)來敘述,而不是一上來就給出答案。從熟悉的事物入手,慢慢接近陌生的事物,這是比較常見的思路。既然都是二進位制程式碼,那麼不妨從最簡單的使用者態C程式,hello world開始。說不定能找到共同點。恰好我是一個喜歡尋根究底的人。也許,理解了hello world程式的啟動過程,有助於更好地理解核心的啟動。
好,開始尋根究底吧。從普通的C語言使用者態程式開始寫。先寫一個簡單的hello world程式。
/*helloworld.c*/
#include <stdio.h>
int main()
{
printf("hello world/n");
return 0;
}
然後gcc helloworld.c -o helloworld,一個最簡單的hello world程式出現了。
它是從哪裡開始執行的呢?這還不簡單?main函式麼。地球人都知道。
為什麼一定要從main函式開始呢?於是,我開始琢磨這個hello world程式。
file helloworld可知,它是一個elf可執行檔案。
反彙編試試。
objdump -d helloworld
反彙編的結果令人吃驚,因為出現了_start()等一堆函式。一定是gcc編譯時預設連結了一些庫函式。
其實,只要執行gcc -v helloworld.c -o helloworld就會顯示gcc詳細的編譯連結過程。其中包括連結/usr/lib/下的crti.o crt1.o crtn.o等等檔案。用objdump檢視,_start()函式就定義在crt1.o檔案中。
那麼helloworld的真正執行的入口在哪裡呢?我們可以使用readelf來檢視,看有沒有有用資訊。
readelf -a helloworld
helloworld作為一個elf檔案,有elf檔案頭,section table和各個section等等。有興趣可以去看看elf檔案格式的文件。
用readelf可知,在helloworld的elf檔案頭的資訊中,有這麼一項資訊:
入口點地址: 0x80482c0
可見,helloworld程式的入口地址在0x80482c0處,而由objdump得:
080482c0 <_start>:
可見,_start()是helloworld程式首先執行的函式。_start()執行完一些初始化工作後,經過層層呼叫,最終呼叫main().可以設想,如果_start()裡最終呼叫的是foo(),那麼C程式的主函式就不再是main(),而是foo()了。
再進一步:helloworld程式具體是如何執行的呢。我們只能猜測是由bash負責執行的。然而具體看bash程式碼就太複雜了。我們可以用strace跟蹤helloworld的執行。
strace ./helloworld
出來一大堆函式呼叫。其中第一個是execve().這是一個關鍵的系統呼叫,它負責載入helloworld可執行檔案並執行。其中有很關鍵的一步,就是把使用者態的eip暫存器(實際上是它在記憶體中對應的值)設定為elf檔案中的入口點地址,也就是_start()。具體可見核心中的sys_execve()函式。
由此可見,程式從哪裡開始執行,取決於在剛開始執行的那一刻的eip暫存器的值。而這個eip是由其它程式設定的,在這裡,eip是由Linux核心設定的。具體過程如下:
1.使用者在shell裡執行./helloworld。
2.shell(這裡是bash)呼叫系統呼叫execve()。
3.execve陷入到核心裡執行sys_execve(),把使用者態的eip設定為_start()。
4.當系統呼叫執行完畢,helloworld程序開始執行時,就從_start()開始執行
5.helloworld程序最後才執行到main()。
參考:elf檔案格式
http://www.skyfree.org/linux/references/ELF_Format.pdf