【问题标题】:Why does MSVC debug mode leave out the cmp/jcc for one empty if() body but not another (i++ vs. ++i)?为什么 MSVC 调试模式会为一个空的 if() 主体而不是另一个(i++ 与 ++i)遗漏 cmp/jcc?
【发布时间】:2019-08-27 06:12:34
【问题描述】:

我正在使用一台AMD64计算机(Intel Pentium Gold 4415U)来比较一些从C语言转换而来的汇编指令(当然,确切地说,是反汇编)。

在 Windows 10 中,我将 Visual Studio 2017(15.2) 与他们的 C 编译器一起使用。 我的示例代码如下所示:

int main() {
    int i = 0;
    if(++i == 4);
    if(i++ == 4);
    return 0;
}

反汇编如下:

mov         eax,dword ptr [i]  // if (++i == 4);
inc         eax  
mov         dword ptr [i],eax  

mov         eax,dword ptr [i]  // if (i++ == 4);
mov         dword ptr [rbp+0D4h],eax    ; save old i to a temporary
mov         eax,dword ptr [i]  
inc         eax  
mov         dword ptr [i],eax  
cmp         dword ptr [rbp+0D4h],4      ; compare with previous i
jne         main+51h (07FF7DDBF3601h)  
mov         dword ptr [rbp+0D8h],1  
jmp         main+5Bh (07FF7DDBF360Bh)  
*mov         dword ptr [rbp+0D8h],0

07FF7DDBF3601 转到最后一行指令(*)。
07FF7DDBF360B 去'return 0;'。

if (++i == 4) 中,程序不会观察“添加”i 是否满足条件。

但是在if (i++ == 4) 中,程序将“前一个”i 保存到堆栈中,然后进行增量。之后,程序将“前一个”i 与常数整数 4 进行比较。

两个C代码不同的原因是什么?它只是编译器的机制吗?更复杂的代码会有所不同吗?

我试图通过 Google 找到有关此问题的信息,但未能找到差异的根源。我必须了解“这只是编译器行为”吗?

【问题讨论】:

  • 闻起来像优化。
  • 代码相当于return 0; 编译器可以优化掉其他任何东西。
  • @PaulOgilvie:除了它没有优化。这显然是未经优化的编译器输出,它单独编译每个 C 语句,不进行任何常量传播,并且有效地将所有本地变量视为volatile,因此如果调试器修改它们,程序仍然可以工作。当然,它仍然可以在in语句中进行优化,并在空的if body 上移除跳转。
  • 为什么要查看调试模式程序集?您想从中获得什么见解?
  • 不太清楚他的意思。当然它们的工作方式不同,这就是为什么有两个操作的原因。我们不需要两个工作相同的操作,现在是吗?

标签: c assembly visual-c++ reverse-engineering


【解决方案1】:

正如 Paul 所说,该程序没有可观察到的副作用,启用优化后,MSVC 或任何其他主要编译器 (gcc/clang/ICC) 将把 main 编译为简单的 xor eax,eax / ret

i 的值永远不会转义函数(不存储到全局或返回),因此可以完全优化它。即使是这样,这里的恒定传播也是微不足道的。


这只是一个怪癖/实现细节,MSVC 的调试模式反优化代码生成决定不在空的 if 主体上发出 cmp/jcc;即使在调试模式下也对调试毫无帮助。这将是一个跳转到它所经过的相同地址的分支指令。

调试模式代码的要点是您可以单步执行源代码行,并使用调试器修改 C 变量。并不是说 asm 是 C 到 asm 的字面和忠实音译。 (而且编译器可以快速生成它,无需在质量上花费任何精力,以加快编辑/编译/运行周期。)Why does clang produce inefficient asm with -O0 (for this simple floating point sum)?

编译器的代码生成到底有多脑残不取决于任何语言规则;没有实际的标准来定义编译器在调试模式下必须做什么,只要实际使用一个空的if 主体的分支指令。


显然对于您的编译器版本,i++ 后增量足以让编译器忘记循环体为空?

我无法使用 MSVC 19.0 或 19.10 on the Godbolt compiler explorer, with 32 or 64-bit mode 重现您的结果。 (VS2015 或 VS2017)。或任何其他 MSVC 版本。我根本没有从 MSVC、ICC 或 gcc 获得条件分支。

MSVC 确实实现了i++,并将旧值实际存储到内存中,就像你展示的那样。太坏了。 GCC -O0 显着提高了调试模式代码的效率。当然,这仍然很脑残,但在一个单一的声明中,它有时会好很多。

可以用clang重现它! (但它同时适用于ifs):

# clang8.0 -O0
main:                                   # @main
        push    rbp
        mov     rbp, rsp
        mov     dword ptr [rbp - 4], 0       # default return value

        mov     dword ptr [rbp - 8], 0       # int i=0;

        mov     eax, dword ptr [rbp - 8]
        add     eax, 1
        mov     dword ptr [rbp - 8], eax
        cmp     eax, 4                       # uses the i++ result still in a register
        jne     .LBB0_2                      # jump over if() body
        jmp     .LBB0_2                      # jump over else body, I think.
.LBB0_2:

        mov     eax, dword ptr [rbp - 8]
        mov     ecx, eax
        add     ecx, 1                       # i++ uses a 2nd register
        mov     dword ptr [rbp - 8], ecx
        cmp     eax, 4
        jne     .LBB0_4
        jmp     .LBB0_4
.LBB0_4:

        xor     eax, eax                     # return 0

        pop     rbp                          # tear down stack frame.
        ret

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-07-13
    • 1970-01-01
    • 1970-01-01
    • 2021-07-11
    • 2020-05-24
    • 2021-03-29
    • 2018-01-09
    • 1970-01-01
    相关资源
    最近更新 更多