巧妙方法教你實現多種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 #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()函式處打上斷點,然後分析檢視:
(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); }
在上一節中,我們瞭解到,_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/69955379/viewspace-2914296/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java多執行緒【三種實現方法】Java執行緒
- 多引數路由中巧妙利用自定義鍵名實現需求路由
- Verilog乘法的實現——幾種使用多級流水實現方法對比(2)
- Main()方法AI
- 巧妙地實現 debugOnly 函式Go函式
- 實現多個標籤頁之間通訊的幾種方法
- Spring Boot 3中實現多種身份驗證方法開源案例Spring Boot
- 基於SpringBoot實現單元測試的多種情境/方法(二)Spring Boot
- 直播平臺原始碼,多種方法實現圖片複雜排列原始碼
- 多種跨域方式實現原理跨域
- 十二種方法教你怎麼做人
- 找不到 main 方法, 請將 main 方法定義為: public static void main(String[] args)AI
- 區塊鏈安全:實現公鏈雙花攻擊的多種方法區塊鏈
- Java面試之Java中實現多執行緒有幾種方法Java面試執行緒
- Java - 25 main方法JavaAI
- CSS3實現多種背景效果CSSS3
- 分散式鎖的多種實現方式分散式
- 【Spring Security】實現多種認證方式Spring
- 三種方法實現strlen函式函式
- Python | 多執行緒死鎖問題的巧妙解決方法Python執行緒
- AS執行main()方法報錯:SourceSet with name ‘main‘ not foundAI
- 手把手教你在多種無監督聚類演算法實現Python(附程式碼)聚類演算法Python
- JS實現單例模式的多種方案JS單例模式
- 16種方法實現水平居中垂直居中
- 7種方法實現陣列去重陣列
- Java中6種單例實現方法Java單例
- 兩種方法使vue實現jQuery呼叫VuejQuery
- Css實現垂直居中的幾種方法CSS
- Python 5種方法實現單例模式Python單例模式
- React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)ReactNode.js後端
- 用CSS畫出一個任意角度的扇形,可以寫多種實現的方法CSS
- Java多種方法實現等待所有子執行緒完成再繼續執行Java執行緒
- Error Boundaries是這麼實現的,還挺巧妙Error
- 開啟DOS多種方法
- 實現單例模式的 9 種方法,你知道幾種?單例模式
- Linux中多種方法實時記錄歷史命令Linux
- 三種方法實現算出字串中出現多字元字串字元
- Javascript 五十問——實現的繼承多種方式JavaScript繼承