前一章提到的非条件分支指令,在处理器遇到这种指令时,就会自动发生分支跳转操作。但是,条件分支指令则是根据当前EFLAGS寄存器的情况来决定是否要发生分支跳转操作...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第167页到第177页 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为177/ 577)。

Conditional Branches (条件分支指令):

    前一章提到的非条件分支指令,在处理器遇到这种指令时,就会自动发生分支跳转操作。但是,条件分支指令则是根据当前EFLAGS寄存器的情况来决定是否要发生分支跳转操作。

    EFLAGS寄存器有很多标志位,但是条件分支指令只关注其中的5个标志位:
  • Carry flag (CF) - 进位或借位标志,对应EFLAGS寄存器的位0即第1位标志
  • Overflow flag (OF) - 溢出标志,对应位11即第12位标志
  • Parity flag (PF) - 奇偶标志,对应位2
  • Sign flag (SF) - 正负符号标志,对应位7
  • Zero flag (ZF) - 结果是否为0标志,对应位6
    每种条件跳转指令都会检查对应的标志位,来确定是否要发生跳转操作,下面具体的描述下条件跳转指令。

Conditional jump instructions (条件跳转指令):

    有好几种条件跳转指令,这些指令的通用格式如下:

jxx address

    address是要跳转的目标地址(通常是用标签名来引用),xx是通配符,下表描述了所有可用的条件跳转指令:

Instruction
指令
Description
描述
EFLAGS
标志寄存器
JA Jump if above
如果目标操作数大于源操作数则跳转
(用于无符号操作数的比较)
CF=0 and ZF=0
JAE Jump if above or equal
如果目标大于等于源则跳转
(用于无符号操作数的比较)
CF=0
JB Jump if below
如果目标小于源则跳转
(用于无符号操作数的比较)
CF=1
JBE Jump if below or equal
如果目标小于等于源则跳转
(用于无符号操作数的比较)
CF=1 or ZF=1
JC Jump if carry
如果发生进位或借位则跳转
CF=1
JCXZ Jump if CX register is 0
如果CX寄存器的值为0则跳转
 
JECXZ Jump if ECX register is 0
如果ECX寄存器的值为0则跳转
 
JE Jump if equal
如果两操作数相等则跳转
ZF=1
JG Jump if greater
如果目标操作数大于源操作数则跳转
(用于有符号操作数的比较)
ZF=0 and SF=OF
JGE Jump if greater or equal
如果目标大于等于源则跳转
(用于有符号操作数的比较)
SF=OF
JL Jump if less
如果目标小于源则跳转
(用于有符号操作数的比较)
SF<>OF
JLE Jump if less or equal
如果目标小于等于源则跳转
(用于有符号操作数的比较)
ZF=1 or SF<>OF
JNA Jump if not above
如果目标不大于源则跳转
(用于无符号操作数的比较)
CF=1 or ZF=1
JNAE Jump if not above or equal
如果目标不大于等于源则跳转
(用于无符号操作数的比较)
CF=1
JNB Jump if not below
如果目标不小于源则跳转
(用于无符号操作数的比较)
CF=0
JNBE Jump if not below or equal
如果目标不小于等于源则跳转
(用于无符号操作数的比较)
CF=0 and ZF=0
JNC Jump if not carry
如果没发生借位或进位则跳转
CF=0
JNE Jump if not equal
如果两操作数不相等则跳转
ZF=0
JNG Jump if not greater
如果目标操作数不大于源操作数则跳转
(用于有符号操作数的比较)
ZF=1 or SF<>OF
JNGE Jump if not greater or equal
如果目标不大于等于源则跳转
(用于有符号操作数的比较)
SF<>OF
JNL Jump if not less
如果目标不小于源则跳转
(用于有符号操作数的比较)
SF=OF
JNLE Jump if not less or equal
如果目标不小于等于源则跳转
(用于有符号操作数的比较)
ZF=0 and SF=OF
JNO Jump if not overflow
如果没发生溢出则跳转
OF=0
JNP Jump if not parity
如果结果寄存器中位1的个数为奇数则跳转
PF=0
JNS Jump if not sign
如果结果最高符号位为0则跳转
SF=0
JNZ Jump if not zero
如果结果不为0则跳转
ZF=0
JO Jump if overflow
如果发生溢出则跳转
OF=1
JP Jump if parity
如果结果寄存器中位1的个数为偶数则跳转
PF=1
JPE Jump if parity even
如果结果寄存器中位1的个数为偶数则跳转
PF=1
JPO Jump if parity odd
如果结果寄存器中位1的个数为奇数则跳转
PF=0
JS Jump if sign
如果结果最高符号位为1则跳转
SF=1
JZ Jump if zero
如果结果为0则跳转
ZF=1

    有关无符号数和有符号数的内容将在后面的章节进行介绍。

    条件跳转指令通常采用标签名来表示要跳转的目标地址,该标签名在汇编时会在指令中转成一个偏移地址,不同的偏移地址对应不同的跳转类型,有两种可用的条件跳转类型:
  • Short jumps 短跳转
  • Near jumps 邻近跳转
    短跳转使用一个8位的有符号偏移值(有符号表示可以是正偏移值或负偏移值),而邻近跳转则使用一个16位或32位的有符号偏移值,这些偏移值会被加进指令指针寄存器中。

    注意:条件跳转指令不支持段内存模式(即段加偏移地址来访问内存的方式)的长跳转,也就是不能使用单一的jge之类的指令跳到另一个段去,只能先自己写几条代码判断当前是否符合跳转条件,如果符合条件,然后就用jmp之类的无条件跳转指令来跳到另一个段去。

    要使用条件跳转指令,你必须使用比较之类的指令先设置好EFLAGS寄存器里的标志位,然后再进行条件跳转,下面举例来说明条件跳转的使用。

