main函數和啓動例程

2. main函數和啓動例程 請點評
爲什麼彙編程序的入口是_start,而C程序的入口是main函數呢?本節就來解釋這個問題。在講例 18.1 “最簡單的彙編程序”時,我們的彙編和鏈接步驟是:

$ as hello.s -o hello.o
$ ld hello.o -o hello
以前我們常用gcc main.c -o main命令編譯一個程序,其實也可以分三步做,第一步生成彙編代碼,第二步生成目標文件,第三步生成可執行文件:

$ gcc -S main.c
$ gcc -c main.s
$ gcc main.o
-S選項生成彙編代碼,-c選項生成目標文件,此外在第 2 節 “數組應用實例:統計隨機數”還講過-E選項只做預處理而不編譯,如果不加這些選項則gcc執行完整的編譯步驟,直到最後鏈接生成可執行文件爲止。如下圖所示。

圖 19.2. gcc命令的選項

 


這些選項都可以和-o搭配使用,給輸出的文件重新命名而不使用gcc默認的文件名(xxx.c、xxx.s、xxx.o和a.out),例如gcc main.o -o main將main.o鏈接成可執行文件main。先前由彙編代碼例 18.1 “最簡單的彙編程序”生成的目標文件hello.o我們是用ld來鏈接的,可不可以用gcc鏈接呢?試試看。

$ gcc hello.o -o hello
hello.o: In function `_start':
(.text+0x0): multiple definition of `_start'
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o:(.text+0x0): first defined here
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status
提示兩個錯誤:一是_start有多個定義,一個定義是由我們的彙編代碼提供的,另一個定義來自/usr/lib/crt1.o;二是crt1.o的_start函數要調用main函數,而我們的彙編代碼中沒有提供main函數的定義。從最後一行還可以看出這些錯誤提示是由ld給出的。由此可見,如果我們用gcc做鏈接,gcc其實是調用ld將目標文件crt1.o和我們的hello.o鏈接在一起。crt1.o裏面已經提供了_start入口點,我們的彙編程序中再實現一個_start就是多重定義了,鏈接器不知道該用哪個,只好報錯。另外,crt1.o提供的_start需要調用main函數,而我們的彙編程序中沒有實現main函數,所以報錯。

如果目標文件是由C代碼編譯生成的,用gcc做鏈接就沒錯了,整個程序的入口點是crt1.o中提供的_start,它首先做一些初始化工作(以下稱爲啓動例程,Startup Routine),然後調用C代碼中提供的main函數。所以,以前我們說main函數是程序的入口點其實不準確,_start纔是真正的入口點,而main函數是被_start調用的。

我們繼續研究上一節的例 19.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,這兩個目標文件和我們的main.o鏈接在一起生成可執行文件main。-lc表示需要鏈接libc庫,在第 1 節 “數學函數”講過-lc選項是gcc默認的,不用寫,而對於ld則不是默認選項,所以要寫上。-dynamic-linker /lib/ld-linux.so.2指定動態鏈接器是/lib/ld-linux.so.2,稍後會解釋什麼是動態鏈接。

那麼crt1.o和crti.o裏面都有什麼呢?我們可以用readelf命令查看。在這裏我們只關心符號表,如果只看符號表,可以用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中這條指令暫時寫成push $0x0,等到和main.o鏈接成可執行文件時就知道這個地址是多少了,比如是0x80483c4,那麼可執行文件main中的這條指令就被鏈接器改成了push $0x80483c4。鏈接器在這裏起到符號解析(Symbol Resolution)的作用,在第 5.2 節 “可執行文件”我們看到鏈接器起到重定位的作用,這兩種作用都是通過修改指令中的地址實現的,鏈接器也是一種編輯器,vi和emacs編輯的是源文件,而鏈接器編輯的是目標文件,所以鏈接器也叫Link Editor。T _start這一行表示_start這個符號在crt1.o中提供了定義,這個符號的類型是代碼(T表示Text)。我們從上面的輸出結果中選取幾個符號用圖示說明它們之間的關係:

圖 19.3. C程序的鏈接過程

 


其實上面我們寫的ld命令做了很多簡化,gcc在鏈接時還用到了另外幾個目標文件,所以上圖多畫了一個框,表示組成可執行文件main的除了main.o、crt1.o和crti.o之外還有其它目標文件,本書不做深入討論,用gcc的-v選項可以瞭解詳細的編譯過程:

$ gcc -v main.c -o main
Using built-in specs.
Target: i486-linux-gnu
...
 /usr/lib/gcc/i486-linux-gnu/4.3.2/cc1 -quiet -v main.c -D_FORTIFY_SOURCE=2 -quiet -dumpbase main.c -mtune=generic -auxbase main -version -fstack-protector -o /tmp/ccRGDpua.s
...
 as -V -Qy -o /tmp/ccidnZ1d.o /tmp/ccRGDpua.s
...
 /usr/lib/gcc/i486-linux-gnu/4.3.2/collect2 --eh-frame-hdr -m elf_i386 --hash-style=both -dynamic-linker /lib/ld-linux.so.2 -o main -z relro /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crti.o /usr/lib/gcc/i486-linux-gnu/4.3.2/crtbegin.o -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../.. /tmp/ccidnZ1d.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-linux-gnu/4.3.2/crtend.o /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crtn.o
鏈接生成的可執行文件main中包含了各目標文件所定義的符號,通過反彙編可以看到這些符號的定義:

$ objdump -d main
main:     file format elf32-i386


Disassembly of section .init:

08048274 <_init>:
 8048274: 55                    push   %ebp
 8048275: 89 e5                 mov    %esp,%ebp
 8048277: 53                    push   %ebx
...
Disassembly of section .text:

