条件码
除了整数寄存器,cpu还维护着一组单个条件码的寄存器,他们用来描述最近算术和逻辑操作的属性。可以检测这些寄存器来执行分支指令。常见的条件码有:
CF:进位标志。最近的操作产生了最高位产生了进位,可以检查无符号数操作的溢出。
ZF:0标志,最近操作结果为0.
SF:符号标志。最近的操作结果得到了负数。
OF:最近的操作产生了补码溢出(正溢出或者负溢出)
可以举例子说明比如一条ADD指令 t = a + b。
(unsigned)t < (unsigned)a 得出 无符号整数加法溢出了 CF设置了
t = 0 得出 ZF标志设置 产生0了
t < 0 得出 SF标志设置 操作结果得到负数
(a < 0 == b<0) AND (t < 0 != a<0) 可以得出产生了溢出 设置OF。
证明下这个表达式 当产生溢出的时候肯定都是a,b同符号。分两种情况。
- 当a > 0,b>0 如果产生溢出,那么 t <= 0
2.当a< 0,b<0 如果负溢出,那么 t >=0
表达式拆分后刚好是这两种情况。
leaq指令不改变条件码,因为是进行地址计算的(这里只能记住就这样了)。 其他指令会修改条件码。
xor指令,CF标志和OF标志都会被设置,移位操作,进位标志被设置成最后一个被移出的位,溢出操作被设置成0。INC和DEC指令会设置OF标志和ZF标志不会改变CF标志。
除了上述这些指令还有两类指令CMP和TEST指令只改变条件不改变其他寄存器。
- CMP , 基于 - 比较
- TEST , 基于 & 测试
CMP指令和SUB行为一样的,如果两个数相等ZF会设置成1,其他标志可以确定两个操作数之间的大小关系。
TEST指的行为和AND指令一样。常用的用法是 testq %rax,%rax来检查%rax是否是负数,零还是正数。或者一个操作码位掩码来指示那些位需要被测试。
访问条件码
条件码一般不会直接被读取,具体应用的形式
- 根据条件码的某种组合,把一个字节设置成0或者1。比如SET指令的应用。
int c = a > b;
反汇编后的结果:
cmp -0x8(%rbp),%eax #比较 a > b
setg %al #如果大于的话 设置%al为1 否则设置0
movzbl %al,%eax #拓展字节到双字
setl 表示 无溢出负数SF&~OF 或者负溢出结果为正数~SF&OF。合并两个表达式SF^OF.
setle 表示 (SF^OF) | ZF
setg以及setge分别对上面的表达式取反即可。
跳转指令编码
跳转指令一般都是相对pc指针的编码的。
if(a > 0)
a= a +1;
反汇编结果如下:
83 7d fc 00 cmpl $0x0,-0x4(%rbp)
7e 04 jle e <main+0xe>
83 45 fc 01 addl $0x1,-0x4(%rbp)
第二行的7e 04 7e表示jle指令,04表示pc指针+4,不难看出第三行指令占用了4个字节,所以刚好跳转到下一条指令。这段目标代码可以不做改变就可以移动到内存的任意位置。
用条件控制来实现分支
将c语言的表达式翻译成汇编,最常见的方式是结合有条件和无条件跳转。
c语言中 if-else 通用模板为
if (test-exp)
then-statement
else
else-statement
对于这种形式,汇编实现往往会用这种形式
t = test-exp
if (!t)
goto false;
then-statement
goto done;
false:
else-statement
done:
用条件传送实现条件分支
实现条件操作的传统方法是使用控制的条件转移,当条件满足时沿一条执行路径走,不满足时沿另一条路径走。这种方式很低效。一种替代策略是使用“数据的条件转移”,这种方法计算条件操作的两个结果,在 根据条件选择其中一个。
long absdiff(long x,long y){
long result;
if(x<y)
result = y-x;
else
result = x-y;
return result;
}
选择O0去编译得到结果
48 89 7d e8 mov %rdi,-0x18(%rbp)
48 89 75 e0 mov %rsi,-0x20(%rbp)
48 8b 45 e8 mov -0x18(%rbp),%rax
48 3b 45 e0 cmp -0x20(%rbp),%rax
7d 0e jge 24 <absdiff+0x24>
48 8b 45 e0 mov -0x20(%rbp),%rax
48 2b 45 e8 sub -0x18(%rbp),%rax
48 89 45 f8 mov %rax,-0x8(%rbp)
eb 0c jmp 30 <absdiff+0x30>
48 8b 45 e8 mov -0x18(%rbp),%rax
48 2b 45 e0 sub -0x20(%rbp),%rax
48 89 45 f8 mov %rax,-0x8(%rbp)
选择O1去编译得到的结果
48 89 f2 mov %rsi,%rdx
48 29 fa sub %rdi,%rdx
48 89 f8 mov %rdi,%rax
48 29 f0 sub %rsi,%rax
48 39 f7 cmp %rsi,%rdi
48 0f 4c c2 cmovl %rdx,%rax
可以观察到O0没有优化,是根据条件转移来控制分支的,O1下优化后,通过条件传送来实现分支的。
这么优化可以带来性能的提成,除了上述指令条数减少外。其实在cpu内部指令并不是串行的,而是每个阶段执行所需操作的一部分,这种重叠指令的方式来获取性能的提升。比如取一条指令的同时计算前面一条指令的算术运算。所以需要流水线里充满待执行的指令序列。当遇到条件跳转时,需要先获取值然后在能决定往哪里跳转。处理器会猜测一个方向,只要猜测可靠,那么流水线里就会充满指令。错误的预测会丢掉之前的工作,然后重新从开始正确的位置开始填充流水线,一个错误预测会有15到30个的时钟周期惩罚。
使用条件传送,可以保证流水线里充满着指令
用条件传送法编译出来的抽象代码描述
v = then-state
ve = else-state
t = test-exp
if(!t)
v = ve
网友评论