The compare instruction (比较指令):

    比较指令就是通过两个值的比较结果来设置EFLAGS寄存器对应的标志位。

    比较指令即CMP指令对应的格式如下:

cmp operand1, operand2

    CMP指令会在内部将第二个操作数减去第一个操作数,即(operand2 - operand1) ,通过减法运算的结果来设置EFLAGS寄存器里的标志位,但是这两个操作数的值本身并不会被修改。

    这里需要特别注意的是,在Intel汇编中操作数的顺序是刚好相反的,只要记住是目标操作数减去源操作数即可,这个顺序问题常常让程序员花费很多时间去调试。

    下面通过cmptest.s的例子来看看比较指令是如何配合条件跳转指令来一起工作的:

# cmptest.s - An example of using the CMP and JGE instructions
.section .text
.globl _start
_start:
    nop
    movl $15, %eax
    movl $10, %ebx
    cmp %eax, %ebx
    jge greater
    movl $1, %eax
    int $0x80
greater:
    movl $20, %ebx
    movl $1, %eax
    int $0x80

    该例子先将立即数15赋值给EAX寄存器,同时将10赋值给EBX寄存器,接着通过cmp指令来比较两个寄存器的值,最后jge指令根据比较的结果来判断是否需要跳转:

cmp %eax, %ebx
jge greater

    由于EBX的值小于EAX,所以没发生跳转,处理器继续执行下一条指令,将1赋值给EAX,再调用int $0x80的linux系统调用来退出程序,你可以在编译后运行该程序,并通过echo回显的结果来进行判断:

$ ./cmptest
$ echo $?

10
$

    通过回显的退出码可知,EBX寄存器在退出时的值是10,确实没发生跳转,如果发生了跳转则结果会是20 。

    前面的例子是用CMP指令来比较两个寄存器的值,下面是CMP指令的一些其他示例:

cmp $20, %ebx   ; compare EBX with the immediate value 20
cmp data, %ebx  ; compare EBX with the value in the data memory location
cmp (%edi), %ebx  ; compare EBX with the value referenced by the EDI pointer

    上面第一个示例是将EBX和立即数20进行比较,第二个示例是将EBX和data内存位置的值进行比较,第三个示例是将EBX和EDI寄存器指针所引用的值进行比较。

Examples of using the flag bits 使用标志位的例子:

    前面例举了可用的条件跳转指令,可以看到这些跳转指令相当的多,对于初学者要学会这么多的指令可能会比较棘手,所以下面就演示如何使用这些条件跳转指令,以及EFLAGS里的标志位是如何影响条件跳转指令的。

Using the Zero flag 使用结果是否为零的标志:

    JE和JZ指令都是通过Zero标志来判断是否需要跳转的,当Zero标志被设置时(即两操作数相等时),JE和JZ就会发生跳转,Zero标志可以通过CMP指令或者数学运算指令来进行设置,例如下面的例子:

