我是準大一,學計算機的,剛剛接觸計算機,萌新求解答


(大家都不提 C99 讓我有點傷心……)

這裡要求是有宿主實現 (hosted implementation) 的,假設不遞歸調用 main :

C99 開始基本沒影響,標準要求等價於自動補上 return 0; 。

C99 之前如果控制抵達 main 結尾而沒有 return ,則行為未定義

如果需要(直接或間接)遞歸調用 main ,則 return 的要求和其他函數一樣,控制流抵達結尾也需要 return 。

(題外話: C++ 不允許用戶定義函數調用 main 或令 main 遞歸)

如果你的代碼需要面向某些古老的編譯器,或者需要採用 C89 標準編譯,或者需要使 main 遞歸,就寫上 return 吧。不需要的話就無所謂。


推送過來好多次了,簡要回答一下好了。其實這個問題的考慮角度應該是:為什麼一個C程序要從main()函數開始,以及main()函數結束(返回)時是返回給「誰」了。

我在寫過的關於C程序函數調用的系列文章裏,有兩篇講了普通函數的調用和返回:

醉臥沙場:簡單函數的調用原理

醉臥沙場:簡單函數的返回原理

那麼問題繼續引申的話,我們可以考慮main()函數是由誰來調用,又是返回給誰的呢?弄明白這個問題,你也就知道了main函數結尾的return有什麼用了。

首先,為什麼是main函數?為什麼C語言要從main函數開始?是誰在調用main函數?

如果你認為程序的起始就是main,而main返回就是程序的結束,那就錯了。程序在鏈接時是有很多複雜的工作引入進來的。

我們以一個簡單的例子開始:

// mytest.c
int main(int argc, char *argv[])
{
return 0;
}

編譯器編譯的過程大家都知道——預編譯,編譯,彙編,鏈接。不管你在什麼系統,都會有這樣的過程,只是具體行為會不太一樣。我就以Linux和glibc為例,在glibc-2.27-38, gcc-8.3.1-2等的環境下來舉例一下(不同的系統,不同的軟體版本,不同的編譯選項,不同的程序都會造成不一樣的結果,所以請盡量從廣義上理解下面的講述)。在我們執行編譯的時候,如:

gcc -o mytest mytest.c

我們實際上忽略了一個默認的選項,那就是"-lc",也就是默認鏈接libc庫函數。所以上述編譯實際上相當於:

gcc -o mytest mytest.c -lc

C語言在編譯時都依賴libc庫,為什麼?因為main函數之前和之後的工作都「藏」在libc裡面。我們首先看一下未經過libc鏈接的一個目標文件:

# gcc -c -o mytest.o mytest.c
# objdump -dS mytest.o

mytest.o: file format elf64-x86-64

Disassembly of section .text:

0000000000000000 &:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 48 89 75 f0 mov %rsi,-0x10(%rbp)
b: b8 00 00 00 00 mov $0x0,%eax
10: 5d pop %rbp
11: c3 retq

很明顯,就是一個main函數。那麼經過libc鏈接後的可執行文件是什麼樣呢?會多些什麼呢?

首先考慮怎麼做這個鏈接操作。直接Load glibc嗎?圖樣圖森破,看:

# ld -o mytest -lc mytest.o
ld: warning: cannot find entry symbol _start; defaulting to 000000000040020b

除了libc以外,我們還需要鏈接一些其它的必要目標文件,這些都是我們平時執行編譯操作時被「隱藏」的操作:

# ld /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o -o mytest -lc mytest.o -dynamic-linker /lib64/ld-linux-x86-64.so.2

可以看到我們另外用到了crt1.o, crti.o和crtn.o三個目標文件,這三個文件來源於glibc-devel包。是編譯C源程序不可或缺的。那麼我們現在看一下經過鏈接後的最終可執行程序和上面的未經過鏈接的目標文件有什麼區別:

# objdump -dS mytest
mytest: file format elf64-x86-64

Disassembly of section .init:

0000000000400330 &:
400330: 48 83 ec 08 sub $0x8,%rsp
400334: 48 8b 05 bd 0c 20 00 mov 0x200cbd(%rip),%rax # 600ff8 &
40033b: 48 85 c0 test %rax,%rax
40033e: 74 02 je 400342 &
400340: ff d0 callq *%rax
400342: 48 83 c4 08 add $0x8,%rsp
400346: c3 retq

