簡體   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