movl $30, %eax
subl $30, %eax
jz overthere

    上面的sub是将两操作数相减的减法指令(该指令将在后面的章节中进行介绍),可以看出减法计算的结果是零,就会设置Zero标志,jz指令根据该标志就会发生跳转。

    jz指令也常用在循环操作的计数器中,在循环中通过递减计数器的值,当计数器到达零时,就跳出循环,如下面的例子:

movl $10, %edi
loop1:
    < other code instructions>
    dec %edi
    jz out
    jmp loop1
out:

    这个代码片段中EDI寄存器就是循环计数器,它会从10开始递减,每递减一次就表示执行了一次循环体里的代码,当它减到零时,JZ指令就会退出循环。

Using the overflow flag 使用溢出标志:

    溢出标志主要配合有符号数来使用,当一个有符号数太大以致超出数据元素可以容纳的尺寸时,就会设置该标志位。这主要发生在一些数学运算中,如下面的例子:

    movl $1, %eax    ; move 1 to the EAX register
    movb $0x7f, %bl ; move the signed value 127 to the 8-bit BL register
    addb $10, %bl    ; Add 10 to the BL register
    jo overhere
    int $0x80            ; call the Linux system call
overhere:
    movl $0, %ebx   ; move 0 to the EBX register
    int $0x80            ; call the Linux system call

    代码片段中十六进制 0x7f 对应十进制值127,通过mov指令将bl寄存器设置为127,然后将10加入到bl中,这样bl里的值就是137,137对于无符号数来说是一个有效的字节值,但却不是一个有效的有符号字节数(有符号字节数的有效范围是-127到127),137超出了127,所以溢出标志就会被设置,jo指令就会执行跳转。

Using the parity flag 使用奇偶标志:

    奇偶标志用于初略的表示运算结果中有多少个二进制位是1 ,通常用于简单的校验运算结果是否正确。

    当运算结果中二进制位为1的个数是偶数时,该标志位就会被设置为1,当为奇数时,标志位就会被重置为0 。

    下面通过例子paritytest.s来测试奇偶标志位:

# paritytest.s - An example of testing the parity flag
.section .text
.globl _start
_start:
    movl $1, %eax
    movl $4, %ebx
    subl $3, %ebx
    jp overhere
    int $0x80
overhere:
    movl $100, %ebx
    int $0x80

    在这段代码中,sub指令减法运算的结果是1,对应的二进制格式为:00000001 ,可以看出来只有一个二进制位是1,也就是奇数个1,所以奇偶标志不会被设置为1,jp指令就不会发生跳转,程序直接退出,退出码为减法运算的结果即1 :

$ ./paritytest
$ echo $?

1
$

    现在我们再来测试奇偶标志被设置时的情况,将sub指令对应的操作数修改如下:

subl $1, %ebx

    这样EBX减去1后,EBX里存放的减法运算的结果就会是3,对应的二进制格式为:00000011 。可以看到有两个二进制位是1,也就是偶数个1,所以奇偶标志会被设置为1,jp指令就会跳转到overhere标签处,退出时的退出码就会是100 :

$ ./paritytest
$ echo $?

100
$

    命令行测试的结果和我们预期的一样。

Using the sign flag 使用符号标志:

    符号标志用于有符号数,表示寄存器里的值的符号位的变化情况。对于有符号数,二进制格式中的最高位被用作符号位,当符号位为1时表示负数,为0时表示正数。

    前面我们介绍过Zero结果是否为零的标志,通过该标志可以在循环中检测计数器是否为零,为零则跳出循环,但是对于数组来说,它的第一个元素的索引值为0,如果仅通过Zero标志,当计数器为0时就跳出循环的话,那么数组的第一个元素就无法被处理。

    通过符号标志,就可以让计数器为-1时再跳出循环,这样数组的所有元素都可以得到循环处理,如下面的signtest.s例子:

# signtest.s - An example of using the sign flag
.section .data
value:
.int 21, 15, 34, 11, 6, 50, 32, 80, 10, 2
output:
    .asciz "The value is: %d\n"
.section .text
.globl _start
_start:
    movl $9, %edi