Disassembly of section .text:

0000000000400350 &:
400350: 31 ed xor %ebp,%ebp
400352: 49 89 d1 mov %rdx,%r9
400355: 5e pop %rsi
400356: 48 89 e2 mov %rsp,%rdx
400359: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
40035d: 50 push %rax
40035e: 54 push %rsp
40035f: 49 c7 c0 f0 03 40 00 mov $0x4003f0,%r8
400366: 48 c7 c1 90 03 40 00 mov $0x400390,%rcx
40036d: 48 c7 c7 f1 03 40 00 mov $0x4003f1,%rdi
400374: ff 15 76 0c 20 00 callq *0x200c76(%rip) # 600ff0 &
40037a: f4 hlt
40037b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
...
...
0000000000400390 &:
400390: 41 57 push %r15
400392: 49 89 d7 mov %rdx,%r15
400395: 41 56 push %r14
400397: 49 89 f6 mov %rsi,%r14
40039a: 41 55 push %r13
40039c: 41 89 fd mov %edi,%r13d
40039f: 41 54 push %r12
4003a1: 4c 8d 25 f8 0a 20 00 lea 0x200af8(%rip),%r12 # 600ea0 &
4003a8: 55 push %rbp
4003a9: 48 8d 2d f0 0a 20 00 lea 0x200af0(%rip),%rbp # 600ea0 &
4003b0: 53 push %rbx
4003b1: 4c 29 e5 sub %r12,%rbp
4003b4: 48 83 ec 08 sub $0x8,%rsp
4003b8: e8 73 ff ff ff callq 400330 &
4003bd: 48 c1 fd 03 sar $0x3,%rbp
4003c1: 74 1b je 4003de &
4003c3: 31 db xor %ebx,%ebx
4003c5: 0f 1f 00 nopl (%rax)
4003c8: 4c 89 fa mov %r15,%rdx
4003cb: 4c 89 f6 mov %r14,%rsi
4003ce: 44 89 ef mov %r13d,%edi
4003d1: 41 ff 14 dc callq *(%r12,%rbx,8)
4003d5: 48 83 c3 01 add $0x1,%rbx
4003d9: 48 39 dd cmp %rbx,%rbp
4003dc: 75 ea jne 4003c8 &
4003de: 48 83 c4 08 add $0x8,%rsp
4003e2: 5b pop %rbx
4003e3: 5d pop %rbp
4003e4: 41 5c pop %r12
4003e6: 41 5d pop %r13
4003e8: 41 5e pop %r14
4003ea: 41 5f pop %r15
4003ec: c3 retq
4003ed: 0f 1f 00 nopl (%rax)

00000000004003f0 &:
4003f0: c3 retq
00000000004003f1 &:
4003f1: 55 push %rbp
4003f2: 48 89 e5 mov %rsp,%rbp
4003f5: 89 7d fc mov %edi,-0x4(%rbp)
4003f8: 48 89 75 f0 mov %rsi,-0x10(%rbp)
4003fc: b8 00 00 00 00 mov $0x0,%eax
400401: 5d pop %rbp
400402: c3 retq

Disassembly of section .fini:

0000000000400404 &:
400404: 48 83 ec 08 sub $0x8,%rsp
400408: 48 83 c4 08 add $0x8,%rsp
40040c: c3 retq

可以看出經過鏈接後,在main函數前後都多了很多內容。程序的.text段(代碼段)的入口地址已經變成了&,這就是程序的正式入口。經過一些初始化後調用__libc_start_main,這個__libc_start_main一般會做三件事:

  1. 初始化程序,執行_init等操作。
  2. 註冊退出處理程序,就是main返回後需要執行的處理程序。
  3. 調用main函數。

來看它是怎麼調用__libc_start_main的。根據我們前面的經驗,在函數調用前會先傳參,在x86_64下__libc_start_main的三個參數一般是由rdi, rcx, r8等寄存器來擔任的,如下:

40035f: 49 c7 c0 f0 03 40 00 mov $0x4003f0,%r8
400366: 48 c7 c1 90 03 40 00 mov $0x400390,%rcx
40036d: 48 c7 c7 f1 03 40 00 mov $0x4003f1,%rdi
400374: ff 15 76 0c 20 00 callq *0x200c76(%rip) # 600ff0 &

