巧妙方法教你實現多種main()

roc_guo發表於2022-09-12

大家都知道,我是做上層應用的,對底層不是很瞭解,更別說那幫人在討論核心的時候,根本插不上話。更多的時候,還是默默記筆記,緊跟大佬們的步伐?。
於是,為了調研這個問題,也查了相關資料。今天藉助本文,來分析下C語言中main()的實現,順便解答下群裡的這個問題。

定義

作為C/C++開發人員,都知道main()函式是一個可執行程式的入口函式,大都會像如下這樣寫:
巧妙方法教你實現多種main()巧妙方法教你實現多種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
#0  main () at main.c:2
(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
#0  main () at main.c:2
#1  0x00007ffff7a2f555 in __libc_start_main () from /lib64/libc.so.6
#2  0x0000000000400429 in _start ()
(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

#include "bp-sym.h"
   .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 Seidlfor 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.  */
#ifdef SHARED
   /* 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
#else
   /* 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)
#endif
   hlt   /* Crash if somehow `exit' does return.  */
#ifdef SHARED
1: movl (%esp), %ebx
   ret
#endif
/* 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,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
                ElfW(auxv_t) *__unbounded auxvec,
#endif
                __typeof (main) init,
                void (*fini) (void),
                void (*rtld_fini) (void), void *__unbounded stack_end)
{
#if __BOUNDED_POINTERS__
 char **argv;
#else
# define argv ubp_av
#endif
 /* 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(),如果命令列為空,則對應的欄位為空(即沒有從棧上取得對應的資料)。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901823/viewspace-2914307/,如需轉載,請註明出處,否則將追究法律責任。

相關文章