最近在看《深入理解计算机系统》,发现汇编挺有趣。
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