本章向程序员的汇编语言工具箱中引入一个重要的内容,使得编写出来的程序具备作决策的功能。几乎所有的程序都需要这种能力。首先,介绍布尔操作,由于能影响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 PROC
mov ecx, LENGTHOF array
mov esi, 0
L1:
test array[esi], 00100000b ;无符号数比较,影响零标志和进位标志
pushfd
add esi, TYPE array
popfd
loopz L1
sub esi, TYPE array
mov ax, WORD PTR array[esi]
mov value, ax
INVOKE 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 PROC
mov esi, OFFSET array
mov 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:
nop
INVOKE ExitProcess,0
main 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 PROC
mov esi, OFFSET array
mov 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, ax
INVOKE ExitProcess,0
main ENDP
END main
运行调试:

5.挑战:6.4.2节的LOOPNZ示例依靠一个标记值来处理没有发现正数的可能性。如果把这个标记值去掉,会发生什么?
答:如果没有发现匹配值,ESI将以指向数组末层之外作为结束。若指向了一个未定义的内存位置,那么程序运行就可能导致运行时错误。
6.5 条件结构
条件结构被定义为,能够在不同的逻辑分支中触发选择的一个或多个条件表达式。每一个分支都执行不同的指令序列。毫无疑问,在高级编程语言中已经使用了条件结构,但是你可能并不了解语言编译器是如何将条件结构转换为低级机器代码的。现在就来讨论这个转换过程.
6.5.1 块结构的 IF 语句
IF结构包含一个布尔表达式,其后有两个语句列表:一个是当表达式为真时执行,另一个是当表达式为假时执行:
if(boolean - expression)
statement - list - 1
else
statement - 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, op1
cmp eax, op2 ; op1 == op2 ?
je L1 ;是:跳转到L1
jmp L2 ;否:跳过赋值语句
L1: mov X, 1 ;X,Y赋值
mov Y, 2
jmp quit
L2: mov X, 10
mov Y, 20
quit:
nop
INVOKE ExitProcess,0
main ENDP
END main
方式1调试:

方式2调试:

从上面的例子可以看出,相同的条件结构在汇编语言中有多种实现方法。本章给出的编译代码示例只代表一种假想的编译器可能产生的结果
示例2 NTFS文件存储系统中,磁盘簇的大小取决于磁盘卷的总容量。如下面的伪代码所示,如果卷大小(用变量terrabytes存放)不超过16TB,则簇大小设置为4096。否则簇大小设置为8192
clustersize=8192
if terrabytes <16
clusterSize = 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 PROC
mov clusterSize, 8192 ;假设较大的磁盘簇
cmp terrabytes, 16 ;小于16TB ?
jae next ;大于或等于跳转
mov clusterSize, 4096 ;切换到较小的磁盘簇
next: nop
INVOKE ExitProcess,0
main ENDP
END main
运行调试:

示例3 下面的伪代码有两个分支:
if op1 > op2
call Routine1
else
call 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 PROC
mov eax, op1 ;op1送入寄存器
cmp eax, op2 ;op1 >op2?
jg A1 ;是:调用 Routine1
call Routine2 ;否:调用 Routine2
jmp A2 ;退出IE语句
A1: call Routine1
A2: nop
INVOKE ExitProcess,0
main ENDP
Routine1 PROC
mov edx, op1
ret
Routine1 ENDP
Routine2 PROC
mov edx, op2
ret
Routine2 ENDP
END main
运行调试:

白盒测试
复杂条件语句可能有多个执行路径,这使得它们难以进行调试检查(查看代码)。程序员经常使用的技术称为白盒测试,用来验证子程序的输入和相应的输出。白盒测试需要源代码,并对输人变量进行不同的赋值。对每个输入组合,要手动跟踪源代码,验证其执行路径和子程序产生的输出。下面,通过嵌套正语句的汇编程序来看看这个测试过程:
if op1 == op2
if X > Y
call Routine1
else
call Routine2
else
call Routine3
end if
下面是可能的汇编语言翻译,加上了参考行号。程序改变了初始条件(op1=-op2),并立即跳转到 ELSE部分。剩下要翻译的内容是内层IF-ELSE 语句:
mov eax, op1
cmp eax, op2 ;op1 == op2 ?
jne L2 ;否:调用Routine3, 不等于跳转到L2
mov eax, X ;处理内层IF-ELSE 语句
cmp eax, Y ;X > Y ?
jg L1 ;是:调用 Routine1, 大于跳转到L1
call Routine2 ;否:调用 Routine2
jmp L3 ;退出
L1: call Routine1 ;调用 Routine1
jmp 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 PROC
mov al, 5
mov bl, 4
mov cl, 3
;方式1
cmp al, bl ;第一个表达式…
ja L1 ;大于跳转到L1
jmp next ;否则退出if
L1: cmp bl, cl ;第二个表达式…
ja L2 ;大于跳转到L2
jmp 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:
nop
INVOKE ExitProcess,0
main 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 PROC
mov al, 2
mov bl, 4
mov cl, 3
cmp al, bl ;1:比较AL和 BL
ja L1 ;如果真,跳过第二个表达式
cmp bl, cl ;2:比较BL和CL
jbe next ;假:跳过下一条语句
L1: mov X, 1 ;真:将X置1
next:
nop
INVOKE ExitProcess,0
main ENDP
END main
运行调试:

