2014-01-15 27 views
3

我正在閱讀關於彙編代碼的一章,它有一個示例。這裏是C程序:舊的和新的GCC生成的彙編代碼的循環差異

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 <[email protected]> 
    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循環。

+0

哪裏調用'你在書中提供的代碼puts'? –

+0

「我的版本」究竟是什麼?你寫了嗎?無法真正告訴你爲什麼你這樣寫的;) –

+2

@John Wu:「他的版本」可能是他的編譯器的輸出。 –

回答

2

你沒有從你的教科書中引用函數的完整彙編語言體,但是我的精神力量告訴我它看起來像這樣(爲了清楚起見,我用標號代替了文字地址):

# ... 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,所以這是它的所作所爲。這是一個稱爲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 ... 

(以上實際上是由鏘生成My GCC的版本做了別的東西,equally sensible but harder to explain。)

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標誌),看看它後面會有多大的不同。