最近在看《深入理解計算機系統》,發現匯編挺有趣。
1.條件分支:if語句
下面是一個簡單的ifelse函數:
int absdiff(int x, int y) { if (x < y) return y - x; else return x - y; }
對這個程序使用如下命令,得到匯編程序,(注意-S選項大寫,並且始終用-O1優化選項)
gcc -S ifelse.c -o ifelse.s –O1
可以看到gcc對改程序的翻譯與書上略有不同:
pushl %ebx .cfi_def_cfa_offset 8 .cfi_offset 3, -8 movl 8(%esp), %ecx movl 12(%esp), %edx movl %edx, %eax subl %ecx, %eax movl %ecx, %ebx subl %edx, %ebx cmpl %edx, %ecx cmovge %ebx, %eax popl %ebx
gcc中,%ecx: x, %edx:y , %eax: y-x, %ebx: x-y. 比較x與y,若x>=y, %eax: x-y. 最終在%eax中存放result。
其中,cmovge使用了后面將要講到的 條件傳送指令,即先計算一個條件操作的兩種結果,然后再根據條件是否滿足而選取一個。它要求處理器類型在i686以上,在gcc中可以添加'-march=i686'來編譯,但是ubuntu11.10的處理器類型就是i686的(使用uname –p查看),所以上面的編譯直接得到采用條件傳送指令的匯編代碼。
使用條件傳送並不總是能改進代碼效率,對GCC來說,只有很容易計算時(如只有一條加法指令),它才使用條件傳送指令。
【題外話】:
下面的語句產生條件傳送的匯編代碼:
int arith(int x){ return x / 4; }
使用-O1選項產生匯編代碼如下:
.cfi_startproc movl 4(%esp), %eax //get x leal 3(%eax), %edx //temp = x+3 testl %eax, %eax cmovs %edx, %eax //if(x < 0) x = temp sarl $2, %eax // return x >> 2 ret .cfi_endproc
可以看到,如果是負數,在算術右移時,要加上2^k-1=3的偏置。注意,這里加偏置的原因:一般來說,我們可以直接對補碼進行右移操作表示2^k冪,但是真正的除法與補碼右移還是有一定區別的:
真正除法一定是舍入到0,所以-2.5得到-2;補碼右移則會向下舍入,所以-2.5會得到-3(因為它總是把低位丟棄);
所以,在做真正除法時會加上一個偏置值,(原來CS:APP第65頁2.3.7節講到了這個問題,哎,可惜跳過去了。。)
int i = -9; cout << i/4 << endl; //get -2 cout << (i>>2) << endl; //get -3
-9的右移過程如下:得到原碼1001——轉為補碼0111——右移兩位1101——轉為原碼0011,即得到-3。
-9+偏置3過程: -6原碼 0110——轉為補碼1010——右移兩位1110——轉為原碼0010,得到-2.
2.循環
2.1 do-while循環的翻譯
匯編中的循環使用 條件測試和跳轉 組合起來實現。大部分編譯器根據do-while形式產生循環代碼,如下求階乘的循環代碼:
int fact_do(int n) { int result = 1; do{ result *= n; n = n-1; }while(n>1); return result; }
產生匯編如下:
.cfi_startproc movl 4(%esp), %edx //get n movl $1, %eax //set result=1 .L2: imull %edx, %eax // result *= n subl $1, %edx //n-- cmpl $1, %edx //compare n-1 jg .L2 //if(n>1): goto .L2 rep ret .cfi_endproc
2.2 for循環的翻譯
// Step1: for循環語句 for(init-expr; test-expr; update-expr) body-statement; // Step2: while循環語句 init-expr; while(test-expr){ body-statement; update-expr; } // Step3: do-while循環語句 init-expr; if(!test-expr) goto done do{ body-statement; update-expr; }while(test-expr); done: // Step4: goto語句(直觀的展示了匯編代碼實現) init-expr; if(!test-expr) goto done loop: body-statement; update-expr; if(test-expr) goto loop; done:
帶continue語句時的特例(練習3.24):
i = 0; while(i < 10){ if(i&1) continue; //continue在i++之前,阻止了i的更新 sum += i; i++; } i = 0; if(i >= 10) goto done do{ if(i&1) continue; //continue在i++之前,阻止了i的更新 sum += i; i++; }while(i < 10); done:
do-while循環的continue語句還有一個問題要注意:
翻譯為do-while循環時出現了問題,關鍵是continue的含義是不執行循環體內的內容,直接到達下一個循環點(也就是while處的判斷,而不是“do{”處),所以下面語句只會輸出1.
int i = 1; do{ printf("%d\n", i); i++; if(i<15) continue; }while(0);
使用goto語句來保證while循環的更新(寫代碼時,直接在continue前加一個i++即可):
while(i < 10){ if(i&1) goto next; sum += i; next: i++; }
3.switch語句
對switch的匯編,GCC會根據開關數量和稀少程度選擇是否使用 跳轉表 來翻譯開關語句。跳轉表是一個數組,表項i是代碼短的地址,其執行時間與開關情況的數量無關。如下switch語句:
int switch_eg(int x, int n){ int result = x; switch(n){ case 100: result *= 13; break; case 102: result += 10; case 103: result += 11; break; case 104: case 106: result *= result; break; default: result = 0; } return result; }
使用-O1翻譯成匯編為:
.cfi_startproc movl 4(%esp), %eax movl 8(%esp), %edx subl $100, %edx cmpl $6, %edx ja .L8 jmp *.L7(,%edx,4) .section .rodata .align 4 .align 4 .L7: .long .L3 .long .L8 //case 101: default .long .L4 .long .L5 .long .L6 .long .L8 //case 105: default .long .L6 .text .L3: //case 100: result *= 13 leal (%eax,%eax,2), %edx // get 3*x leal (%eax,%edx,4), %eax //get x+4*(3x)= 13*x ret .L4: //case 102: result += 10 addl $10, %eax .L5: //case 103: result += 11 addl $11, %eax ret .L6: //case 104/106: result *= result imull %eax, %eax ret .L8: //default: result = 0 movl $0, %eax ret .cfi_endproc