对于一个给定的复合表达式而言,汇编语句有多种实现方法。
6.5.3 WHILE 循环
WHILE循环在执行语句块之前先进行条件测试。只要循环条件一直为真,那么语句块就不断重复。下面是用C++ 编写的循环:
while (val1 < val2)
{
val1++;
val2--;
}
用汇编语言实现这个结构时,可以很方便地改变循环条件,当条件为真时,跳转到endwhile。假设 vall和 val2 都是变量,那么在循环开始之前必须将其中的一个变量送入寄存器,并且还要在最后恢复该变量的值:
;6.5.3_1.asm 6.5.3 WHILE 循环 用汇编语言实现下面语句
;while (val1 < val2)
;{
; val1++;
; val2--;
;}
.386
.model flat, stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD
.data
val1 DWORD 12
val2 DWORD 15
.code
main PROC
mov eax, val1 ;把变量复制到EAX
beginwhile:
cmp eax, val2 ;如果非 val1<va12
jnl endwhile ;不小于,跳转到endwhile结束循环
inc eax ;val1++
dec val2 ;val2--
jmp beginwhile ;重复循环
endwhile:
mov val1, eax ;保存 val1的新值
INVOKE ExitProcess,0
main ENDP
END main
运行调试:

在循环内部,EAX是val1的代理(替代品),对val1的引用必须要通过EAX。INL的使用意味着val1和val2是有符号整数。
示例:循环内的 IF 语句嵌套
高级语言尤其善于表示嵌套的控制结构。如下C++代码所示,在一个WHILE循环中有嵌套IF语句。它计算所有大于sample 值的数组元素之和:
int array[] = { 10, 60, 20, 33, 72, 89, 45, 65, 72, 18 };
int sample = 50;
int ArraySize = sizeof array / sizeof sample;
int index = 0;
int sum = 0;
while (index < ArraySize)
{
if (array[index] > sample)
{
sum += array[index];
}
index++;
}
在用汇编语言编写该循环之前,用图6-1的流程图来说明其逻辑。为了简化转换过程,并通过减少内存访问次数来加速执行,图中用寄存器来代替变量:EDX=sample,EAX=sum,ESI=index,ECX=ArraySize(常数)。标号名称也已经添加到逻辑框上。
汇编代码 从流程图生成汇编代码最简单的方法就是为每个流程框编写单独的代码。注意流程图标签和下面源代码使用标签之间的直接关系(参阅Flowchart.asm):
; Flowchart.asm
INCLUDE Irvine32.inc
.data
sum DWORD 0
sample DWORD 50
array DWORD 10, 60, 20, 33, 72, 89, 45, 65, 72, 18
ArraySize = ($ - Array) / TYPE array
.code
main PROC
mov eax, 0 ;求和
mov edx, sample
mov esi, 0 ;索引
mov ecx, ArraySize
L1: cmp esi, ecx ;如果esi < ecx
jl L2
jmp L5
L2: cmp array[esi*4], edx ;如果array[esi] > edx
jg L3
jmp L4
L3: add eax, array[esi*4]
L4: inc esi
jmp L1
L5: mov sum, eax
INVOKE ExitProcess, 0
main ENDP
END main
运行调试:

6.5 节最后的一个回顾问题将提供机会来改进上述代码。

6.5.4 表驱动选择
表驱动选择是用查表来代替多路选择结构的一种方法。使用这种方法,需要新建一个表,表中包含查询值和标号或过程的偏移量,然后必须用循环来检索这个表。当有大量比较操作时,这个方法最有效。
例如,下面是一个表的一部分,该表包含单字符查询值,以及过程的地址:
.data
CaseTable BYTE 'A ;查询值
DWORD Process_A ;过程地址
BYTE 'B'
DWORD Process_B
(etc.)
假设ProcessA、ProcessB、ProcessC和Process D的地址分别是120h、130h、140h和150h。上表在内存中的存放如图6-2所示。