loop:
    pushl value(, %edi, 4)
    pushl $output
    call printf
    add $8, %esp
    dec %edi
    jns loop
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    这个程序的作用是将value数组的所有元素循环打印出来,程序中使用EDI寄存器既作为循环计数器,同时又作为数组的索引值,从而可以遍历数组中的所有元素,当EDI寄存器的值变为-1时,由于最高符号位由0变为了1,所以jns就不会再跳转到循环体的开头位置,从而退出循环。

    由于signtest.s中使用了C的printf函数,所以链接时要指明C标准库,同时要指出动态库的加载器,这些在前面都详细讲解过,程序运行结果如下:

$ as -o signtest.o signtest.s
$ ld -o signtest signtest.o -lc -dynamic-linker /lib/ld-linux.so.2
$ ./signtest

The value is: 2
The value is: 10
The value is: 80
The value is: 32
The value is: 50
The value is: 6
The value is: 11
The value is: 34
The value is: 15
The value is: 21

$

Using the carry flag 使用进位或借位标志:

    carry标志用于无符号数运算时,检测结果是否发生溢出(需要和前面的overflow溢出标志相区分,overflow是用于有符号数运算的溢出检测)。当某个指令导致寄存器里的值超出了寄存器的数据尺寸限制时,carry标志就会被设置,这种溢出行为也可以用作低字节向高字节的进位,这是由你的代码来决定的。

    不同于overflow溢出标志,DEC和INC指令不会影响carry标志,例如下面的代码:

movl $0xffffffff, %ebx
inc %ebx
jc overflow

    上面的代码使用的inc指令,并不会设置carry标志,而下面的代码则会设置carry标志,从而发生跳转:

movl $0xffffffff, %ebx
addl $1, %ebx
jc overflow

    carry标志还可以用于借位操作,如下面的代码:

movl $2, %eax
subl $4, %eax
jc overflow

    上面代码中EAX寄存器里的值一开始为2,sub指令要用EAX里的2减去4,显然要通过借位才能完成运算,所以会设置carry标志,从而发生跳转。

    另外carry标志还可以被一些特殊的指令直接修改,如下表所示:

Instruction 
指令
Description 
描述
CLC Clear the carry flag (set it to zero) 
将carry标志清零
CMC Complement the carry flag 
(change it to the opposite of what is set) 
将carry标志设置为相反的值,如果原来是1,就变为0,原来为0,就变为1
STC Set the carry flag (set it to one) 
设置carry标志,将其设置为1

    上面这些指令都可以直接修改EFLAGS寄存器里的carry标志位。

Loops 汇编里的循环操作:

[zengl pagebreak]

Loops 汇编里的循环操作:

     前面通过跳转指令实现了一些循环的例子,下面看下和汇编循环有关的其他指令和相关例子。

The loop instructions (loop循环指令):

    前面介绍的signtest.s例子,是通过跳转指令和使用寄存器作为计数器来实现循环的,其实IA-32平台还提供了更加简单的方法来实现汇编循环,即loop指令集。

    这些loop指令是使用ECX寄存器作为计数器的,并且在loop指令执行时会自动递减ECX计数器的值,下表显示了可用的loop指令:

Instruction
指令
Description
描述
LOOP Loop until the ECX register is zero 
当ECX寄存器值为0时才退出循环
LOOPE/LOOPZ Loop until either the ECX register is zero, 
or the ZF flag is not set 
当ECX为0,或者ZF标志为0时退出循环,也就是只有当ECX不为0且ZF标志为1时才会执行循环
LOOPNE/LOOPNZ Loop until either the ECX register is zero, 
or the ZF flag is set 
当ECX为0,或者ZF标志为1时退出循环,也就是只有当ECX不为0且ZF标志为0时才会执行循环

    loope/loopz 和 loopne/loopnz 这些指令提供了检测ZF标志的功能。

    这些loop指令的格式如下:

loop address

    address操作数是程序中要跳转的目标位置对应的标签名,不幸的是,loop指令只支持8位的跳转偏移值,所以loop循环只能实现短跳转。

    在执行loop循环之前,你必须先设置好ECX循环计时器的值,下面是loop指令实现循环的通用模板:

    < code before the loop >
    movl $100, %ecx
