程序的机器级表示--条件码
条件码
条件码表
标志 | 含义 | 描述 |
---|---|---|
CF | 进位标志(Carry Flag) | 最近的操作是最高位产生了进位。可用来检测无符号操作的溢出。 |
ZF | 零标志(Zero Flag) | 最近的操作产生了0。 |
SF | 符号标志(Signal Flag) | 最近的操作得到的结果为负数。 |
OF | 溢出标志(Overflow Flag) | 最近的操作导致一个补码的溢出。 |
leap(地址加载)指令不改变任何条件码,用来进行地址计算。
运算指令改变寄存器同时也会设置条件码。
例如:
XOR,进位与溢出标志会设置成0。
移位操作会将进位标志设置为最后一个被移出的位,而溢出标志设为0。
INC和DEC指令会设置溢出和零标志,但是不会改变进位标志。
比较与测试指令
指令 | 基于 | 描述 |
---|---|---|
CMP S2,S1<br />cmpb<br />cmpw<br />cmpl<br />cmpq | S2-S1 | 比较<br />比较字节<br />比较字<br />比较双字<br />比较四字 |
Test S1,S2<br />testb<br />testw<br />testl<br />testq | S1&S2 | 测试<br />测试字节<br />测试字<br />测试双字<br />测试四字 |
上表中的两类指令(8,16,24,36位形式)只设置条件码,不改变任何寄存器。例如CMP指令是通过两个操作数之差来设置条件码,除了不更新寄存器之外CMP的操作与Sub的操作是一样的,而Test与And指令是一样的。
条件码的访问与使用
使用方法有三种:
-
根据条件码的某种组合,将一个字节设置位0或1(SET指令)。SET指令的目的操作数是低位单字节寄存器元素,或是一个字节的内存位置,指令将其设置0或1,同时对高位清零。
指令 同义名 效果 设置条件 sete D setz D<-ZF 相等/零 setne D setnz D <- ~ZF 不等/非零 sets D D <- SF 负数 setns D D <- ~SF 非负数 setg D setnle D <- ~(SF ^ OF)&~ZF 大于(有符号>) setge D setnl D <- ~(SF^OF) 大于等于(有符号>=) setl D setnge D <- SF^OF 小于(有符号<) setle D setng D <- (SF^OF)|ZF 小等于(有符号<=) seta D setnbe D <- CF&ZF 超过(无符号>) setae D setnb D <- ~CF 超过或相等(无符号>=) setb D setnae D <- CF 低于(无符号<) setbe D setna D <- CF|ZF 低于或相等(无符号<=) set指令的操作流程:
- 执行比较指令
- 根据计算t=a-b设置
int comp(data_t a,data_t b)
a in %rdi, b in %rsi
comp:
cmpq %rsi, %rdi Compare a :b
setl %al Set Low-order byte of %eax to 0 or 1
movezbl %al, %eax Clear rest of %eax(and rest of %rax)
ret
同c语言不同,机器代码不会将每个程序值都和一个数据类型联系起来。机器代码大部分对有符号和无符号的两种情况使用一样的指令。
跳转到某部分代码
有条件的传送数据
程序的跳转
正常程序是顺序执行,但是跳转指令会跳转另一段代码的位置。类似c语言里面的goto,跳转后的位置会用label指明。
跳转主要分:
- 直接跳转:跳转目标是作为指令的一部分编码的。
- 间接跳转:跳转目标是从内存或者寄存器中读出的。间接跳转的写法是*后面跟一个操作指示符,例如jmp*%rax就是从rax寄存器中的内容作为跳转目标,jmp *(%rax)即以%rax中的值作为读地址,从内存中读出跳转目标。
jmp指令集
指令 | 同义名 | 跳转条件 | 描述 |
---|---|---|---|
jmp Label | 1 | 直接跳转 | |
jmp *Operand | 1 | 间接跳转 | |
de Label | jz | ZF | 相等/零 |
jne Label | jnz | ~ZF | 不相等/非零 |
js Label | SF | 负数 | |
jns Label | ~SF | 非负数 | |
jg Label | jnle | (SF^OF)&ZF | 大于(有符号>) |
jge Label | jnl | ~(SF^OF) | 大于或等于(有符号>=) |
jl Label | jnge | SF^OF | 小于(有符号<) |
jle Label | jng | (SF^OF)|ZF | 小于或等于(有符号<=) |
ja Label | jnbe | CF&ZF | 超过(无符号>) |
jae Label | jnb | ~CF | 超过或相等(无符号>=) |
jb Label | jnae | CF | 低于(无符号<) |
jbe Label | jna | CF|ZF | 低于或相等(无符号<=) |
上表中的跳转指令都是有条件的--它们根据条件码的组合,或者跳转,或者继续执行代码序列中下一跳指令。条件跳转只能是直接跳转。
跳转指令的编码分为PC绝对与PC相对。
PC相对:目标指令与紧跟在跳转指令后面那条指令的地址之间的差作为编码。
0: 48 89 f8 mov %rdi,%rax
3: eb 03 jmp 8 <loop+0x8>
5: 48 d1 f8 sar %rax
8: 48 85 c0 test %rax, %rax
b: 7f f8 jg 5 <loop+0x5>
d: f3 c3 repz retq
movq %rdi,%rax
jmp .L2
.L3:
sarq %rax
.L2:
testq %rax
jg .L3
rep;ret
第2行下一条指令的位置是5,相对位置是3,5+3=8,即跳转到8的位置上。
rep与repz除了在AMD上运行的更坏之外,不会改变代码的其他行为,不用管。
PC绝对:用4个字节直接指定目标。
条件分支
条件表达式和语句从c语言翻译成机器代码,最常用的是结合有条件和无条件跳转。条件跳转主要有两种实现方式:
- 控制的条件转移
- 数据的条件转移
控制的条件转移
c语言里面的if-else语句如下:
if (condition)
then-statement;
else
else-statement;
翻译成汇编会先上面语句转换成以下形式:
t = condition;
if (!t)
goto false;
then-statement;
goto done;
false:
else-statement;
done:
再转换为汇编形式。
举个例子:
c版本:
long func(long x,long y){
long result;
if (x < y)
result = y - x;
else{
result = x - y;
}
return result;
}
转换后的版本:
long func(long x, long y){
long result;
if (x >= y)
goto x_ge_y;
result = y - x;
return result;
x_ge_y:
result = x - y;
return result;
}
汇编版本:
long func(long x, long y)
x in %rdi, y in %rsi
func:
cmpq %rsi,%rdi
jge .L2
movq %rsi, %rax
subq %rdi, %rax
ret
.L2
movq %rdi, %rax
subq %rsi, %rax
ret
数据的条件分支
处理器通过流水线获取高性能,一条指令要经过一些列阶段,每个阶段执行所需操作的一部分(例如,从内存中取指令,确定指令类型,从旁那个内存读数据,执行算数运算,想内存写数据,以及更新程序计数器)。这种方法通过重叠连续指令的步骤来获取高性能,例如,在取一条指令的同时,执行前一条指令的计算。处理器需要事先确定要执行的指令序列才能使流水线中保持充满待执行的指令。当遇到调制分支的时候,只要分支条件求知完成后才能决定分支的走向。现代处理器采用非常精密的分支预测逻辑来预测走向,但是只要预测错误就得丢弃为预测的指令所准备的工作,再从正确的位置重新加载,这样一个错误会导致程序性能严重下降。只要控制流不依赖数据,才能使流水线一直是满的。
下表列举了一些可用的条件传送指令。每个指令有两个操作数:源寄存器或者内存地址S,和目的寄存器R。指令的结果取决于条件码的值,只有在条件满足的时候,源寄存器的值才会被复制到目的寄存器中。
指令 | 同义名 | 传送条件 | 描述 |
---|---|---|---|
cmove S, R | cmovz | ZF | 相等/零 |
cmovne S, R | cmovnz | ~ZF | 不相等/非零 |
cmovs S, R | SF | 负数 | |
cmovns S, R | ~SF | 非负数 | |
cmovg S, R | cmovnle | (SF^OF)&ZF | 大于(有符号) |
cmovge S, R | cmovnl | ~(SF^OF) | 大等于(有符号) |
cmovl S, R | cmovnge | SF^OF | 小于(有符号) |
cmovle S, R | cmovng | (SF^OF)|ZF | 小等于(有符号) |
cmova S, R | cmovnbe | CF&ZF | 超过(无符号) |
cmovae S, R | cmovnb | ~CF | 超过或相等(无符号) |
cmovb S, R | cmovnae | CF | 低于(无符号) |
cmovbe S, R | cmovna | CF|ZF | 低于或等于(无符号) |
同条件跳转不同,数据跳转无需预测测试的结果就可以执行条件传送。
- 处理器只是读源值(可能从内存)
- 检查条件码
- 要么更新目的寄存器,要么不变
数据传送表达式v = a ? b : c
会被转换为类似以下表达式:
v = then-statement;
ve = else-statement;
t = condition;
if (!t) v = ve;
但是条件数据传送也不是万能的,上面两条语句只要一条产生panic就会导致程序崩溃。并且也不总是提高代码的效率,比如上述两个表达式计算量比较大的时候,条件不满足就白计算了,甚至代价比预测错分支还大,条件数据传送还是比较受限制的情况下好用。
循环
循环主要基于两种形式:
- do-while循环。
- while循环(while内部也有以下两种写法)
- while
- for
do-while循环
do-while的通用写法如下:
do
body
while(condition)
翻译成伪代码就是以下形式:
loop:
body;
t = condition;
if (t)
goto loop;
举个例子,有以下c函数:
long func(long n){
long result = 1;
do{
result *= n;
n--;
}while(n > 1);
return result;
}
翻译成等价无条件跳转+有条件跳转的形式:
long func(long n){
long result = 1;
loop:
result *= n;
n--;
if (n > 1)
goto loop;
return result;
}
转换成汇编:
long func(long n)
n in %rdi
fact_do:
movl $1, %eax
.L2:
imuq %rdi, %rax
subq $1, %rdi
cmpq $1, %rdi
jg .L2
rep;ret
while循环
通用形式如下:
while(condition)
body;
转换后形式如下:
goto test;
loop:
body;
test:
t = condition;
if (t)
goto loop;
可以看到与do-while不同的是,第一次条件判断如果没通过,程序就直接跳出循环。
用do-while循环同一个例子举例。
c形式:
long func(long n){
long result = 1;
while(n > 1);{
result *= n;
n--;
}
return result;
}
翻译成goto后的形式:
long func(long n){
long result = 1;
goto test;
loop:
result *= n;
n--;
test:
if (n > 1)
goto loop;
return result;
}
转换为汇编的形式:
long func(long n)
n in %rdi
fact_while:
movl $1, %eax
jmp .L5
.L6:
imuq %rdi, %rax
subq $1, %rdi
.L5:
cmpq $1, %rdi
jg .L2
rep;ret
还有一种翻译方法,当gcc使用o1优化等级的时候,会先转换成do-while的形式再转成汇编:
long func(long n){
long result = 1;
if(n <= 1)
goto done;
loop:
result *= n;
n--;
if (n != 1)
goto loop;
done:
return result;
}
汇编版本:
long func(long n)
n in %rdi
fact_while:
cmpq $1, %rdi
jle .L7
movl $1, %eax
.L6:
imuq %rdi, %rax
subq $1, %rdi
cmpq $1, %rdi
jne .L6
rep;ret
.L7:
movl $1, %eax
rep;ret
for循环
for循环其实是while的一种翻译形式,拿上面的例子举例:
long func(long n){
long i;
long result = 1;
for(i = 2;i <= n;i++){
result *= i;
}
return result;
}
翻译成while后的形式:
long func(long n){
long i = 2;
long result = 1;
while(i <= n);{
result *= i;
i++;
}
return result;
}