為什麼彙編程序的入口是_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
執行完整的編譯步驟,直到最後連結生成執行檔為止。如下圖所示。
這些選項都可以和-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)。我們從上面的輸出結果中選取幾個符號用圖示說明它們之間的關係:
其實上面我們寫的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
,以後還要詳細解釋這兩個函數。