繁体   English   中英

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

[英]Difference in for loops of old and new GCC's generated assembly code

我正在阅读有关汇编代码的章节,其中有一个例子。 这是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循环中一样。

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

让我们来看看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标志),看看它后面会有多大的不同。

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

    # ... 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注意到它可以通过将无条件的jmp替换为循环底部的cmp来消除初始cmpjle ,这就是它所做的。 这是一种称为循环反转的标准优化。 显然,即使优化器关闭,它也能做到这一点; 在优化的情况下,它也会注意到初始比较必须是假的,提升地址加载,将循环索引放在寄存器中,并转换为倒计时循环,这样它就可以完全消除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版本做了别的事, 同样合理但难以解释 。)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM