旧的和新的GCC生成的汇编代码的for循环的差异

时间:2014-01-15 15:15:09

标签: c gcc assembly

我正在阅读有关汇编代码的章节,其中有一个例子。这是C程序:

int main()
{
    int i;
    for(i=0; i < 10; i++)
    {
        puts("Hello, world!\n");
    }
    return 0;
}

这是书中提供的汇编代码:

0x08048384 <main+0>:    push ebp
0x08048385 <main+1>:    mov ebp,esp
0x08048387 <main+3>:    sub esp,0x8
0x0804838a <main+6>:    and esp,0xfffffff0
0x0804838d <main+9>:    mov eax,0x0
0x08048392 <main+14>:   sub esp,eax
0x08048394 <main+16>:   mov DWORD PTR [ebp-4],0x0
0x0804839b <main+23>:   cmp DWORD PTR [ebp-4],0x9
0x0804839f <main+27>:   jle 0x80483a3 <main+31>
0x080483a1 <main+29>:   jmp 0x80483b6 <main+50>
0x080483a3 <main+31>:   mov DWORD PTR [esp],0x80484d4
0x080483aa <main+38>:   call 0x80482a8 <_init+56>
0x080483af <main+43>:   lea eax,[ebp-4]
0x080483b2 <main+46>:   inc DWORD PTR [eax]
0x080483b4 <main+48>:   jmp 0x804839b <main+23>

以下是我的版本的一部分:

   0x0000000000400538 <+8>: mov    DWORD PTR [rbp-0x4],0x0
=> 0x000000000040053f <+15>:    jmp    0x40054f <main+31>
   0x0000000000400541 <+17>:    mov    edi,0x4005f0
   0x0000000000400546 <+22>:    call   0x400410 <puts@plt>
   0x000000000040054b <+27>:    add    DWORD PTR [rbp-0x4],0x1
   0x000000000040054f <+31>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x0000000000400553 <+35>:    jle    0x400541 <main+17>

我的问题是,为什么在本书的版本中它将0分配给变量(mov DWORD PTR [ebp-4],0x0)并在此之后与cmp进行比较,但在我的版本中,它分配然后它确实jmp 0x40054f <main+31>所在的cmp

在没有任何jump的情况下分配和比较似乎更合乎逻辑,因为它就像在for循环中一样。

2 个答案:

答案 0 :(得分:5)

为什么你的编译器做的不同于本书中使用的不同编译器?因为它是一个不同的编译器。没有两个编译器会编译所有相同的代码,即使是非常简单的代码也可以被两个不同的编译器甚至同一个编译器的两个版本编译得大不相同。很明显,两者都是在没有任何优化的情况下编译的,通过优化,结果会更加不同。

让我们了解for循环的作用。

for (i = 0; i < 10; i++) {
    code;
}

让我们把它写得更接近第一个编译器生成的汇编程序。

        i = 0;
start:  if (i > 9) goto out;
        code;
        i++;
        goto start;
out:

“我的版本”现在也是一样:

        i = 0;
        goto cmp;
start:  code;
        i++;
cmp:    if (i < 10) goto start;

这里明显不同的是,在“我的版本”中,只有一个跳转在循环内执行,而书籍版本有两个。由于CPU对分支的敏感程度,在更现代的编译器中生成循环是一种非常常见的方法。许多编译器即使没有任何优化也会生成这样的代码,因为它在大多数情况下表现更好。较旧的编译器没有这样做,因为要么他们没有考虑它,要么这个技巧是在编译书中的代码时未启用的优化阶段中执行的。

请注意,启用了任何类型优化的编译器甚至不会先执行goto cmp,因为它会知道这是不必要的。尝试编译你的代码并启用优化(你说你使用gcc,给它-O2标志),看看它会有多大的不同。

答案 1 :(得分:2)

你没有从教科书中引用该函数的完整汇编语言体,但是我的精神力量告诉我它看起来像这样(为了清楚起见,我还用标签替换了字面地址):< / p>

    # ... establish stack frame ...

    mov    DWORD PTR [rbp-4],0x0
    cmp    DWORD PTR [rbp-4],0x9
    jle    .L0
.L1:
    mov    rdi, .Lconst0
    call   puts
    add    DWORD PTR [rbp-0x4],0x1
    cmp    DWORD PTR [rbp-0x4],0x9
    jle    .L1
.L0:

    # ... return from function ...

GCC注意到它可以通过将无条件cmp替换为循环底部的jle来消除最初的jmpcmp,因此它就是这样做的。这是一个名为loop inversion的标准优化。显然,即使优化器关闭,它也能做到这一点;在优化的情况下,它也会注意到初始比较必须是假的,提升地址加载,将循环索引放在寄存器中,并转换为倒计时循环,这样它就可以完全消除cmp ;像这样的东西:

    # ... establish stack frame ...

    mov    ebx, 10
    mov    r14, .Lconst0
.L1:
    mov    rdi, r14
    call   puts
    dec    ebx
    jne    .L1

    # ... return from function ...

(上面的内容实际上是由Clang生成的。我的GCC版本做了别的事,equally sensible but harder to explain。)