080482e0 <_start>:
 80482e0: 31 ed                 xor    %ebp,%ebp
 80482e2: 5e                    pop    %esi
 80482e3: 89 e1                 mov    %esp,%ecx
...
08048394 <bar>:
 8048394: 55                    push   %ebp
 8048395: 89 e5                 mov    %esp,%ebp
 8048397: 83 ec 10              sub    $0x10,%esp
...
080483aa <foo>:
 80483aa: 55                    push   %ebp
 80483ab: 89 e5                 mov    %esp,%ebp
 80483ad: 83 ec 08              sub    $0x8,%esp
...
080483c4 <main>:
 80483c4: 8d 4c 24 04           lea    0x4(%esp),%ecx
 80483c8: 83 e4 f0              and    $0xfffffff0,%esp
 80483cb: ff 71 fc              pushl  -0x4(%ecx)
...
Disassembly of section .fini:

0804849c <_fini>:
 804849c: 55                    push   %ebp
 804849d: 89 e5                 mov    %esp,%ebp
 804849f: 53                    push   %ebx
crt1.o中的未定義符號main在main.o中定義了,所以鏈接在一起就沒問題了。crt1.o還有一個未定義符號__libc_start_main在其它幾個目標文件中也沒有定義,所以在可執行文件main中仍然是個未定義符號。這個符號是在libc中定義的,libc並不像其它目標文件一樣鏈接到可執行文件main中,而是在運行時做動態鏈接:

操作系統在加載執行main這個程序時,首先查看它有沒有需要動態鏈接的未定義符號。

如果需要做動態鏈接,就查看這個程序指定了哪些共享庫(我們用-lc指定了libc)以及用什麼動態鏈接器來做動態鏈接(我們用-dynamic-linker /lib/ld-linux.so.2指定了動態鏈接器)。

動態鏈接器在共享庫中查找這些符號的定義,完成鏈接過程。

瞭解了這些原理之後,現在我們來看_start的反彙編:

...
Disassembly of section .text:

080482e0 <_start>:
 80482e0:       31 ed                   xor    %ebp,%ebp
 80482e2:       5e                      pop    %esi
 80482e3:       89 e1                   mov    %esp,%ecx
 80482e5:       83 e4 f0                and    $0xfffffff0,%esp
 80482e8:       50                      push   %eax
 80482e9:       54                      push   %esp
 80482ea:       52                      push   %edx
 80482eb:       68 00 84 04 08          push   $0x8048400
 80482f0:       68 10 84 04 08          push   $0x8048410
 80482f5:       51                      push   %ecx
 80482f6:       56                      push   %esi
 80482f7:       68 c4 83 04 08          push   $0x80483c4
 80482fc:       e8 c3 ff ff ff          call   80482c4 <__libc_start_main@plt>
...
首先將一系列參數壓棧,然後調用libc的庫函數__libc_start_main做初始化工作,其中最後一個壓棧的參數push $0x80483c4是main函數的地址,__libc_start_main在完成初始化工作之後會調用main函數。由於__libc_start_main需要動態鏈接,所以這個庫函數的指令在可執行文件main的反彙編中肯定是找不到的,然而我們找到了這個:

Disassembly of section .plt:
...
080482c4 <__libc_start_main@plt>:
 80482c4:       ff 25 04 a0 04 08       jmp    *0x804a004
 80482ca:       68 08 00 00 00          push   $0x8
 80482cf:       e9 d0 ff ff ff          jmp    80482a4 <_init+0x30>
這三條指令位於.plt段而不是.text段,.plt段協助完成動態鏈接的過程。我們將在下一章詳細講解動態鏈接的過程。

main函數最標準的原型應該是int main(int argc, char *argv[]),也就是說啓動例程會傳兩個參數給main函數,這兩個參數的含義我們學了指針以後再解釋。我們到目前爲止都把main函數的原型寫成int main(void),這也是C標準允許的,如果你認真分析了上一節的習題,你就應該知道,多傳了參數而不用是沒有問題的,少傳了參數卻用了則會出問題。

由於main函數是被啓動例程調用的,所以從main函數return時仍返回到啓動例程中,main函數的返回值被啓動例程得到,如果將啓動例程表示成等價的C代碼(實際上啓動例程一般是直接用匯編寫的),則它調用main函數的形式是:

exit(main(argc, argv));
也就是說,啓動例程得到main函數的返回值後,會立刻用它做參數調用exit函數。exit也是libc中的函數,它首先做一些清理工作,然後調用上一章講過的_exit系統調用終止進程,main函數的返回值最終被傳給_exit系統調用,成爲進程的退出狀態。我們也可以在main函數中直接調用exit函數終止進程而不返回到啓動例程,例如:

#include <stdlib.h>

int main(void)
{
 exit(4);
}
這樣和int main(void) { return 4; }的效果是一樣的。在Shell中運行這個程序並查看它的退出狀態:

$ ./a.out
$ echo $?
4
按照慣例,退出狀態爲0表示程序執行成功,退出狀態非0表示出錯。注意,退出狀態只有8位,而且被Shell解釋成無符號數,如果將上面的代碼改爲exit(-1);或return -1;,則運行結果爲

$ ./a.out
$ echo $?
255
注意,如果聲明一個函數的返回值類型是int,函數中每個分支控制流程必須寫return語句指定返回值,如果缺了return則返回值不確定(想想這是爲什麼),編譯器通常是會報警告的,但如果某個分支控制流程調用了exit或_exit而不寫return,編譯器是允許的,因爲它都沒有機會返回了,指不指定返回值也就無所謂了。使用exit函數需要包含頭文件stdlib.h,而使用_exit函數需要包含頭文件unistd.h,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章