当脱离非 void 函数末尾时写入未使用的参数的返回值

qwr*_*qwr 3 c x86 gcc return-value kernighan-and-ritchie

在这个高尔夫答案中,我看到了一个技巧,其中返回值是未传入的第二个参数。

int f(i, j) 
{
    j = i;   
}

int main() 
{
    return f(3);
}
Run Code Online (Sandbox Code Playgroud)

gcc 的汇编输出来看,当代码复制时,j = i它存储的结果eax恰好是返回值。

f:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    -4(%rbp), %eax
        movl    %eax, -8(%rbp)
        nop
        popq    %rbp
        ret
main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $3, %edi
        movl    $0, %eax
        call    f
        popq    %rbp
        ret 
Run Code Online (Sandbox Code Playgroud)

那么,这一切仅仅是因为幸运吗?gcc 对此有记录吗?它只适用于,但它适用于我尝试过-O0的一堆值,以及一堆不同版本的 GCC。i-m32

Pet*_*des 6

如果确实需要寄存器, gcc-O0喜欢计算返回值寄存器中的表达式。 (GCC-O0通常只喜欢在 retval 寄存器中包含值,但这超出了将其选为第一个临时值的范围。)

我已经测试了一下,看起来确实是 GCC-O0有意跨多个 ISA 执行此操作,有时甚至使用额外的mov指令或等效指令。IIRC 我使表达式变得更加复杂,因此计算结果最终出现在另一个寄存器中,但它仍然将其复制回 retval 寄存器。

类似的事情x++可以(在 x86 上)编译到内存目标 inc 或 add 不会将值保留在寄存器中,但赋值通常会。因此,值得注意的是,GCC 正在像GNU C 语句表达式一样对待函数体。


没有任何记录、保证或标准化。这是一个实现细节,而不是供您这样利用的东西。

以这种方式“返回”一个值意味着您正在使用“GCC -O0”而不是 C 进行编程。 代码高尔夫规则的措辞表明程序必须至少在一种实现上运行。但我对此的理解是,它们应该出于正确的原因而工作,而不是因为一些副作用的实现细节。它们在 clang 上崩溃并不是因为 clang 不支持某些语言功能,只是因为它们甚至不是用 C 编写的。

启用优化后进行破坏也不酷;在代码高尔夫中,某种程度的 UB 通常是可以接受的,例如整数环绕或指针转换类型双关语,人们可能合理地希望得到明确的定义。但这纯粹是滥用一个编译器的实现细节,而不是一种语言功能。

我在Codegolf.SE C 高尔夫技巧问答的相关答案下的评论中争论了这一点(错误地声称它超出了 GCC 范围)。这个答案有 4 票反对(在我看来应该有更多票),但有 16 票赞成。因此,社区的一些成员不认为这是可怕且愚蠢的。


有趣的事实:在 ISO C++(但不是 C)中,执行从非函数末尾脱落void是未定义行为,即使调用者使用结果。即使在 GNU C++ 中也是如此;在 GCC之外-O0,clang 有时会发出类似ud2(非法指令)的代码,用于到达没有return. 因此,GCC 通常不会定义此处的行为(允许实现哪些 ISO C 和 C++ 未定义的操作。例如,gcc -fwrapv将有符号溢出定义为 2 的补码环绕。)

在 ISO C 中,脱离非 void 函数的末尾是合法的:只有当调用者使用返回值时,它才会变成 UB。没有-WallGCC 甚至可能不会发出警告。 检查没有 return 语句的函数的返回值

禁用优化后,函数内联不会发生,因此 UB 在编译时并不真正可见。(除非你使用__attribute__((always_inline)))。


传递第二个参数只是给你一些要分配的东西。它是一个函数 arg 并不重要。但即使这样你也需要一个单独的变量i=i;来优化。-O0也只是i;优化掉。

有趣的事实:递归f(i){ f(i); }函数体i在将其复制到第一个参数传递寄存器之前确实会通过 EAX 进行反弹。所以 GCC 真的很喜欢 EAX。

        movl    -4(%rbp), %eax
        movl    %eax, %edi
        movl    $0, %eax             # without a full prototype, pass # of FP args in AL
        call    f
Run Code Online (Sandbox Code Playgroud)

i++;不加载到 EAX 中;它只是使用内存目标add而不加载到寄存器中。值得尝试使用 gcc -O0 for ARM。