最近在看《深入理解计算机系统》,发现汇编挺有趣。
1.条件分支:if语句
下面是一个简单的ifelse函数:
int absdiff(int x, int y)
{if (x < y)return y - x;elsereturn x - y;
}
对这个程序使用如下命令,得到汇编程序,(注意-S选项大写,并且始终用-O1优化选项)
gcc -S ifelse.c -o ifelse.s –O1
可以看到gcc对改程序的翻译与书上略有不同:
pushl %ebx.cfi_def_cfa_offset 8.cfi_offset 3, -8movl 8(%esp), %ecxmovl 12(%esp), %edxmovl %edx, %eaxsubl %ecx, %eaxmovl %ecx, %ebxsubl %edx, %ebxcmpl %edx, %ecxcmovge %ebx, %eaxpopl %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_startprocmovl 4(%esp), %eax //get xleal 3(%eax), %edx //temp = x+3testl %eax, %eax cmovs %edx, %eax //if(x < 0) x = tempsarl $2, %eax // return x >> 2ret.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 -2cout << (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_startprocmovl 4(%esp), %edx //get nmovl $1, %eax //set result=1
.L2:imull %edx, %eax // result *= nsubl $1, %edx //n--cmpl $1, %edx //compare n-1jg .L2 //if(n>1): goto .L2repret.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_startprocmovl 4(%esp), %eaxmovl 8(%esp), %edxsubl $100, %edxcmpl $6, %edxja .L8jmp *.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 *= 13leal (%eax,%eax,2), %edx // get 3*xleal (%eax,%edx,4), %eax //get x+4*(3x)= 13*xret
.L4: //case 102: result += 10addl $10, %eax
.L5: //case 103: result += 11addl $11, %eaxret
.L6: //case 104/106: result *= resultimull %eax, %eaxret
.L8: //default: result = 0movl $0, %eaxret.cfi_endproc