第七章之main函数和启动例程
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執行完整的編譯步驟,直到最后鏈接生成可執行文件為止。
?
這些選項都可以和-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,
轉載于:https://www.cnblogs.com/invisible2/p/6874645.html
總結
以上是生活随笔為你收集整理的第七章之main函数和启动例程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第五章 应用程序主窗口
- 下一篇: String的常用方法(java)