程序的机器级表示-控制
3.6.1 条件码
除了整数寄存器,CPU还维护着一组单个位的条件码(condition code)寄存器。常用的条件码有:
- CF: 进位标志。最近的操作使最高位产生了进位。可以用来检查无符号操作数的溢出。
- ZF: 零标志。最近的操作得出的结果为0;
- SF: 符号标志。最近的操作得到的结果为负数。
- OF: 溢出标志。最近的操作导致一个补码溢出(正溢出或负溢出)
3.6.2 访问条件码
条件码通常不会直接读取,常用的使用方法有三种:
- 可以根据条件码的某个组合,将一个字节设置为0和或者 1;
- 可以条件跳转到程序的某个其他的部分;
- 可以有条件地传送数据。
3.6.3 跳转指令及其编码
正常执行的情况下,指令按照它们出现的顺序一条一条地执行。跳转指令会导致 执行切换到程序中一个全新的位置。在汇编代码中,这些跳转的目的通常用一个标号(label)指明。
movl $0,%eax
jmp .L1
movl (%eax), %edx
.L1:
popl %edx
jmp .L1 会导致程序跳过movl指令,从popl指令开始继续执行。
- jmp *%eax: 用寄存器%eax中的值作为跳转目标
- jmp *(%eax): 以%eax中的值作为读地址,从存储器中读出跳转目标。
3.6.4 翻译条件分支
将条件表达式和语句从C语言翻译成机器码,最常用的方式是结合有条件和无条件跳转。 示例:
//原始c代码
int absdiff(int x, int y){
if(x < y){
return y - x;
}else{
return x - y;
}
}
//等价的goto版本 c代码
int gotodiff(int x, int y){
int result;
if(x > = y){
goto x_ge_y;
}
result = y - x;
goto done;
x_ge_y:
result = x - y;
done:
return result;
}
// 产生的汇编代码:
// x at %ebp + 8, y at %ebp + 12
movl 8(%ebp), %edx // Get x
movl 12(%ebp),%eax // Get y
cmpl %eax, %edx // Compare x:y
jge .L2 //if >= goto x_ge_y
subl %edx, %eax //Compute result = y - x, 结果放到eax中
jmp .L3
.L2:
sub %eax, %edx // Compute result = x - y, 结果放到edx中
movl %edx, %eax // result 放到eax中作为返回值
.L3: // done: Begin completion code
3.6.5 循环
C语言提供了多种循环结构,即do-while、while和for。汇编中没有相应的指令存在,可以用条件测试和跳转组合来实现循环效果。大多数汇编器根据一个循环的do-while形式来产生循环代码,即使在实际程序这种形式用的相对较少。其他的循环会首先转换成do-while形式,然后再编译成机器码。
1.do-while循环
do-while语句的通用格式:
do
body-statement
while(test-exp)
body-statement至少会执行一次,test-exp的值如果是非零,则继续执行body-statement。 do-while的通用形式可以翻译成如下所示的条件和goto语句:
loop:
body-statement
t = test-expr;
if(t)
goto loop;
// c代码 求n!
int fact_do(int n){
int result = 1;
do{
result *= n;
n = n -1;
}while(n > 1)
return result;
// 寄存器使用
寄存器 变量 初始值
%eax result 1
%edx n n
// 汇编代码
//Argument: n at %ebp + 8
//Registers: n in %edx, result in %eax
movl 8(%ebp), %eax //Get n
movl $1, %eax // Set result = 1
.L2:
imull %edx, %eax //Compute result *= n
subl $1, %edx // Decrement n
cmpl $1, %edx // Compare n:1
jg .L2 // If > , goto loop
}
2.while循环
通用形式:
while(test-expr){
body-statement
}
翻译成do-while:
if(!test-expr){
goto done;
}
do
body-statement
while(test-expr);
done:
翻译成goto:
t = test-expr;
if(!t)
goto done;
loop:
body-statement
t = test-expr;
if(t)
goto loop;
done:
示例:
// c代码求n!
int fact_while(int n){
int result = 1;
while(n > 1){
result *= n;
n = n - 1;
}
}
// goto 版本
int fact_while_goto(int n){
int result = 1;
if (n <= 1)
goto done;
loop:
result *= n;
n = n - 1;
if(n > 1)
goto loop;
done:
return result;
}
// 汇编代码
// Argument: n at %ebp + 8
// Registers: n in %edx, result in %eax
movl 8(%ebp), %eax // Get n
movl $1, %eax // Set result = 1
cmpl $1, %edx // Compare n:1
jle .L7 // If <=, goto done
.L10: // loop
imull %edx, %eax // Compute result *= n
subl $1, %edx // Decrement n
cmpl $1, %edx // Compare n:1
jg .L10 // If > , goto loop
.L7: // done
Return result
3.for循环
通用形式:
for(init-expr; test-expr; update-expr)
body-statement
转成while形式:
init-expr;
while(test-expr){
body-statement;
update-expr;
}
转成do-while形式:
init-expr;
if(!test-expr)
goto done;
do{
body-statement
update-expr;
}while(test-expr)
done:
转成goto形式:
init-expr;
t = test-expr;
if(!t)
goto done;
loop:
body-statement
update-expr;
t = test-expr;
if(t)
goto loop;
done:
3.6.6 条件传送指令
实现条件操作的传统方法是利用控制的条件转移。当条件满足时,程序沿着一条 执行路径进行,而当条件不满足时,就走另一条路径。这种机制简单而通用,但是在现代处理器上,它可能会非常的低效率。
数据的条件转移是一种替代的策略。这种方法先计算一个条件操作的两个结果,然后再根据条件是否满足从而选取一个。只有在一些受限制的情况下,这种策略才可行,但是如果可行,就可以用一条简单的条件传送指令来实现它。 条件传送指令更好地匹配了现代处理器的特性。
基于条件传送的代码比基于条件控制转移的代码(cmpl 和jmp ,C语言则是if-else)性能好。为了理解其中的原因,必须了解一些现代处理器如何运行的知识。处理器通过使用流水线(pipelining)来获得高性能。在流水线中,一条指令的处理要经过一系列的阶段,每个阶段执行所需操作的一小部分(如:从存储器中取指令,确定指令类型,从存储器中读取数据,执行算术运算,向存储器中写数据,以及更新程序计数器)这种方法通过重叠连续指令的步骤来获得高性能。例如,在取一条指令的时候,执行它前面一套指令的算术运算。要做到这一点,要求能够事先确定执行指令的序列,这样才能保持流水线中充满了待执行的指令。当机器遇到条件跳转(也称“分支”)时,它常常还不能够确定是否会进行跳转。处理器采用非常精密的分支预测逻辑视图猜测每条跳转指令是否会执行。只要它的猜测还比较可靠(现代微处理器设计试图达到90%以上的成功率),指令流水线中就会充满指令。另一方面,错误预测一个跳转要求处理器丢掉它为该跳转指令后所有指令已经做了的工作,然后再开始从正确位置处起始的指令去填充流水线。这样一个错误预测会招致严重的惩罚,大约20~40个时钟周期的浪费,导致程序性能严重下降。
控制流不依赖数据,可使得处理器更容易保持流水线是满的。
3.6.7 switch
switch通过使用跳转表(jump table)这种数据结构使得实现更加高效。 跳转表是一个数组,表项i是一个代码段的地址,这个代码段实现当开关索引值 等于i时程序应该采取的动作。和一组很长的if-else相比,使用跳转表的优点是执行开关语句的时间与开关情况的数量无关。
示例:
// c代码
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;
}
// 翻译到拓展的C语言
int switch_eg_impl(int x, int y){
/*Table of code pointer*/
static void *jt[7] = {&&loc_A, &&loc_def, &&loc_B,
&&loc_C, &&loc_D, &&loc_def, &&loc_D};
unsigned index = n - 100;
int result;
if(inex > 6){
goto loc_def;
}
// 多个分支
goto *jt[index];
loc_def: // Default case
result = 0;
goto done;
loc_C: // Case 103
result = 0;
goto rest;
loc_A:
result = x * 13;
goto done;
loc_B: // Case 102
result = x + 10;
rest:
result += 11;
goto done;
loc_D: // case 104, 106
result = x * x;
done:
return result;
}