浅析C中函数返回值的一例UB
前几天,群友马指导分享了一个有趣的问题:如果一个本该返回整形的函数没有写返回语句,那么这个函数会返回什么呢?
我们观察下面的代码:
#include <stdio.h>
int foo(int a) {
if (a > 0) {
return 3;
}
}
int main() {
int a = foo(-1);
printf("%d\n", a);
return 0;
}
我们在不同的编译器下编译并运行这段代码,得到如下结果:
编译器版本 | 输出结果 |
---|---|
x86-64 gcc 14.2 | 4198716 |
x86-64 clang 19.1 | 0 |
MinGW clang 16.0.2 | 0 |
x64 MSVC 19.40 | 1105862232 |
x86 MSVC 19.40 | 4505280 |
zig cc 0.13.0 | 0 |
奇怪的是,我们多次运行这段代码,得到的输出都是这样。这是为什么呢?下面,我们以 x86-64 gcc 14.2 为例,来分析这个问题。
我们首先来看看程序的汇编代码:
foo:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
cmp DWORD PTR [rbp-4], 0
jle .L2
mov eax, 3
jmp .L1
.L2:
.L1:
pop rbp
ret
.LC0:
.string "%d\n"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov edi, -1
call foo
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
leave
ret
容易看到,foo
函数的返回值是存放在 eax
寄存器中的。如果 a
大于 0,那么 eax
的值是 3;否则,eax
的值是什么呢?我们知道,eax
寄存器是一个临时寄存器,它的值是不确定的。在这里,eax
的值是由上一次调用的函数决定的。我们可以通过下面的代码来验证这一点:
#include <stdio.h>
int foo(int a) {
if (a > 0) {
return 3;
}
}
int main() {
int a = foo(1);
a = foo(-1);
printf("%d\n", a);
return 0;
}
我们编译并运行这段代码,得到的输出是 3。这是因为,foo(1)
的返回值是 3,也就是说,eax
的值是 3;而再次调用 foo(-1)
时,没有改变 eax
的值,故 eax
依然是 3,所以 foo(-1)
的返回值是 3。
类似的,我们也可以内嵌汇编来修改 eax
的值,从而改变 foo(-1)
的返回值:
#include <stdio.h>
int foo(int a) {
asm("mov $5, %eax");
if (a > 0) {
return 3;
}
}
int main() {
int a = foo(-1);
printf("%d\n", a);
return 0;
}
我们编译并运行这段代码,得到的输出是 5。这是因为,我们在 foo
函数中,通过内嵌汇编指令 mov $5, %eax
来修改 eax
的值为 5,所以 foo(-1)
的返回值是 5。
但事情还没有结束,如果我们原来的程序在编译时加上 -O1
或更高的优化选项,那么 foo(-1)
的返回值会变为 0。此时,程序的汇编代码如下:
foo:
test edi, edi
jg .L3
ret
.L3:
mov eax, 3
ret
.LC0:
.string "%d\n"
main:
sub rsp, 8
mov esi, 0
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov eax, 0
add rsp, 8
ret
可以看到,这里并没有调用 foo
函数。不同于之前通过 mov esi eax
来输出 eax
的值,这次是通过 mov esi, 0
来输出 0。这是因为,编译器在优化时,发现 foo(-1)
的返回值是不确定的,所以直接将其优化为 0。
当然了,这归根结底是个 Undefined Behavior,其结果取决于编译器的实现,在实际应用中也不应当使用。
Comments