【问题标题】:Confused with simple assembly code (IA32)对简单的汇编代码(IA32)感到困惑
【发布时间】:2016-05-23 06:55:25
【问题描述】:

考虑以下 C 函数:

void f1(int i) 
{ 
    int j=i+a; 
}

int f2(int i) 
{
    return i+a; 
}

以及他们的汇编语言翻译(由讲师提供):

#f1 translation :

subl $8, %esp 
movl 12(%esp), %eax 
movl %eax, 4(%esp) 
movl 4(%esp), %eax 
addl a, %eax 
movl %eax, (%esp) 
addl $8, %esp 
ret

#f2 translation :

subl $8, %esp 
movl 12(%esp), %eax 
movl %eax, 4(%esp) 
movl a, %eax 
movl %eax, (%esp) 
movl (%esp), %eax 
addl 4(%esp), %eax 
addl $8, %esp 
ret

我试图画出并写下这两个汇编代码的每一步,但我根本看不出这两者是如何导致不同的 C 代码的。

按照惯例,寄存器 %eax 包含函数的返回值。如果我没记错的话,寄存器 %eaxBOTH 汇编代码的末尾包含值 (i+a) 尽管 f1 不返回任何内容

1) 为什么会这样?究竟是什么表明一个函数正在返回一个值?

另外,在这两个代码中,我们有两行这样的两行:

movl %eax, (%esp) 
movl (%esp), %eax

最后一个似乎是多余的,2) 不是吗?

【问题讨论】:

  • 简单地说:你错了。 f1 不返回任何内容,因为您返回任何内容。没有return 声明,因此您不会返回任何内容。就这么简单。即使%eax总是返回返回值,f1的返回值也会保持void

标签: c assembly translation


【解决方案1】:

如果 ABI 说 EAX 包含返回值,则返回某些内容的函数将在那里具有返回值。如果函数不返回任何内容,则寄存器可能包含任何内容。在这种情况下它可能是相同的值,我没有阅读代码。

如果调用函数没有读取返回值,那么该寄存器包含什么并不重要。所以这都是关于调用者和被调用函数的。他们必须遵守 ABI。如果调用了 void 函数,调用代码将永远不会尝试将该寄存器用作任何东西。

因此,汇编代码中没有任何内容表明该函数返回了某些内容。全部在 C 代码中。

至于 2,MOV 是多余的。这是因为您没有使用优化进行编译,所以编译器只会输出它想要的任何简单的东西,而且不是最优的。

【讨论】:

    【解决方案2】:

    如果您查看启用了优化的编译器输出,则更容易理解差异:

    gcc 5.3 with -O3 -m32 on the Godbolt Compiler Explorer:

    int a = 1234;  // global, not static or const, so it has to get loaded from memory
    
    void f1(int i) { int j=i+a; }
    // 3 : warning: unused variable 'j' [-Wunused-variable]
        ret
    
    int f2(int i) { return i+a; }
        movl    a, %eax         # load a
        addl    4(%esp), %eax   # add i from its arg-passing location on the stack
        ret
    

    f1 被完全优化掉,因为它没有外部可见的效果(没有返回值也没有副作用)。当函数返回时,局部变量会消失(超出范围),因此根本不需要计算它。 (因为不是volatile

    您的教授可能试图说明本地变量是如何存储在堆栈中的。 (C 称之为“自动”存储,而不是动态(malloc)或静态(全局和static)。)

    gcc -O0 太吵了,无法很好地说明这一点,尤其是。将 args 从返回地址上方复制到本地的方式。

    gcc -O0 大多只是将每个 C 语句直接转换为 asm,而不考虑函数的其余部分。而且,变量在语句之间存储/重新加载,而不是留在寄存器中。 (除了有时它们确实作为大型表达式的一部分保持实时)。

    gcc -Og 只是稍微优化了一下,和源码对应得还不错。它仍然将f1 优化为一个空函数。 -O1 也是如此。

    【讨论】:

    • 我是赞成票,但讲师可能会尝试教他的学生如何将参数传递到堆栈以及如何以这种方式访问​​它们。这也是一个学习如何将数据作为局部变量放入堆栈的机会。对于新的汇编程序开发人员来说,使用堆栈可能并不明显,但使用堆栈上的参数传递(以及堆栈上的局部变量)的示例可能是学生学习的一个小步骤。我可以看到教授这样做的合理论据。
    • @MichaelPetch: 原始gcc -O0 输出太脑残,无法很好地说明如何做到这一点。 OP 似乎不知道这是编译器输出,只是教授给了他们。如果我正在分发代码,我会分发在堆栈上保留空间并在那里存储变量的手写 asm,但不会分发将堆栈参数复制到本地的代码和类似的废话。 gcc -O0 很难理解,因为它更长,而且它让你想知道存储/重新加载是否有原因(例如,寄存器是否仍然“有效”,或者内存现在是唯一的副本?)
    • 如果我是教授,我会经历进步。根据我试图实现的目标,我仍然认为它是有价值的。事实上,我希望我的学生对过多的负载/存储感到好奇。在后续任务中,我会要求他们修改代码以仅使用堆栈进行参数传递,但删除不必要的代码。过度负载存储仍在执行预期的功能。学生应该能够通过混乱进行追踪。如果他们甚至无法追踪那些臃肿的输出,那么就有严重的问题。
    • @MichaelPetch:我觉得这并不难,只是耗时且信息量不大。但我不教课。我觉得我可以很快地跟踪它,只是因为我已经习惯了编译器输出的那种模式,而且我可以猜测它在做什么。如果它看起来像未优化的编译器输出但有一条指令不同,我可能不会注意到。也许教授告诉班级这是未优化的编译器输出,但 OP 错过了它?我反对在不知道它是故意坏的线索的情况下分发该代码。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-10-04
    • 1970-01-01
    • 2011-09-17
    • 1970-01-01
    • 2015-07-12
    • 2012-10-05
    • 2016-11-13
    相关资源
    最近更新 更多