為什麼匯編程序的入口是_start,而C程序的入口是main函數呢?以下就來解釋這個問題
在《x86匯編程序基礎(AT&T語法)》一文中我們匯編和鏈接的步驟是:
$ as hello.s -o hello.o $ ld hello.o -o hello
我們用gcc main.c -o main開編譯一個c程序,其實際分為三個步驟:編譯、匯編、鏈接
$ gcc -S main.c 生成匯編代碼 $ gcc -c main.s 生成目標文件 $ gcc main.o 生成可執行文件
我們先前在《x86匯編程序基礎(AT&T語法)》中由第一個匯編程序生成的目標文件hello.o我們使用ld來鏈接的,那能不能用gcc呢?如下:
報了兩個錯誤:1. _start有多處定義,一個定義是我們匯編代碼中的。另一個定義來自/usr/lib/cr1l.o;2. crt1.o的_start函數要調用main函數,而我們的匯編代碼中沒有提供main函數的定義。最後一行顯示這些錯誤提示是ld報出的。所以如果我們用gcc做鏈接,gcc實際是調用ld將目標文件crt1.o和我們寫的hello.o鏈接在一起。
如果目標文件是由C程序編譯生成的,用gcc做鏈接就沒錯了,整個程序的入口是crtl.o中提供的_start,它首先做一些初始化操作(以下稱為啟動例程,Startup Routine),然後調用C代碼中提供的main函數。_start才是真正的入口點,main是被_start調用的。 我們繼續上一篇文章《[匯編與C語言關系]1.函數調用》研究,gcc main.o -o main其實是調用ld做鏈接的,相當於這樣的命令:
$ ld /usr/lib/crt1.o /usr/lib/crti.o main.o -o main -lc -dynamic-linker /lib/ld-linux.so.2
除了crt1.o之外還有crti.o,這兩個目標文件和我們的hello.o鏈接在一起生成可執行文件main。-lc表示需要鏈接libc庫,-lc選項是gcc默認的,不用寫,而對於ld則不是默認選項,所以要寫上。-dynamic-linker /lib/ld-linux.so.2指定動態鏈接器是/lib/ld-linux.so.2。
我們可以用readelf查看crt1.o和crti.o裡面的內容。在這裡我們只關心符號表,如果只看符號表,可以用readelf命令的-s選項,也可以用nm命令。
$ nm /usr/lib/crt1.o 00000000 R _IO_stdin_used 00000000 D __data_start U __libc_csu_fini U __libc_csu_init U __libc_start_main 00000000 R _fp_hw 00000000 T _start 00000000 W data_start U main $ nm /usr/lib/crti.o U _GLOBAL_OFFSET_TABLE_ w __gmon_start__ 00000000 T _fini 00000000 T _init
U main這一行表示main這個符號在crt1.o中用到了,但是沒有定義(U表示Undefined),因此需要別的目標文件提供一個定義並且和crt1.o鏈接在一起。具體來說,在crt1.o中要用到main這個符號所代表的地址,例如有一條指令是push $符號main所代表的地址, 但不知道這個地址是多少,所以在crt1.o中這條指令暫時寫成$0x0,等到和main.o鏈接成可執行文件時就知道這個地址是多少了,比如是0x80483c4,那麼可執行文件main中的這條指令就被鏈接器改成push $0x80483c4。鏈接器在這裡起到符號解析(Symbol Resolution)的作用。鏈接器還有一種作用就是重定位作用,而鏈接器編輯的是目標文件,所以鏈接器也是一種編輯器,vi等其他編輯器編輯的是源文件,而鏈接器編輯的是目標文件,所以鏈接器也叫Link Editor。T _start這一行表示_start這個符號在crt1.o中提供了定義,這個符號的類型是代碼(T表示Text)。我們從上面的輸出結果中選取幾個符號用圖示說明它們之間的關系:
上邊我們寫的ld命令做了簡化,gcc在鏈接過程中還用到了其他幾個目標文件,所以上圖多畫了一個框,表示組成可執行文件main的除了main.o、crt1.o和crti.o之外還有其他目標文件,gcc -v選項可以了解詳細的編譯過程。
鏈接生成的可執行文件main中包含了各目標文件所定義的符號, 通過反匯編可以看到這些符號的定義:
crt1.o中的未定義符號main在main.o中定義了,所以鏈接在一起就沒有問題了。crt1.o還有一個未定義符號_libc_start_main在其他幾個目標文件中也沒有定義,所以在可執行文件main中仍然是個未定義符號。這個符號是在libc中定義的,libc並不像其他目標文件一樣鏈接到可執行文件main中,而是在運行時做動態鏈接:
1.操作系統在加載執行main這個程序時,首先查看它有沒有需要動態鏈接的未定義符號。
2. 如果需要做動態鏈接,就查看這個程序制定了哪些共享庫(我們用-lc指定了libc)以及用什麼動態鏈接器來做動態鏈接(我們用 -dynamic-linker /lib/ld-linux.so.2指定了動態鏈接器)。
3. 動態鏈接器在共享庫中查找這些符號的定義,完成鏈接過程。
了解了這些以後我們來看_start的反匯編:
首先將一系列參數壓棧,然後調用libc的庫函數__libc_start_main做初始化工作,其中最後一個壓棧的參數push $0x80483c4是main函數的地址,__libc_start_main在完成初始化工作之後會調用main函數。由於__libc_start_main需要動態鏈接,所以這個庫函數的指令可以在可執行文件main的反匯編中肯定是找不到的,然而我們找到了這個:
一開始看到這以為是libc被鏈接進去了,其實不是。這三條指令位於.plt段不是.text段,.plt段協助完成動態鏈接的過程。
main函數的原型是int main(int argc, char *argv[]),也就是說啟動例程會傳兩個參數給main函數。
由於main函數是被啟動例程調用的,所以從main函數return時仍返回到啟動例程中,main函數的返回值被啟動例程得到,如果將啟動例程表示成等價的C代碼(實際上啟動例程一般是直接用匯編寫的),則它調用main函數的形式是:
exit(main(argc, argv));
也就是說啟動例程得到main函數的返回值後,會立刻用它做參數調用exit函數。exit也是lib中的函數,它首先做一些清理工作,然後調用_exit系統調用終止進程,main函數的返回值最終被傳給_exit系統調用,成為進程的退出狀態。我們也可以在main函數中直接調用exit函數終止進程而不返回到啟動例程。
注意,退出狀態只有8位,而且被Shell解釋成無符號數,如果將上面的代碼改為exit(-1);或return -1;則echo $?會輸出255。
使用_exit函數需要包含頭文件unistd.h。