【问题标题】:Why is 0 moved to stack when using return value?为什么在使用返回值时将 0 移到堆栈中?
【发布时间】:2015-09-17 22:20:18
【问题描述】:

我正在尝试反汇编简单 C 程序的 clang 二进制文件(使用 -O0 编译),我对生成的特定指令感到困惑。

这里有两个带有标准参数的空 main 函数,其中一个返回值,另一个不返回:

// return_void.c
void main(int argc, char** argv)
{
}

// return_0.c
int main(int argc, char** argv)
{
    return 0;
}

现在,当我分解它们的程序集时,它们看起来相当不同,但有一行我不明白:

return_void.bin:
(__TEXT,__text) section
_main:
0000000000000000    pushq   %rbp
0000000000000001    movq    %rsp, %rbp
0000000000000004    movl    %edi, -0x4(%rbp)
0000000000000007    movq    %rsi, -0x10(%rbp)
000000000000000b    popq    %rbp
000000000000000c    retq

return_0.bin:
(__TEXT,__text) section
_main:
0000000100000f80    pushq   %rbp                
0000000100000f81    movq    %rsp, %rbp          
0000000100000f84    xorl    %eax, %eax          # We return with EAX, so we clean it to return 0
0000000100000f86    movl    $0x0, -0x4(%rbp)    # What does this mean?
0000000100000f8d    movl    %edi, -0x8(%rbp)
0000000100000f90    movq    %rsi, -0x10(%rbp)
0000000100000f94    popq    %rbp
0000000100000f95    retq

只有在我使用函数时才会生成不是void,所以我认为它可能是另一种返回0的方式,但是当我更改返回的常量时,这一行根本没有改变:

// return_1.c
int main(int argc, char** argv)
{
    return 1;
}

empty_return_1.bin:
(__TEXT,__text) section
_main:
0000000100000f80    pushq   %rbp
0000000100000f81    movq    %rsp, %rbp
0000000100000f84    movl    $0x1, %eax           # Return value modified
0000000100000f89    movl    $0x0, -0x4(%rbp)    # This value is not modified
0000000100000f90    movl    %edi, -0x8(%rbp)
0000000100000f93    movq    %rsi, -0x10(%rbp)
0000000100000f97    popq    %rbp
0000000100000f98    retq

为什么会生成这条线,它的目的是什么?

【问题讨论】:

  • 您是否使用普通函数(即不称为 main 的函数)对此进行了测试?
  • main 没有int 返回类型违反了托管环境的标准。不要期望任何特定的行为。
  • 你禁止 clang 删除无用的代码(-O0)。为什么要询问程序集中的奇怪代码?

标签: c assembly clang disassembly calling-convention


【解决方案1】:
movl   $0x0,-0x4(%rbp)

该指令将0 存储在%rbp - 4。似乎clang为main的隐式返回值分配了一个隐藏的局部变量。

来自 clang 邮件列表:

是的。我们分配一个隐式局部变量来保存返回值; return 语句然后只需初始化返回槽并跳转到 尾声,其中插槽被加载并返回。我们不使用 phi 因为到达结尾的控制流程不是 由于本地清理,必须像简单的分支一样简单 作用域(如 C++ 析构函数)。

隐式返回值(如 main 的)由隐式存储处理 在序言中。

来源:http://lists.cs.uiuc.edu/pipermail/cfe-dev/2012-February/019767.html

【讨论】:

  • eax 确实有返回值,不知道你是从哪里想到将它存储在堆栈中的,尤其是在被调用者的框架中,该框架将被时间销毁调用者获得控制权来检查返回值。您似乎是将它与返回 address 混淆的人。
  • 除了对于任何系统来说这都是 32 位 ABI 而 OP 显然使用 64 位系统,返回值 存储在 EAX/RAX 中。你搞混了:),也许名字叫Return address,OP问题确实是个好问题。
  • 你们都对,我不抱歉我犯了一个错误我正在更新答案。谢谢
  • @Giuseppe Pes:在 x86 平台上,随着堆栈的增长,堆栈指针向下移动(到较低的内存地址)。这意味着返回地址始终存储在函数开始时与rsp 值的 偏移处。 -0x4(%rbp) 在这种情况下不可能是返回地址。
  • 很高兴您尝试改进您的答案,但事实是它不相关。 OP 清楚地知道堆栈和函数调用是如何工作的,它只是想知道(就像我一样)为什么会有一个带有 0 的移动。如果你知道答案,我认为最好删除它并写一个新的:)
【解决方案2】:

根据标准(对于托管环境),需要5.1.2.2.1main 才能返回int 结果。因此,如果违反此规定,请不要指望已定义的行为。

此外,main 实际上_不需要显式返回0;如果它到达函数的末尾,则隐式返回 this。 (注意这仅适用于main,它也没有原型。

【讨论】:

  • 这与问题有什么关系?
  • @FUZxxl:在讨论生成的代码时,main 的签名不符合标准与讨论该代码的问题无关?
  • 啊,是的,我错过了 OP 使用的虚假返回类型。
【解决方案3】:

clang 正在堆栈上为参数(寄存器edirsi)腾出空间,并且出于某种原因也将值 0 放入堆栈。我假设 clang 将您的代码编译为这样的 SSA 表示:

int main(int argc, char** argv)
{
    int a;

    a = 0;
    return a;
}

这可以解释为什么要分配堆栈槽。如果 clang 也进行持续传播,这可以解释为什么 eax 被清零而不是从 -4(%rbp) 加载。一般来说,不要过多考虑 未优化 程序集中的可疑结构。毕竟,你禁止编译器删除无用的代码。

【讨论】:

    【解决方案4】:

    以下代码揭示了该区域的用途

    int main(int argc, char** argv)
    {
        if (rand() == 42)
          return 1;
    
        printf("Helo World!\n");
        return 0;
    }
    

    一开始就是这样

    movl    $0, -4(%rbp)
    

    那么提前返回如下所示

    callq   rand
    cmpl    $42, %eax
    jne .LBB0_2
    movl    $1, -4(%rbp)
    jmp .LBB0_3
    

    最后还是这样

    .LBB0_3:
    movl    -4(%rbp), %eax
    addq    $32, %rsp
    popq    %rbp
    retq
    

    所以,这个区域确实是用来存储函数返回值的。它似乎不是非常必要,也没有在优化代码中使用,但在-O0 模式下它就是这样工作的。

    【讨论】:

      猜你喜欢
      • 2018-08-29
      • 1970-01-01
      • 1970-01-01
      • 2011-07-08
      • 2023-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-15
      相关资源
      最近更新 更多