loop1:
    < code to loop through >
    loop loop1
    < code after the loop >

    在实现< code to loop through > 即循环体里的代码时,需要注意的是,循环体的代码中如果对ECX寄存器进行了修改,而又没有相关的恢复措施的话,就会影响到loop指令的执行,因为loop指令需要ECX作为计数器。另外还需要特别注意的地方是,在循环体中如果调用函数,那么函数里很有可能在你不知情的情况下修改ECX的值,所以最好在这些函数里通过PUSH和POP指令对ECX的值进行压栈和恢复操作。

    使用loop指令在内部递减ECX值时,并不会影响EFLAGS寄存器里的标志位,所以当ECX寄存器减到0时,Zero是否为零标志并不会被设置。

A loop example 一个loop循环的例子:

    下面通过loop.s程序来演示LOOP指令的用法:

# loop.s - An example of the loop instruction
.section .data
output:
    .asciz "The value is: %d\n"
.section .text
.globl _start
_start:
    movl $100, %ecx
    movl $0, %eax
loop1:
    addl %ecx, %eax
    loop loop1
    pushl %eax
    pushl $output
    call printf
    add $8, %esp
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的代码中一开始将ECX设为100,第一次add指令后,EAX里的值就变为100,然后loop指令会在内部将ECX的值减1,所以第二次add指令执行时ECX里的值就变为99,EAX里的值就变为100+99的和,这样一直循环下去,直到ECX里的值减到1,并将1加到EAX后,loop将ECX减到0,最后退出循环,所以这段代码的作用相当于将100+99+98+97....+1的表达式给计算出来,并将结果打印显示出来。

    下面是loop.s的运行情况:

$ as -gstabs -o loop.o loop.s
$ ld -o loop loop.o -dynamic-linker /lib/ld-linux.so.2 -lc
$ ./loop

The value is: 5050
$

    上面输出的5050是正确的累加结果,因为代码中使用了printf函数,所以ld链接时添加了-lc参数来引入C标准库,同时通过-dynamic-linker参数来指定动态链接库的加载器。

Preventing LOOP catastrophes 防止ECX计数器可能导致的误循环:

    在上面的loop.s程序中,如果我们在一开始就将ECX计数器的值设置为0,看看会产生什么样的结果:

............ //省略N行代码

_start:
    movl $0, %ecx
    movl $0, %eax
loop1:
    addl %ecx, %eax
    loop loop1

............ //省略N行代码

    汇编链接修改后的loop.s程序,运行结果如下:

$ ./loop
The value is: -2147483648
$

    我们期望的值是0,可是结果却是-2147483648 ,之所以会产生这样的结果是因为:loop指令在执行时会先将ECX里的值减一,然后才会去检测ECX里的值,判断ECX是否到了0,所以如果ECX一开始就是0的话,那么loop指令对ECX先减一后,ECX就变为-1了,再对ECX进行检测时,由于ECX为-1不等于0,所以就会继续循环,这样ECX循环递减下去,直到ECX发生溢出才会退出循环,得到的结果自然就不正确了。

    要解决这个问题,可以在循环开始之前,通过前面提到过的jcxz或jecxz指令来判断ECX里的值是否为0,如果已经为0了,就直接跳过循环。

    下面的betterloop.s就使用jcxz指令来实现一个更好的循环:

# betterloop.s - An example of the loop and jcxz instructions
.section .data
output:
    .asciz "The value is: %d\n"
.section .text
.globl _start
_start:
    movl $0, %ecx
    movl $0, %eax
    jcxz done
loop1:
    addl %ecx, %eax
    loop loop1
done:
    pushl %eax
    pushl $output
    call printf
    add $8, %esp
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    运行结果如下:

$ ./betterloop
The value is: 0
$

    可以看到输出结果为0,是我们期望的值,你还可以将上面的ECX一开始设为100,再测试一次,可以得到5050的正确结果,这段代码就既可以计算正常情况下的累加值,又可以计算ECX为0时的值。

    限于篇幅,本篇就到这里,下一篇介绍高级语言的流程控制反汇编后的代码形式,以及其他一些和分支指令优化等相关的内容。

    OK,到这里,休息,休息一下 o(∩_∩)o~~

上下篇

下一篇: 汇编流程控制 (三) 流程控制结束篇

上一篇: 汇编流程控制 (一)

相关文章

优化汇编指令 (三)

汇编函数的定义和使用 (三) 汇编函数结束篇

什么是汇编语言(一) 汇编底层原理,指令字节码

优化汇编指令 (二)

使用IA-32平台提供的高级功能 (一)

Moving Data 汇编数据移动 (三) 数据交换指令