示例程序下面的示例程序(ProcTable.asm)中,用户从键盘输人一个字符。通过循环,该字符与表的每个表项进行比较。第一个匹配的查询值将会产生一个调用,调用对象是紧接在该查询值后面的过程偏移量。每个过程加载到EDX的偏移量都代表了一个不同的字符串,它将在循环中显示:
;ProcTable.asm 6.5.4 表驱动选择 示例程序
;过程偏移量表
;本程序包含了过程偏移量表格。
;它用这个表格执行间接过程调用。
INCLUDE Irvine32.inc
.data
CaseTable BYTE 'A' ;查询值
DWORD Process_A ;过程地址
EntrySize = ($ - CaseTable)
BYTE 'B'
DWORD Process_B
BYTE 'C'
DWORD Process_C
BYTE 'D'
DWORD Process_D
NumberOfEntries = ($ - CaseTable) / EntrySize
prompt BYTE "Press capital A,B,C, or D: ", 0
;为每个过程定义一个单独的消息字串
msgA BYTE "Process_A",0
msgB BYTE "Process_B",0
msgC BYTE "Process_C",0
msgD BYTE "Process_D",0
.code
main PROC
mov edx, OFFSET prompt ;请求用户输入
call WriteString
call ReadChar ;读取字符到 AL
mov ebx, OFFSET CaseTable ;设EBX为表指针
mov ecx,NumberOfEntries ;循环计数器
L1: cmp al, [ebx] ;发现匹配项?
jne L2 ;否:继续
call NEAR PTR [ebx+1] ;是:调用过程
;这个CALL指令调用过程,其地址保存在EBX+1指向的内存位置中。像这样的间接调用需要使用 NEAR PTR 运算符。
call WriteString ;显示消息
call Crlf
jmp L3 ;退出搜索
L2:
add ebx, EntrySize ;指向下一个表项
loop L1 ;重复直到ECX=0
L3:
exit
main ENDP
;下面的每个过程向EDX加载不同字符串的偏移量:
Process_A PROC
mov edx, OFFSET msgA
ret
Process_A ENDP
Process_B PROC
mov edx, OFFSET msgB
ret
Process_B ENDP
Process_C PROC
mov edx, OFFSET msgC
ret
Process_C ENDP
Process_D PROC
mov edx, OFFSET msgD
ret
Process_D ENDP
END main
运行结果:

表驱动选择有一些初始化开销,但是它能减少编写的代码总量。一个表就可以处理大量的比较,并且与一长串的比较、跳转和CALL指令序列相比,它更加容易修改。甚至在运行时,表还可以重新配置。
6.5.5 本节回顾
注意:所有复合表达式都使用短路求值。假设val1和X是32位变量
1.用汇编语言实现下述伪代码:
if ebx > ecx
X = 1
;6.5.5_4.asm
INCLUDE Irvine32.inc
.data
X DWORD 0
.code
main PROC
cmp ebx, ecx
jbe next ;小于等于退出
mov X, 1 ;大于执行X=1
next:
INVOKE ExitProcess,0
main ENDP
END main
运行调试:

2.用汇编语言实现下述伪代码:
if edx <= ecx
X=1
else
X=2
;6.5.5_2.asm
INCLUDE Irvine32.inc
.data
X DWORD 0
.code
main PROC
cmp edx, ecx
ja L1 ;大于跳转到L1
mov X, 1 ;小于或等于执行X=1
jmp next ;退出if
L1: mov X, 2
next:
INVOKE ExitProcess,0
main ENDP
END main
运行调试:

3.对6.5.4节中的程序来说,为什么让汇编器计算NumberOfEntries比对其赋值(如NumberOfEntries=4)效果更好?
答:方便维护,后面表格的变化会修改NumberOfEntries的值。程序员可能会忘记手动修改常数,但汇编器会正确调整计算结果。
4.挑战:重新编写6.5.3节中的代码,使其功能不变,但指令数更少。
;6.5.5_4.asm 6.5.5 本节回顾
;4.挑战:重新编写6.5.3节中的代码,使其功能不变,但指令数更少。
INCLUDE Irvine32.inc
.data
array DWORD 10, 60, 20, 33, 72, 89, 45, 65, 72, 18
sample DWORD 50
sum DWORD 0
.code
main PROC
mov eax, sum
mov ecx, LENGTHOF array ;元素个数
mov edx, sample
mov esi, 0
L1:
mov ebx, array[esi*4]
cmp array[esi*4], edx
jbe next ;小于或等于跳转到下一个元素
add eax, ebx ;大于50, sum += array[esi]
next:inc esi ;下一个元素
loop L1
mov sum, eax ;保存结果
call WriteInt
call Crlf
INVOKE ExitProcess,0
main ENDP
END main
运行调试:

7356

被折叠的 条评论
为什么被折叠?



