C語言不支持重載,多種main()如何實現的呢?
大家都知道,我是做上層應用的,對底層不是很了解,更別說那幫人在討論內核的時候,根本插不上話。更多的時候,還是默默記筆記,緊跟大佬們的步伐??。
于是,為了調研這個問題,也查了相關資料。今天借助本文,來分析下C語言中main()的實現,順便解答下群里的這個問題。
定義
作為C/C++開發人員,都知道main()函數是一個可執行程序的入口函數,大都會像如下這樣寫:
int main() {}
int main(int argc, char *argv[]) {}
但是,作為一個開發老油條,也僅僅知道是這樣做的,當看到二哥提出這個問題的時候,第一反應是重載,但是大家都知道C語言是不支持重載的,那么有沒有可能使用的是默認參數呢?如下這種:
int main(int argc = 1, char **argv = NULL)
好了,為了驗證我的疑問,咱們著手開始進行分析。
ps:在cppreference上對于main()的聲明有第三個參數即char *envp[],該參數是環境變量相關,因為我們使用更多的是不涉及此參數的方式,所以該參數不在本文的討論范圍內。
斷點調試
為了能夠更清晰的理解main()函數的執行過程,寫了一個簡單的代碼,通過gdb查看堆棧信息,代碼如下:
int main() {
return 0;
}
編譯之后,我們通過gdb進行調試,在main()函數處設置斷點,然后看堆棧信息,如下:
(gdb) bt
(gdb)
從上述gdb信息,我們看出main()位于棧頂,顯然,我們的目的是分析main()的調用堆棧信息,而這種main()在棧頂的方式顯然不足以解答我的疑問。
于是,查閱了相關資料后,發現可以通過其它方式打印出更詳細的堆棧信息。
編譯命令如下:
gcc -gdwarf-5 main.c -o main
然后gdb的相關命令(具體的命令可以網上查閱,此處不做過多分析):
gdb ./main -q
Reading symbols from /mtad/main...done.
(gdb) set backtrace past-entry
(gdb) set backtrace past-main
(gdb) show backtrace past-entry
Whether backtraces should continue past the entry point of a program is on.
(gdb) show backtrace past-main
Whether backtraces should continue past "main" is on.
然后在main()處設置斷點,運行,查看堆棧信息,如下:
(gdb) bt
(gdb)
通過如上堆棧信息,我們看到_start()-->__libc_start_main()-->main(),看來應該在這倆函數中,開始分析~~
_start()
為了查看_start()的詳細信息,繼續在_start()函數處打上斷點,然后分析查看:
(gdb) r
Starting program: xxx
Missing separate debuginfos, use: debuginfo-install glibc-2.17-317.el7.x86_64
Breakpoint 1, 0x0000000000400400 in _start ()
(gdb) s
Single stepping until exit from function _start,
which has no line number information.
0x00007ffff7a2f460 in __libc_start_main () from /lib64/libc.so.6
通過如上分析,沒有看到_start()函數的可執行代碼,于是通過網上搜索,發現_start()是用匯編編寫,于是下載了glibc2.5源碼,在路徑處sysdeps/i386/elf/start.S
.text
.globl _start
.type _start,@function
_start:
/* Clear the frame pointer. The ABI suggests this be done, to mark
the outermost frame obviously. */
xorl %ebp, %ebp
/* Extract the arguments as encoded on the stack and set up
the arguments for `main': argc, argv. envp will be determined
later in __libc_start_main. */
popl %esi /* Pop the argument count. */
movl %esp, %ecx /* argv starts just at the current stack top.*/
/* Before pushing the arguments align the stack to a 16-byte
(SSE needs 16-byte alignment) boundary to avoid penalties from
misaligned accesses. Thanks to Edward Seidl <seidl@janed.com>
for pointing this out. */
andl $0xfffffff0, %esp
pushl %eax /* Push garbage because we allocate
28 more bytes. */
/* Provide the highest stack address to the user code (for stacks
which grow downwards). */
pushl %esp
pushl %edx /* Push address of the shared library
termination function. */
/* Load PIC register. */
call 1f
addl $_GLOBAL_OFFSET_TABLE_, %ebx
/* Push address of our own entry points to .fini and .init. */
leal __libc_csu_fini@GOTOFF(%ebx), %eax
pushl %eax
leal __libc_csu_init@GOTOFF(%ebx), %eax
pushl %eax
pushl %ecx /* Push second argument: argv. */
pushl %esi /* Push first argument: argc. */
pushl BP_SYM (main)@GOT(%ebx)
/* Call the user's main function, and exit with its value.
But let the libc call main. */
call BP_SYM (__libc_start_main)@PLT
/* Push address of our own entry points to .fini and .init. */
pushl $__libc_csu_fini
pushl $__libc_csu_init
pushl %ecx /* Push second argument: argv. */
pushl %esi /* Push first argument: argc. */
pushl $BP_SYM (main)
/* Call the user's main function, and exit with its value.
But let the libc call main. */
call BP_SYM (__libc_start_main)
hlt /* Crash if somehow `exit' does return. */
1: movl (%esp), %ebx
ret
/* To fulfill the System V/i386 ABI we need this symbol. Yuck, it's so
meaningless since we don't support machines < 80386. */
.section .rodata
.globl _fp_hw
_fp_hw: .long 3
.size _fp_hw, 4
.type _fp_hw,@object
/* Define a symbol for the first piece of initialized data. */
.data
.globl __data_start
__data_start:
.long 0
.weak data_start
data_start = __data_start
上述實現也是比較簡單的:
xorl %ebp, %ebp:將ebp寄存器清零。
popl %esi、movl %esp, %ecx:裝載器把用戶的參數和環境變量壓棧,實際上按照壓棧的方法,棧頂的元素就是argc,接著其下就是argv和環境變量的數組。這兩句相當于int argc = pop from stack; char **argv = top of stack。
call BP_SYM (__libc_start_main):相當于調用__libc_start_main,調用的時候傳入參數,包括argc、argv。
上述邏輯功能,偽代碼實現如下:
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);
}
__libc_start_main
在上一節中,我們了解到,_start()才是整個可執行程序的入口函數,在_start()函數中調用__libc_start_main()函數,該函數聲明如下:
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char *__unbounded *__unbounded ubp_av,
ElfW(auxv_t) *__unbounded auxvec,
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *__unbounded stack_end)
{
char **argv;
/* Result of the 'main' function. */
int result;
__libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up;
...
...
if (init)
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
...
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);
}
可以看出,在該函數中,最終調用了main()函數,并傳入了相關命令行。(result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);)
截止到此,我們了解了整個main()函數的調用過程,但是,仍然沒有回答二哥的問題,main()是如何實現有參和無參兩種方式的,其實說白了,在標準中,main()只有一種聲明方式,即有參方式。無論是否有命令行參數,都調用該函數。如果有參數,則通過壓棧出棧(對于x86 32位)或者寄存器(x86 64位)的方式獲取參數,然后傳入main(),如果命令行為空,則對應的字段為空(即沒有從棧上取得對應的數據)。