那麼__libc_start_main就得到了三個參數,這三個參數是三個地址,在mytest的反彙編代碼中搜索一下就可以發現這三個地址分別對應:

00000000004003f0 &:
...
0000000000400390 &:
...
00000000004003f1 &:
...

就是這三個地方的入口地址。__libc_csu_init會調用_init,__libc_csu_fini和_fini自然脫不了幹係,它是留給程序結束時用的。main函數就是我們最常見的main函數。

所以現在用腳後跟我們都能猜到,在鏈接時我們從crt1.o, crti.o和crtn.o中得到main函數前和後的很多必要操作&start&>, &init&>和&等,從libc裏得到__libc_csu_fini, __libc_csu_init和__libc_start_main等。程序從&start&>開始,在__libc_start_main裏執行main函數之前的操作(如__libc_csu_init),執行main函數,main函數返回後還需要執行回收操作(如__libc_csu_fini)。

可以看出一個完整的程序,絕不是你在C源程序中看到的那麼樣。在main之前和之後都有編譯器根據標準C庫為我們做的很多工作。而main函數不管你最後有沒有return語句(聲明有返回值最好寫返回),在main函數結束後都會有後續操作。有時你不寫return,不代表就沒有return,編譯器有時會給補充一個return 0等,這和你的編譯器以及編譯選項都有關。但是強烈建議最後還是要明確返回值,因為我們經常不是執行一個程序就完了,很多時候在批處理任務時,程序是聯動的,一個程序的返回結果是需要作為參考的,不要讓不規範成為一種習慣。

由於篇幅和時間所限,我這裡不想再繼續展開討論了。如果以後有時間和機會,我會再寫一篇文章放到專欄裏來補充函數調用和返回系列。這個回答就到此了。如果有興趣可以自己去翻看glibc的代碼,看看我上面說的那些代碼段到底幹了什麼。


更多專業文章及回答請參閱索引:

醉臥沙場:README - 專業性文章及回答總索引?

zhuanlan.zhihu.com圖標

一般情況下main函數return語句的缺失的情況其他回答都提到了,那麼我就說說如果main函數沒有return 0; 但是卻有exit(0);時的情況吧。

首先,題主應該有一個感性的認識,那就是C語言就是彙編語言的更高一層的抽象。和其他高級語言不同,大多數的彙編語句都可以直接轉寫成C語言的語句,C語言的有些語句也只需要兩三行彙編來解釋。如果要深入探求一些C語言的原理,那麼不妨就看看其對應的彙編代碼。

一般情況

對於一個沒有返回值的main函數,如:

// test.c
int main()
{

}

我們通過在終端下鍵入

clang test.c -S

生成其彙編代碼test.s.

其彙編代碼大致如下(平臺為macOS,架構為x86-64)

.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
xorl %eax, %eax
popq %rbp
retq
.cfi_endproc
## -- End function

.subsections_via_symbols

我們可以看到xorl %eax, %eaxretq, 它的意思就是C語言中的return 0;. 因此,我們看到,如果在main函數中不加return 0;, 編譯器會在編譯的時候自動加上。

exit(0);時……

但是,有一種特殊情況,那就是exit(0). 如果我們把最初的C語言文件改為

// test.c
#include &

int main()
{
exit(0);
}

再次生成其彙編代碼,可以看到是:

.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
xorl %edi, %edi
callq _exit
.cfi_endproc
## -- End function

.subsections_via_symbols

我們驚奇的發現,xorl %eax, %eaxretq竟然消失了!取而代之的是xorl %edi, %edicallq _exit. 這是怎麼一回事呢?

原理探求

要理解這個事情,我們還得回到最初的有return 0;的版本來,也就是

// test.c
int main()
{
return 0;
}

我們直接編譯出可執行文件:

clang test.c -g -o test

同時,我們用lldb進行調試:

lldb test

然後,在其第4行,也就是return 0;處打上斷點:

(lldb) breakpoint set --line 4

然後運行:

(lldb) run
Process 1662 launched: /Users/evian/Downloads/test (x86_64)
Process 1662 stopped
* thread #1, queue = com.apple.main-thread, stop reason = breakpoint 1.1
frame #0: 0x0000000100000fad test`main at test.c:4:5
1 // test.c
2 int main()
3 {
-&> 4 return 0;
5 }
Target 0: (test) stopped.

