本文由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 (条件跳转指令):
有好几种条件跳转指令,这些指令的通用格式如下:
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指令会在内部将第二个操作数减去第一个操作数,即(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指令对应的操作数修改如下:
这样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指令的格式如下:
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~~