3.6.2 访问条件码

3.6.2 访问条件码

条件码通常不会直接读取,常用的使用方法有三种:1)可以根据条件码的某种组合,将一个字节设置为 0 或者 1,2)可以条件跳转到程序的某个其他的部分,3)可以有条件地传送数据。对于第一种情况,图 3-14 中描述的指令根据条件码的某种组合,将一个字节设置为 0 或者 1。我们将这一整类指令称为 SET 指令;它们之间的区别就在于它们考虑的条件码的组合是什么,这些指令名字的不同后缀指明了它们所考虑的条件码的组合。这些指令的后缀表示不同的条件而不是操作数大小,了解这一点很重要。例如,指令 setlsetb 表示“小于时设置(set less)”和“低于时设置(set below)”,而不是“设置长字(set long word)”和“设置字节(set byte)”。

一条 SET 指令的目的操作数是低位单字节寄存器元素(图 3-2)之一,或是一个字节的内存位置,指令会将这个字节设置成 0 或者 1。为了得到一个 32 位或 64 位结果,我们必须对高位清零。一个计算 C 语言表达式 a < b 的典型指令序列如下所示,这里 ab 都是 long 类型:

指令 同义名 效果 设置条件
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 低于或相等(无符号 <=)

图 3-14 SET 指令。每条指令根据条件码的某种组合,将一个字节设置为 0 或者 1。有些指令有“同义名”,也就是同一条机器指令有别的名字

int comp(data_t a, data_t b)
a in %rdi, b in %rsi

1  comp:
2      cmpq    %rsi, %rdi       Compare a:b
3      setl    %al              Set low-order byte of %eax to 0 or 1
4      movzbl  %al, %eax        Clear rest of %eax (and rest of %rax)
5      ret

注意 cmpq 指令的比较顺序(第 2 行)。虽然参数列出的顺序先是 %rsi(b)再是 %rdi(a),实际上比较的是 a 和 b。还要记得,正如在 3.4.2 节中讨论过的那样,movzbl 指令不仅会把 %eax 的高 3 个字节清零,还会把整个寄存器 %rax 的高 4 个字节都清零。

某些底层的机器指令可能有多个名字,我们称之为“同义名(synonym)”。比如说,setg(表示“设置大于”)和 setnle(表示“设置不小于等于”)指的就是同一条机器指令。编译器和反汇编器会随意决定使用哪个名字。

虽然所有的算术和逻辑操作都会设置条件码,但是各个 SET 命令的描述都适用的情况是:执行比较指令,根据计算 t = a - b 设置条件码。更具体地说,假设 a、b 和 t 分别是变量 a、b 和 t 的补码形式表示的整数,因此 t = a -tw b,这里 w 取决于 a 和 b 的大小。

来看 sete 的情况,即“当相等时设置(set when equal)”指令。当 a = b 时,会得到 t = 0,因此零标志置位就表示相等。类似地,考虑用 setl,即“当小于时设置(set when less)”指令,测试一个有符号比较。当没有发生溢出时(OF 设置为 0 就表明无溢出),我们有当 a -tw b < 0 时 a < b,将 SF 设置为 1 即指明这一点,而当 a -tw b >= 0 时 a >= b,由 SF 设置为 0 指明。另一方面,当发生溢出时,我们有当 a -tw b > 0(负溢出)时 a < b,而当 a -tw b < 0(正溢出)时 a > b。当 a = b 时,不会有溢出。因此,当 OF 被设置为 1 时,当且仅当 SF 被设置为 0,有 a < b。将这些情况组合起来,溢出和符号位的 EXCLUSIVE-OR 提供了 a < b 是否为真的测试。其他的有符号比较测试基于 SF ^ OF 和 ZF 的其他组合。

对于无符号比较的测试,现在设 a 和 b 是变量 a 和 b 的无符号形式表示的整数。在执行计算 t = a - b 中,当 a - b < 0 时,CMP 指令会设置进位标志,因而无符号比较使用的是进位标志和零标志的组合。

注意到机器代码如何区分有符号和无符号值是很重要的。同 C 语言不同,机器代码不会将每个程序值都和一个数据类型联系起来。相反,大多数情况下,机器代码对于有符号和无符号两种情况都使用一样的指令,这是因为许多算术运算对无符号和补码算术都有一样的位级行为。有些情况需要用不同的指令来处理有符号和无符号操作,例如,使用不同版本的右移、除法和乘法指令,以及不同的条件码组合。

练习题 3.13 考虑下列的 C 语言代码:

int comp(data_t a, data_t b) {
    return a COMP b;
}

它给出了参数 a 和 b 之间比较的一般形式,这里,参数的数据类型 data_t(通过 typedef)被声明为表 3-1 中列出的某种整数类型,可以是有符号的也可以是无符号的,COMP 通过 #define 来定义。

假设 a 在 %rdi 中某个部分,b 在 %rsi 中某个部分。对于下面每个指令序列,确定哪种数据类型 data_t 和比较 COMP 会导致编译器产生这样的代码。(可能有多个正确答案,请列出所有的正确答案。)

A.  cmpl  %esi, %edi
    setl  %al

B.  cmpw  %si, %di
    setge %al

C.  cmpb  %sil, %dil
    setbe %al

D.  cmpq  %rsi, %rdi
    setne %al

练习题 3.14 考虑下面的 C 语言代码:

int test(data_t a) {
    return a TEST 0;
}

它给出了参数 a 和 0 之间比较的一般形式,这里,我们可以用 typedef 来声明 data_t,从而设置参数的数据类型,用 #define 来声明 TEST,从而设置比较的类型。对于下面每个指令序列,确定哪种数据类型 data_t 和比较 TEST 会导致编译器产生这样的代码。(可能有多个正确答案,请列出所有的正确答案。)

A.  testq %rdi, %rdi
    setge %al

B.  testw %di, %di
    sete  %al

C.  testb %dil, %dil
    seta  %al

D.  testl %edi, %edi
    setne %al