可以看到,程序就在運行到第4行的時候暫停了。

接著,我們使用next進行單步調試,意思是說只運行接下來的第一個語句,然後立刻停止。也就是說,運行return 0;以後就再次暫停。我們發現,畫風突然發生了變化:

(lldb) next
Process 1662 stopped
* thread #1, queue = com.apple.main-thread, stop reason = step over
frame #0: 0x00007fff6f8412f5 libdyld.dylib`start + 1
libdyld.dylib`start:
-&> 0x7fff6f8412f5 &: movl %eax, %edi
0x7fff6f8412f7 &: callq 0x7fff6f85de1a ; symbol stub for: exit
0x7fff6f8412fc &: hlt
0x7fff6f8412fd &: nop
Target 0: (test) stopped.

哦嚯,居然又變成了彙編語句。

看不懂不要緊,我之前說過,彙編語句可以轉寫成C語言語句。那麼,這段代碼的意思是什麼呢?

exit(main());

也就是說,main函數的返回值實際上是有意義的,是用於exit()的參數。在我們的代碼中,main函數的返回值是0, 因此,代碼就實際上是exit(0);.

回到最初的問題

理解了這個,我們就可以理解為什麼我們之前的代碼

// test.c
#include &

int main()
{
exit(0);
}

生成的彙編語言中沒有return 0;對應的語句了。

我們模仿剛剛的步驟,在第6行,也就是exit(0); 處打上斷點,然後運行,並且使用next進行調試,效果居然是……

(lldb) run
Process 1877 launched: /Users/evian/Downloads/test (x86_64)
Process 1877 stopped
* thread #1, queue = com.apple.main-thread, stop reason = breakpoint 1.1
frame #0: 0x0000000100000f91 test`main at test.c:6:5
3
4 int main()
5 {
-&> 6 exit(0);
7 }
Target 0: (test) stopped.
(lldb) next
Process 1877 exited with status = 0 (0x00000000)

居然,居然就直接退出了。

也就是說,在main函數中顯式地使用exit(0)以後,之前說的exit(main());就不會再做了。所以我們可以發現,一個程序真正的退出,在C語言層面上,實際是exit()函數,而main函數的作用,則是給exit()提供返回值。

最後……

以上就是在main函數中使用exit(0);時的情況,希望題主的C語言越來越好!

如果大家想要在macOS上入門彙編語言的話,可以關注我的專欄macOS 上的彙編入門。


這個答案實際是回答的這個問題,但是很奇怪發布之後答案被移到了這個問題之下,可能是知乎界面bug,我就保留在這裡吧。:

敢問 C 語言的「void main」是怎麼一代代傳下來的??

www.zhihu.com圖標

其實,這也是個UB問題啊。

C語言的標準規定main函數必須返回int,如果返回void的話,這個行為屬於未定義行為(UB)。對於未定義行為,不同編譯器可以根據自己的愛好做出任何自己想要的事。

之所以這種習慣能夠被傳下來,當然是因為,那些人使用的編譯器,恰好對於void返回值的main做出了理智的處理,沒有發生任何過激行為,於是,自然這種習慣被流傳下來了。

不過我當年恰好遇上了一個特立獨行的編譯器,你敢返回void,它就敢讓你main函數死循環無法退出。於是我就。。。被調教了。

--

補充,既然恰好看到了這個問題就一併回答了:

關於本問題,本來應該是有定義的,現在C語言標準定義允許main函數末尾不return。但有部分編譯器沒有正確實現(我遇到過)。所以實際在不同環境中你能獲取到的返回值不確定。——這是一個有明確定義的特性但在實踐中卻等同於UB的例子。


永遠牢記 ISO 標準!其它騷操作都可能導致編譯器的未定義行為。按照標準寫就不會和別人的程序出現兼容性問題。

從 C99 開始, main 函數只有兩種

int main(int argc, char *argv[])
{
//帶命令行參數的寫法
return 0;
}

int main(void)
{
//不帶命令行參數的寫法
return 0;
}

而現代 C++ 標準也只有兩種 main 函數的寫法:

int main(int argc, char *argv[])
{
//獲取命令行參數的寫法
return 0; //可省略,隱式定義了
}

int main()
{
//忽略命令行參數的寫法
return 0; //可省略,隱式定義了
}


推薦閱讀:
相關文章