【前言】main函數(shù)執(zhí)行前后的宏觀過(guò)程(C++)
- linux系統(tǒng)下壓板程序的入口是"_start"浙巫,這個(gè)函數(shù)是linux系統(tǒng)庫(kù)(Glibc)的一部分,當(dāng)我們的程序和Glibc鏈接在一起形成最終的可執(zhí)行文件的之后荤懂,這個(gè)函數(shù)就是程序執(zhí)行初始化的入口函數(shù)。
- 程序初始化部分完成一系列初始化過(guò)程之后喝峦,會(huì)調(diào)用main函數(shù)來(lái)執(zhí)行程序的主體势誊。在main函數(shù)執(zhí)行完成以后,再返回到初始化部分谣蠢,進(jìn)行一些清理工作粟耻,然后結(jié)束進(jìn)程查近。
- 對(duì)C++而言:(ELF文件為其定義了兩個(gè)特殊的段)
- .init 該段保存的是可執(zhí)行的命令,它構(gòu)成了進(jìn)程的初始化代碼挤忙。因此霜威,當(dāng)一個(gè)程序開始運(yùn)行的時(shí)候,在main函數(shù)被調(diào)用之前册烈,Glibc的初始化部分安排執(zhí)行這個(gè)段中的代碼
- .fini 該段保存著進(jìn)程終止命令代碼戈泼。因此,當(dāng)一個(gè)程序的main函數(shù)正常退出的時(shí)候赏僧,Glibc會(huì)安排執(zhí)行這個(gè)段中的代碼大猛。
- 這兩個(gè)段的存在有特別的目的,如果一個(gè)函數(shù)放到.init段淀零,在mai函數(shù)執(zhí)行前系統(tǒng)就會(huì)執(zhí)行它(就是因?yàn)樗谶@個(gè)段)挽绩。同理,如果一個(gè)函數(shù)放到.fini段驾中,在main函數(shù)返回后該函數(shù)就會(huì)被執(zhí)行唉堪。利用這兩個(gè)特性,C++實(shí)現(xiàn)了全局構(gòu)造和析構(gòu)函數(shù)肩民。
一個(gè)典型程序的大致運(yùn)行步驟
- 操作系統(tǒng)創(chuàng)建進(jìn)程后唠亚,把控制權(quán)交到了程序入口,這個(gè)入口往往是程序運(yùn)行庫(kù)中的某個(gè)入口函數(shù)持痰。
- 入口函數(shù)對(duì)運(yùn)行庫(kù)和程序運(yùn)行環(huán)境進(jìn)行初始化灶搜,包括堆、I/O共啃、線程占调、全局變量的構(gòu)造等等。
- 入口函數(shù)在完成初始化之后移剪,調(diào)用main函數(shù),正式開始執(zhí)行函數(shù)主體部分薪者。
- main函數(shù)執(zhí)行完畢之后纵苛,返回到入口函數(shù),入口函數(shù)進(jìn)行清理工作言津,包括全局變量析構(gòu)攻人、堆銷毀、關(guān)閉I/O等悬槽,然后進(jìn)行系統(tǒng)調(diào)用結(jié)束進(jìn)程怀吻。
入口函數(shù)的實(shí)現(xiàn)
-
Glibc的入口函數(shù)
- _start函數(shù)
??該入口是由ld鏈接器默認(rèn)的鏈接腳本指定的,當(dāng)然用戶也可以通過(guò)參數(shù)進(jìn)行設(shè)定初婆。_start由匯編代碼實(shí)現(xiàn)蓬坡。大致用如下偽代碼表示:
- _start函數(shù)
void _start()
{
%ebp = 0;
int argc = pop from stack
char ** argv = top of stack;
__libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,
edx, top of stack);
}
具體過(guò)程可以參見下圖:

??在調(diào)用_start之前猿棉,裝載器就會(huì)將用戶的參數(shù)和環(huán)境變量壓入棧中,如圖所示屑咳,棧頂元素是argc萨赁,接著就是argv和環(huán)境變量的數(shù)組。
??其中argv除了指向參數(shù)表外兆龙,還隱含緊接著環(huán)境變量表杖爽。這個(gè)環(huán)境變量表要在__libc_start_main里從argv內(nèi)提取出來(lái)。
??實(shí)際執(zhí)行代碼的是__libc_start_main紫皇。
- __libc_start_main函數(shù)
- 函數(shù)頭
```
int __libc_start_main(
int (*main)(int, char **, char *),
char * __unbounded *__unbounded ubp_av,
__typeof(main) init,
void (*fini)(void),
void (*rtld_fini)(void),
viud *__unbounded stack_end)
??可以啊看出慰安,一共有7個(gè)參數(shù),其中main由第一個(gè)參數(shù)傳入聪铺,緊接著就是argc和argv(這里叫做ubp_av泻帮,應(yīng)為其中還包括了環(huán)境變量表)。此外的3個(gè)函數(shù)指針:
(1)init:main調(diào)用之前的初始化工作计寇;
(2)fini:main結(jié)束之后的收尾工作锣杂;
(3)rtld_fini:和動(dòng)態(tài)加載有關(guān)的收尾工作。
最后的stack_end標(biāo)明了棧底的位置番宁,即最高的棧地址元莫。
- \__libc_start_main代碼中的一個(gè)特殊的宏(宏INIT_ARGV_and_ENVIRON)
宏展開之后如下:
`char **ubp_rv = &ubp_av[argc+1];`
`__environ = ubo_ev;`
`__libc_stack_end = stack_end;`
??上述代碼實(shí)際上就是從_start源代碼分析得到的棧布局,重點(diǎn)是讓_environ指針指向緊跟子啊argv數(shù)組后面的環(huán)境變量數(shù)組蝶押。如下圖:

- __libc_start_main代碼中的一系列重要的函數(shù)
```
__pthread_initialize_minimal();
__cxa_atexit(rtld_fini, NULL, NULL);
__libc_init_first(argc, argv, __environ);
__cxa_atexit(fini, NULL, NULL);
(*init)(argc, argv, __environ);
- __cxa_atexit函數(shù)是glibc的內(nèi)部函數(shù)踱蠢,等同于atexit,在main之后調(diào)用棋电。
- 所以可以看出茎截,參數(shù)傳入的fini和rtld_fini均是用于main結(jié)束之后調(diào)用的。在\__libc_start_main末尾赶盔,關(guān)鍵是如下兩行的代碼:
`result = main(argc, argv, _environ);`
`exit(result);`
main函數(shù)最終被調(diào)用企锌,并退出。
【補(bǔ)充】程序正常結(jié)束有兩種情況:main函數(shù)正常返回于未;程序中exit()退出撕攒。但是在\__libc_start_main中可以看出,即使main正常返回了烘浦,exit還是會(huì)被調(diào)用抖坪。所以說(shuō)exit()是程序退出的必經(jīng)之路。