《汇编语言:基于X86处理器》第6章 条件处理(2)
本章向程序员的汇编语言工具箱中引入一个重要的内容,使得编写出来的程序具备作决策的功能。几乎所有的程序都需要这种能力。首先,介绍布尔操作,由于能影响CPU状态标志,它们是所有条件指令的核心。然后,说明怎样使用演绎CPU状态标志的条件跳转和循环指令。接着演示如何用本章介绍的工具来实现理论计算机科学中最根本的结构之一:有限状态机。本章最后展示的是MASM内置的32位编程的逻辑结构。
6.4 条件循环指令
6.4.1 LOOPZ和 LOOPE 指令
LOOPZ(为零跳转)指令的工作和LOOP指令相同,只是有一个附加条件:为零控制转向目的标号,零标志位必须置1。指令语法如下:
LO0PZ destination
LOOPE(相等跳转)指令相当于LOOPZ,它们有相同的操作码。这两条指令执行如下任务:
ECX=ECX-1
if ECX > 0 and F =1, jump to destination
否则,不发生跳转,并将控制传递到下一条指令。LOOPZ和LOOPE不影响任何状态标志位。32位模式下,ECX是循环计数器;64位模式下,RCX是循环计数器。
完整代码测试笔记
;6.4.1.asm LOOPNZ指令 和LOOPNE指令用法学习
;找出数组中第1个小写字母.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
array WORD 'A', 'C', 'S', 'P', 'a', 'y', 'h', 'x'
value WORD 0.code
main PROCmov ecx, LENGTHOF arraymov esi, 0
L1:test array[esi], 00100000b ;无符号数比较,影响零标志和进位标志pushfdadd esi, TYPE arraypopfdloopz L1sub esi, TYPE arraymov ax, WORD PTR array[esi]mov value, axINVOKE ExitProcess,0
main ENDP
END main
运行调试
6.4.2 LOOPNZ和 LOOPNE 指令
LOOPNZ(非零跳转)指令与LOOPZ相对应。当ECX中无符号数值大于零(减1操作之后)且零标志位等于零时,继续循环。指令语法如下:
LOOPNZ destination
LOOPNE(不等跳转)指令相当于LOOPNZ,它们有相同的操作码。这两条指令执行如下任务:
ECX=ECX-1
if ECX > 0 and F =0, jump to destination
否则,不发生跳转,并将控制传递到下一条指令。
完整代码测试笔记:
;6.4.2.asm LOOPNZ指令 和LOOPNE指令学习
;与Loopnz.asm功能一样.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
array SWORD -1, -6, -1, -10, 10, 30, 40, 4
sentinel SWORD 0.code
main PROCmov esi, OFFSET arraymov ecx, LENGTHOF array
L1: test WORD PTR [esi], 8000h ;测试符号位,该指令影响符号标志,零标志,奇偶标志jns quit ;无符号跳转(表示非负数)add esi,TYPE array ;移动到下一个位置loopnz L1 ;继续循环jnz notFind ;没有发现非负数
quit:movzx eax, WORD PTR[esi]mov sentinel, ax
notFind:nopINVOKE ExitProcess,0main ENDP
END main
运行调试:
如果找到一个非负数,ESI会指向该数值。如果没有找到一个正数,则只有当ECX=0时才终止循环。在这种情况下,JNZ指令跳转到标号quit,同时ESI指向标记值(0),其在内存中的位置正好紧接着该数组。
6.4.3 本节回顾
1.(真/假):当(且仅当)零标志位被清除时,LOOPE指令跳转到标号
答:假。当ZF=1并且ECX > 0时才跳转
2.(真/假):32位模式下,当ECX大于零且零标志位被清除时,LOOPNZ指令跳转到标号。
答:真。
3.(真/假):LOOPZ指令的目的标号必须处在距离其后指令的-128到+127字节范围之内。
答:真
4.修改6.4.2节中的LOOPNZ示例,使之扫描数组并搜索其中的第一个负数。改变数组的初始化,用正数作为其起始值。
答:修改后的代码如下
;6.4.3_4.asm 6.4.3 本节回顾 4.修改6.4.2节中的LOOPNZ示例,
;使之扫描数组并搜索其中的第一个负数。改变数组的初始化,用正数作为其起始值。.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
array SWORD 10, 30, 40, 4, -56, -17, -98
sentinel SWORD 0.code
main PROCmov esi, OFFSET arraymov ecx, LENGTHOF array
L1: test WORD PTR [esi], 8000h ;测试符号位,该指令影响符号标志,零标志,奇偶标志pushfd ;标志位入栈add esi,TYPE array ;移动到下一个位置popfd ;标志位出栈loopz L1 ;继续循环jz quit ;没有发现负数sub esi, TYPE array ;ESI指向数值
quit:movzx eax, WORD PTR[esi]mov sentinel, axINVOKE ExitProcess,0main ENDP
END main
运行调试:
5.挑战:6.4.2节的LOOPNZ示例依靠一个标记值来处理没有发现正数的可能性。如果把这个标记值去掉,会发生什么?
答:如果没有发现匹配值,ESI将以指向数组末层之外作为结束。若指向了一个未定义的内存位置,那么程序运行就可能导致运行时错误。
6.5 条件结构
条件结构被定义为,能够在不同的逻辑分支中触发选择的一个或多个条件表达式。每一个分支都执行不同的指令序列。毫无疑问,在高级编程语言中已经使用了条件结构,但是你可能并不了解语言编译器是如何将条件结构转换为低级机器代码的。现在就来讨论这个转换过程.
6.5.1 块结构的 IF 语句
IF结构包含一个布尔表达式,其后有两个语句列表:一个是当表达式为真时执行,另一个是当表达式为假时执行:
if(boolean - expression)statement - list - 1
elsestatement - list - 2
结构中的else部分是可选的。在汇编语言中,则是用多个步骤来实现这种结构的。首先,对布尔表达式求值,这样一来某个CPU状态标志位会受到影响。然后,根据相关CPU状态标志位的值,构建一系列跳转把控制传递给两个语句列表。
示例1下面的C++代码中,如果op1等于op2,则执行两条赋值语句:
if(op1 == op2)
{X = 1;Y = 2;
}
在汇编语言中,这种正语句转换为条件跳转和CMP指令。由于op1和op2都是内存操作数(变量),因此,在执行CMP之前,要将其中的一个操作数送人寄存器。下面实现IF语句的程序是高效的,当逻辑表达式为真时,它允许代码“通过”直达两条期望被执行的MOV 指令:
;6.5.1_1.asm 块结构的 IF 语句 示例1 用汇编语言实现下面c++语句
;if(op1 == op2) {
; X = 1;
; Y = 2;
;}.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
op1 DWORD 45
op2 DWORD 45
X DWORD ?
Y DWORD ?.code
main PROC;1.jne的方式。它允许代码“通过”直达两条期望被执行的MOV 指令:
; mov eax, op1
; cmp eax, op2 ;op1 == op2 ?
; jne L1 ;否:跳过后续指令
; mov X, 1 ;是:X,Y赋值
; mov Y, 2
; jmp quit
;L1: mov X, 10
; mov Y, 20;2.je的方式。用正来实现==运算符,生成的代码就没有那么紧凑了(6条指令,而非5条指令):mov eax, op1cmp eax, op2 ; op1 == op2 ?je L1 ;是:跳转到L1jmp L2 ;否:跳过赋值语句
L1: mov X, 1 ;X,Y赋值mov Y, 2 jmp quit
L2: mov X, 10 mov Y, 20
quit:nopINVOKE ExitProcess,0main ENDP
END main
方式1调试:
方式2调试:
从上面的例子可以看出,相同的条件结构在汇编语言中有多种实现方法。本章给出的编译代码示例只代表一种假想的编译器可能产生的结果
示例2 NTFS文件存储系统中,磁盘簇的大小取决于磁盘卷的总容量。如下面的伪代码所示,如果卷大小(用变量terrabytes存放)不超过16TB,则簇大小设置为4096。否则簇大小设置为8192
clustersize=8192
if terrabytes <16clusterSize = 4096:
用汇编语言实现该伪代码:
;6.5.1_2.asm 块结构的 IF 语句 示例2 用汇编语言实现下面语句
;clustersize = 8192
;if terrabytes < 16
; clusterSize = 4096:.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
clusterSize WORD ? ;簇大小
terrabytes WORD 1 ;磁盘卷大小,单位TB.code
main PROCmov clusterSize, 8192 ;假设较大的磁盘簇cmp terrabytes, 16 ;小于16TB ?jae next ;大于或等于跳转 mov clusterSize, 4096 ;切换到较小的磁盘簇
next: nopINVOKE ExitProcess,0main ENDP
END main
运行调试:
示例3 下面的伪代码有两个分支:
if op1 > op2call Routine1
elsecall Routine2
end if
用汇编语言翻译这段伪代码,设op1和op2是有符号双字变量。对这两个变量比较时其中一个必须送入寄存器:
;6.5.1_3.asm 块结构的 IF 语句 示例3 用汇编语言实现下面语句
;if op1 > op2
; call Routine1
;else
; call Routine2
;end if.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
op1 DWORD 55
op2 DWORD 51.code
main PROCmov eax, op1 ;op1送入寄存器cmp eax, op2 ;op1 >op2?jg A1 ;是:调用 Routine1call Routine2 ;否:调用 Routine2jmp A2 ;退出IE语句
A1: call Routine1
A2: nopINVOKE ExitProcess,0
main ENDP Routine1 PROCmov edx, op1ret
Routine1 ENDPRoutine2 PROCmov edx, op2ret
Routine2 ENDP
END main
运行调试:
白盒测试
复杂条件语句可能有多个执行路径,这使得它们难以进行调试检查(查看代码)。程序员经常使用的技术称为白盒测试,用来验证子程序的输入和相应的输出。白盒测试需要源代码,并对输人变量进行不同的赋值。对每个输入组合,要手动跟踪源代码,验证其执行路径和子程序产生的输出。下面,通过嵌套正语句的汇编程序来看看这个测试过程:
if op1 == op2if X > Ycall Routine1elsecall Routine2
elsecall Routine3
end if
下面是可能的汇编语言翻译,加上了参考行号。程序改变了初始条件(op1=-op2),并立即跳转到 ELSE部分。剩下要翻译的内容是内层IF-ELSE 语句:
mov eax, op1 cmp eax, op2 ;op1 == op2 ?jne L2 ;否:调用Routine3, 不等于跳转到L2mov eax, X ;处理内层IF-ELSE 语句cmp eax, Y ;X > Y ?jg L1 ;是:调用 Routine1, 大于跳转到L1 call Routine2 ;否:调用 Routine2jmp L3 ;退出
L1: call Routine1 ;调用 Routine1jmp L3 ;退出
L2: call Routine3
L3:
表6-6给出了示例代码的白盒测试结果。前四列对op1、op2、X和Y进行测试赋值。
第5列和第6列对生成的执行路径进行了验证
6.5.2 复合表达式
汇编语言很容易实现包含AND运算符的复合布尔表达式。考虑下面的伪代码,假设其中进行比较的是无符号整数:
if (al > bl) AND (bl > cl)X = 1
else if
短路求值 下面的例子是短路求值的简单实现,如果第一个表达式为假,则不需计算第二个表达式。高级语言的规范如下:
;6.5.2_1.asm 复合表达式 1.逻辑 AND 运算符
;if (al > bl) AND (bl > cl)
; X = 1;
;end if.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
X BYTE 23.code
main PROCmov al, 5mov bl, 4mov cl, 3;方式1cmp al, bl ;第一个表达式…ja L1 ;大于跳转到L1jmp next ;否则退出if
L1: cmp bl, cl ;第二个表达式…ja L2 ;大于跳转到L2jmp next ;否则退出if
L2: mov X, 1 ;全为真:将X置1
next:jmp quit;方式2, 如果把第一条 JA指令替换为JBE,就可以把代码减少到5条:cmp al, bl ;第一个表达式…jbe quit ;如果假,则退出 小于或等于跳转到quit,退出if, jbe无符号比较cmp bl, cl 第二个表达式…jbe quit ;如果假,则退出 小于或等于跳转到quit,退出if, jle有符号比较mov X, 1 ;全为真
quit: nopINVOKE ExitProcess,0main ENDP
END main
运行调试
方式1:
方式2:
若第一个JBE不执行,CPU可以直接执行第二个CMP指令,这样就能够减少29%的代码量(指令数从7条减少到5条)。
2.逻辑 OR 运算符
当复合表达式包含的子表达式是用OR运算符连接的,那么只要一个子表达式为真,则整个复合表达式就为真。以如下伪代码为例:
if (al > bl) OR (bl > cl)X = 1
在下面的实现过程中,如果第一个表达式为真,则代码分支到L1;否则代码直接执行第二个CMP指令。第二个表达式翻转了>运算符,并使用了JBE指令:
【代码】
对于一个给定的复合表达式而言,汇编语句有多种实现方法。
2.逻辑 OR 运算符
当复合表达式包含的子表达式是用OR运算符连接的,那么只要一个子表达式为真,则整个复合表达式就为真。以如下伪代码为例:
if (al > bl) OR (bl > cl)X = 1
在下面的实现过程中,如果第一个表达式为真,则代码分支到L1;否则代码直接执行第二个CMP指令。第二个表达式翻转了>运算符,并使用了JBE指令:
;6.5.2_2.asm 复合表达式 2.逻辑 OR 运算符
;if (al > bl) OR (bl > cl)
; X = 1;.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD.data
X BYTE 23.code
main PROCmov al, 2mov bl, 4mov cl, 3cmp al, bl ;1:比较AL和 BLja L1 ;如果真,跳过第二个表达式cmp bl, cl ;2:比较BL和CLjbe next ;假:跳过下一条语句
L1: mov X, 1 ;真:将X置1
next: nopINVOKE ExitProcess,0main ENDP
END main
运行调试:
对于一个给定的复合表达式而言,汇编语句有多种实现方法。