Ch3nyang's blog collections_bookmark

post

person

about

category

category

local_offer

tag

rss_feed

rss

浅析C中函数返回值的一例UB

calendar_month 2024-10
archive 编程
tag asm tag c

前几天,群友马指导分享了一个有趣的问题:如果一个本该返回整形的函数没有写返回语句,那么这个函数会返回什么呢?

我们观察下面的代码:

#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

Share This Post