计算机的知识并不是铁板一块,而是互有联系、可以逐层抽象的。这里就先讲一下C语言函数返回值,由于想讲详细一点,所以要理解这篇需要你对汇编和 shell 有一定程度的了解。
C语言函数的返回值
在C语言当中,一般来说hello world都会这么写(或者类似):
int main() {
printf("Hello World!");
return 0;
}
看起来是很基础的代码,但是这段代码里还有很多细节可以深究。比如,return 0;
这条语句为什么要返回0,它到底做了什么?
这一点看单个文件是匪夷所思的。不如换个情景,假设包括这段代码的文件叫hello_world.c
,编译这个文件,并把视角换到一个需要调用函数main()
的返回值的文件。
用shell执行可执行文件并获取返回值:
./hello_world && echo $?
这里的返回值是程序的exit status(不是进程的exit status),也可以写作exit(0);
,在shell里也可以作为变量调用。shell是一个管理进程和运行程序的程序,在另一个C文件里,hello_world
的返回值也需要通过调用shell获取(system()
函数实质上就是调用shell)。由于shell定义了返回0为成功,所以表示成功运行必须返回0,其它的返回值则没有定义,因系统架构而异。
可是,为什么有时候不在main()
函数中写return 0;
编译器也不会抛出警告呢?
在比较新的C标准里,main()
函数不写return
或者exit()
语句默认返回0,但在旧标准里具体表现因编译器和设备架构而异。顺着思路向下捋,为了验证这一点,写个C程序看看函数不写return 0;
语句的时候编译器做了什么,这里以clang为例:
int test() {
return 1;
}
int main() {
int a = test();
}
用clang编译的可执行文件是有返回值的,其值为0。为什么呢?
用clang编译程序,生成汇编文件,不开优化:
clang -S -O0 test.c
得到的test.s汇编文件中的相关的x86/x86_64汇编指令如下:
_test: ## @test
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl $1, %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.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
callq _test
xorl %ecx, %ecx
movl %eax, -4(%rbp)
movl %ecx, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
从汇编指令里能看出来,函数test()
返回的值保存在寄存器%eax
上。再看_main
,指令xorl %ecx, %ecx
,这条指令执行了亦或操作,实际多用于将寄存器的值清零;指令movl %ecx, %eax
把寄存器%ecx
的值存储到寄存器%eax
上。这两条指令意味着编译器执行了设置返回值为0的操作。
做一点延伸,如果是声明类型为int的普通函数不写return
语句会发生什么呢?
新开一个文件another_test.c
,代码如下所示:
#include <stdio.h>
int test() {
return 1;
}
int another_test() {
//什么都不做
}
int main() {
int a = test();
int b = another_test();
printf("%d %d\n", a, b);
}
clang抛出警告:
another_test.c:9:1: warning: control reaches end of non-void function [-Wreturn-type]
}
不理会编译器的警告,继续编译运行,运行结果是1 0
。
不开优化的汇编结果如下:
_another_test: ## @another_test
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl -4(%rbp), %eax
popq %rbp
retq
.cfi_endproc
## -- End function
P.S. 这里的返回值与硬件中断没有关系。个人觉得返回值和硬件中断有关系的说法是很离奇的(本来不打算提的结果好像真有这种误会),从上面的汇编指令中也能看出来没有任何诸如int 0
等硬件中断的指令。
参考
还想继续深入了解函数具体行为的朋友可以参考各平台的系统调用约定(比如 Linux 的 System V AMD64 调用约定、Windows 的 Micorsoft x64 调用约定),我在上文提到的函数行为只针对 System V AMD64 架构。