【前言】main函數執行前后的宏觀過程(C++)
- linux系統下壓板程序的入口是"_start",這個函數是linux系統庫(Glibc)的一部分,當我們的程序和Glibc鏈接在一起形成最終的可執行文件的之后,這個函數就是程序執行初始化的入口函數。
- 程序初始化部分完成一系列初始化過程之后,會調用main函數來執行程序的主體。在main函數執行完成以后,再返回到初始化部分,進行一些清理工作,然后結束進程。
- 對C++而言:(ELF文件為其定義了兩個特殊的段)
- .init 該段保存的是可執行的命令,它構成了進程的初始化代碼。因此,當一個程序開始運行的時候,在main函數被調用之前,Glibc的初始化部分安排執行這個段中的代碼
- .fini 該段保存著進程終止命令代碼。因此,當一個程序的main函數正常退出的時候,Glibc會安排執行這個段中的代碼。
- 這兩個段的存在有特別的目的,如果一個函數放到.init段,在mai函數執行前系統就會執行它(就是因為它在這個段)。同理,如果一個函數放到.fini段,在main函數返回后該函數就會被執行。利用這兩個特性,C++實現了全局構造和析構函數。
一個典型程序的大致運行步驟
- 操作系統創建進程后,把控制權交到了程序入口,這個入口往往是程序運行庫中的某個入口函數。
- 入口函數對運行庫和程序運行環境進行初始化,包括堆、I/O、線程、全局變量的構造等等。
- 入口函數在完成初始化之后,調用main函數,正式開始執行函數主體部分。
- main函數執行完畢之后,返回到入口函數,入口函數進行清理工作,包括全局變量析構、堆銷毀、關閉I/O等,然后進行系統調用結束進程。
入口函數的實現
-
Glibc的入口函數
- _start函數
??該入口是由ld鏈接器默認的鏈接腳本指定的,當然用戶也可以通過參數進行設定。_start由匯編代碼實現。大致用如下偽代碼表示:
- _start函數
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);
}
具體過程可以參見下圖:

??在調用_start之前,裝載器就會將用戶的參數和環境變量壓入棧中,如圖所示,棧頂元素是argc,接著就是argv和環境變量的數組。
??其中argv除了指向參數表外,還隱含緊接著環境變量表。這個環境變量表要在__libc_start_main里從argv內提取出來。
??實際執行代碼的是__libc_start_main。
- __libc_start_main函數
- 函數頭
```
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個參數,其中main由第一個參數傳入,緊接著就是argc和argv(這里叫做ubp_av,應為其中還包括了環境變量表)。此外的3個函數指針:
(1)init:main調用之前的初始化工作;
(2)fini:main結束之后的收尾工作;
(3)rtld_fini:和動態加載有關的收尾工作。
最后的stack_end標明了棧底的位置,即最高的棧地址。
- \__libc_start_main代碼中的一個特殊的宏(宏INIT_ARGV_and_ENVIRON)
宏展開之后如下:
`char **ubp_rv = &ubp_av[argc+1];`
`__environ = ubo_ev;`
`__libc_stack_end = stack_end;`
??上述代碼實際上就是從_start源代碼分析得到的棧布局,重點是讓_environ指針指向緊跟子啊argv數組后面的環境變量數組。如下圖:

- __libc_start_main代碼中的一系列重要的函數
```
__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函數是glibc的內部函數,等同于atexit,在main之后調用。
- 所以可以看出,參數傳入的fini和rtld_fini均是用于main結束之后調用的。在\__libc_start_main末尾,關鍵是如下兩行的代碼:
`result = main(argc, argv, _environ);`
`exit(result);`
main函數最終被調用,并退出。
【補充】程序正常結束有兩種情況:main函數正常返回;程序中exit()退出。但是在\__libc_start_main中可以看出,即使main正常返回了,exit還是會被調用。所以說exit()是程序退出的必經之路。