首页 > 解决方案 > 为什么它返回一个随机值而不是我给函数的值?

问题描述

在 C 程序中,有一个交换函数,该函数接受一个名为 x 的参数,我希望它通过更改主函数内交换函数中的 x 值来返回它。

当我将参数赋值为变量时,我想要它,但是当我直接为参数设置整数值时,程序会产生随机输出。

#include <stdio.h>

int swap (int x) {

    x = 20;
    
}

int main(void){

    int y = 100;
    
    int a = swap(y);   

    printf ("Value: %d", a);

    return 0;
}

此代码的输出:100(如我所愿)

但是这段代码:

#include <stdio.h>

int swap (int x) {

    x = 20;
    
}

int main(void){
    
    int a = swap(100);   

    printf ("Value: %d", a);

    return 0;
}

返回随机值,例如Value: 779964766or Value:1727975774

实际上,在两个代码中,我给函数一个整数类型的值,即使是相同的值,但为什么输出不同?

标签: cassemblygccundefined-behavior

解决方案


首先,C 函数是按值调用的:int x函数中的 arg 是一个copy。修改它不会修改调用者传递的任何内容的副本,因此您的swap意义为零。

其次,您正在使用函数的返回值,但您没有return声明。在 C 中(与 C++ 不同),执行从非函数的末尾脱落并不是未定义的行为void(由于历史原因,以前void存在,并且函数返回默认为 int 的类型)。但是,当函数没有返回值时,调用者使用返回值仍然未定义的行为。return

在这种情况下,返回 100 是未定义行为的结果(使用函数的返回值,其中执行在没有return语句的情况下结束)。 这是 GCC 在调试模式下编译的巧合(-O0

GCC-O0喜欢在返回值寄存器中计算非常量表达式,例如 x86-64 上的 EAX/RAX。(这实际上适用于跨架构的 GCC,而不仅仅是 x86-64)。这实际上在 codegolf.SE 答案中被滥用;显然,有些人宁愿将高尔夫gcc -O0作为一种语言而不是 ANSI C。请参阅此“C 高尔夫技巧”答案及其评论,以及有关为什么在函数中将值放入 RAX 的SO Q&A 。i=j请注意,它仅在 GCC 必须将值加载到寄存器中时才有效,而不仅仅是像add dword ptr [rbp-4], 1forx++或其他那样进行内存目标增量。


在您的情况下(您的代码由 Godbolt编译器资源管理器上的 GCC10.2 编译

int y=100;将 100 直接存储到堆栈内存(GCC 编译代码的方式)。

int a = swap(y);加载y到 EAX(没有明显原因),然后复制到 EDI 以作为 arg 传递给swap. 由于 GCC 的 asm forswap不涉及 EAX,因此在调用后,EAX=y,因此函数有效地返回y.

但是如果你用 调用它swap(100),GCC 在设置参数时不会最终将 100 放入 EAX。

GCC 编译你swap的 .asm 的方式不会触及 EAX,所以main剩下的任何东西都被视为返回值。

main:
...
        mov     DWORD PTR [rbp-4], 100          # y=100

        mov     eax, DWORD PTR [rbp-4]          # load y into EAX
        mov     edi, eax                        # copy it to EDI (first arg-passing reg)
        call    swap                            # swap(y)

        mov     DWORD PTR [rbp-8], eax          # a = EAX as the retval = y
...

但与你的其他主要:

main:
...                                    # nothing that touches EAX
        mov     edi, 100
        call    swap
        mov     DWORD PTR [rbp-4], eax   # a = whatever garbage was there on entry to main
...

(后者作为 arg for...重新加载,匹配 ISO C 语义,因为 GCC将每个 C 语句编译为单独的 asm 块;因此,后面的不受早期 UB 的影响(与启用优化的一般情况不同),所以只打印内存位置中的任何内容。)aprintf-O0a

swap函数编译如下(再次,GCC10.2 -O0):

swap:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-4], 20
        nop
        pop     rbp
        ret

请记住,这些都与有效的可移植 C 无关。这(使用留在内存或寄存器中的垃圾)是您在实践中从 C 中看到的一种调用未定义行为的东西,但肯定不是唯一的东西。另请参阅LLVM 博客中的每个 C 程序员应该了解的关于未定义行为的知识。

这个答案只是回答 asm 中到底发生了什么的字面问题。(我假设 GCC 未优化,因为这很容易解释结果,而 x86-64 因为这是一个常见的 ISA,尤其是当人们忘记提及任何 ISA 时。)

其他的编译器不一样,开启优化的GCC也会不一样。


推荐阅读