稍微复杂一些的程序通常需要做某种条件判断,然后再决定程序的执行流程。当然也可以无条件跳转到程序的另一处地址开始执行。本节我们将详细介绍分支结构的程序设计方法。
针对功能较为复杂的程序,程序开发有一套标准的流程,我们将10.1节中的五个步骤进一步细化:
第一步:分析需求,设计程序结构框架;
第二步:数据定义,定义恰当的数据结构;
第三步:分析算法;
第四步:编写伪代码,即用我们自己的语言来编写程序;
第五步:画流程图,使用Visio、Excel或者其他绘图工具绘制算法流程和逻辑关系图;
第六步:编写源程序,其实就是将我们的伪代码翻译成计算机语言;
第七步:调试程序,修复程序中可能出现的BUG;
第八步:优化代码,尝试更好的设计方案,效率更高的算法,逻辑更为清晰简洁明了。这一步可以使我们学到更多的东西,何乐而不为呢。
在今后的学习中,建议读者严格遵循这样的流程,养成良好的编程习惯,受益终身。
本节内容:分支程序设计。
■简单分支结构:汇编语言中一般利用条件测试指令和条件转移指令等实现简单的分支,相当于高级语言中的if和if-else语句。
■多分支结构:在分支结构中,还有一种情形,即需要同时做多种不同的条件判断,形成多个分支语句。在高级语言中称为switch语句。示例代码:t10-8.asm ~t10-12.asm。
10.2.1 简单分支结构
简单分支结构常见的形式有两种,如流程图10-3、10-4所示。
图10-3 条件结构1 图10-4 条件结构2
汇编语言中一般利用条件测试指令和条件转移指令等实现简单的分支,相当于高级语言中的if和if-else语句。接下来我们分析示例代码。
●例1:排序
动手实验66:实现3个无符号数由小到大排序的程序。
在理解下面示例程序的基础上,自己独立编写源程序。编译完成后,在debug调试器中单步跟踪调试,以验证程序的正确性。
假设数据段buffer缓冲区中存储三个十进制数87、234和123。在内存中以二进制数的形式存储,使用简化后的十六进制数表示为57H、EAH和7BH。
注:在源程序中定义数据时,我们可以直接使用十进制数、二进制数或者十六进制数,编译器在编译时会将其转换为二进制数。
方法一:使用3个寄存器进行比较。如图10-5所示:
图10-5 3个数排序方法一
第一步:分析需求:3个无符号数升序排序。
第二步:数据定义: buffer缓冲区存放三个自定义无符号数,buffer db 87,234,123。
第三步:分析算法:两两比较。
第四步:伪代码:
assume cs:code,ds:data
数据段
;数据定义
代码段
;给数据段赋值ds=data
;si变址指针寄存器指向buffer缓冲区
;分别将三个无符号数依次存入al、bl、cl寄存器
;第一次比较al、bl
如果al>bl,需要交换,xchg al,bl
如果al<=bl,不需要交换
进行第二次比较
;第二次比较al、cl
如果al>cl,需要交换,xchg al,cl
如果al<=cl,不需要交换
进行第三次比较
;第三次比较bl、cl
如果bl>cl,需要交换,xchg bl,cl
如果bl<=cl,不需要交换
;分别将al、bl、cl存入buffer缓冲区
buffer=al
buffer+1=bl
buffer+2=cl
;结束
第五步:画流程图,如图10-6所示。
图10-6 t10-8流程图
第六步:写代码:
示例代码32:
;程序名:t10-8.asm
;功能:实现3个无符号数由小到大排序
;算法:3个无符号数两两比较,比较3次
;程序结构:分支结构
;方法一:使用3个寄存器进行比较
;------------------------------------
assume cs:code,ds:data
data segment
buffer db 87,234,123
data ends
code segment
start:
mov ax,data
mov ds,ax
;取出三个数,分别存入al、bl、cl寄存器
mov si,offset buffer
mov al,[si]
mov bl,[si+1]
mov cl,[si+2]
;第一次比较
cmp al,bl
jbe next1
xchg al,bl
next1:
;第二次比较
cmp al,cl
jbe next2
xchg al,cl
next2:
;第三次比较
cmp bl,cl
jbe next3
xchg bl,cl
next3:
mov buffer,al
mov buffer+1,bl
mov buffer+2,cl
over: ;
mov ax,4c00h
int 21h
code ends
end start
第七步:调试,如图10-7所示。
图10-7 调试t10-8.exe
第八步:代码优化:
方法二:使用1个寄存器进行比较。如图10-8所示。
图10-8 3个数排序方法二
伪代码:
数据定义:buffer db 87,234,123
mov si,offset buffer
[SI]=57H
[si+1]=0eah
[si+2]=7bh
第1和2比较
al=[si]
al > [si+1],需要交换
al <= [si+1],不需要交换,xchg al,[si+1]
[si]=al
比较1和3
al,[si+2]
al > [si+2],需要交换
al <= [si+2],不需要交换,xchg al,[si+2]
[si]=al
比较2和3
al=[si+1]
al,[si+2]
al > [si+2],需要交换
al <= [si+2],不需要交换,xchg al,[si+2]
[si+1]=al
结束
示例代码33:
;程序名:t10-9.asm
;功能:实现3个无符号数由小到大排序
;算法:3个无符号数两两比较,比较3次
;程序结构:分支结构
;方法二:使用1个寄存器进行比较
;------------------------------------
assume cs:code,ds:data
data segment
buffer db 87,234,123
data ends
code segment
start:
mov ax,data
mov ds,ax
;取出第一个数
mov si,offset buffer
mov al,[si]
;第一次比较
cmp al,[si+1]
jbe next1
xchg al,[si+1]
mov [si],al
next1:
;第二次比较
cmp al,[si+2]
jbe next2
xchg al,[si+2]
mov [si],al
next2:
;第三次比较
mov al,[si+1]
cmp al,[si+2]
jbe over
xchg al,[si+2]
mov [si+1],al
over: ;
mov ax,4c00h
int 21h
code ends
end start
三个数排序的算法非常简单,两两比较。如果按照升序排序,则第一个数大于第二个数时交换位置。在使用相同算法的情况下,示例代码32采用方法一,需要使用三个寄存器al、bl、cl来实现。示例代码33只需要使用一个寄存器al实现。由于8086 CPU的寄存器数量有限,所以建议使用方法二较为合适。
提示
为了节约篇幅,从示例代码t10-8后的示例及课后练习程序,不再详细描述完整的程序设计流程。请读者务必遵循七个完整的程序设计步骤,独立完成。这是每一位程序员的必经之路。
●例2:一位十六进制数转换为对应的ASCII码
算法分析:
如图10-7所示,十六进制数0~9加上30H就是其对应的ASCII码值30H~39H。十六进制数A~F加上37H就是其对应的ASCII码值41H~46H。两种不同转换方法的判断条件是:
如果该一位十六进制数小于等于9,则加上30H;
如果该一位十六进制数大于9,则加上37H。
图10-9 一位十六进制数转换为ASCII
动手实验67:写一个实现把一位十六进制数转换为对应的ASCII码的程序。
在理解下面示例程序的基础上,自己独立编写源程序。编译完成后,在debug调试器中单步跟踪调试,以验证程序的正确性。
伪代码:
1.注释,程序名,功能
2.数据定义
3.al=x
4.比较判断,0-9,A-F之间
5.如果是0-9:al+30h
6.如果不是0-9,则是A-F: al+37h
7.ASCII =al
8.结束
示例代码34:
;例2:写一个实现把一位十六进制数转换为对应的ASCII码的程序
;程序名:t10-10.asm
;功能:十六进制数转换为ASCII码
;算法:0~9 +30H;A~F +37H
;设计结构:分支结构
;-----------------------------------------------------------
assume cs:code,ds:data
data segment
x db 0fh
ASCII db ?
data ends
code segment
start:
mov ax,data
mov ds,ax
;
mov al,x
add al,30h ;0~9
cmp al,39h
jbe over
add al,7h ;A~F
over:
mov ASCII,al
mov ax,4c00h
int 21h
code ends
end start
这个示例也可以换一种不同的方式实现,比如将“jbe”改成“ja”,或者是先判断x的值是否大于9,然后再加30H或39H。这两种不同的逻辑留给读者独立完成。
10.2.2 多分支结构
在分支结构中,还有一种情形,即需要同时做多种不同的条件判断,形成多个分支语句。在高级语言中称为switch语句。如图10-10所示:
图10-10 多向分支结构示意图
●例3:一位16进制数所对应的ASCII码转换为16进制数
算法分析:
假设AL=ASCII码,X为转换后的16进制数,分为7种情况:
1.AL<'0',X=-1;
2.'0'<=AL<='9',X=AL-'0';
3.'9'<AL<'A',X=-1;
4.'A'<=AL<='F',X=AL-'A'+10 ;
5.'F'<AL<'a',x=-1;
6.'a'<=AL<='f' x=AL-'a'+10;
7.AL>'f',x=-1;
动手实验68:写一个实现把一位16进制数所对应的ASCII码转换为16进制数的程序,如果没有对应数,则转换为-1。
在理解下面示例程序的基础上,自己独立编写源程序。编译完成后,在debug调试器中单步跟踪调试,以验证程序的正确性。
示例代码35:
;例3:写一个实现把一位16进制数所对应的ASCII码转换为16进制数的程序,如果没有对应数,则转换为-1
;程序名:t10-11.asm
;分支结构
;功能:ASCII码转换为16进制数
;-------------------------------------------------------------
assume cs:code,ds:data
data segment
x db ?
ASCII db 'a'
data ends
code segment
start:
mov ax,data
mov ds,ax
;
mov al,ASCII
cmp al,'0'
jb NO
cmp al,'9'
jbe next1
cmp al,'A'
jb NO
cmp al,'F'
jbe next2
cmp al,'a'
jb NO
cmp al,'f'
ja NO
sub al,57h
jmp LAB
next1:
sub al,30h
jmp LAB
next2:
sub al,37h
jmp LAB
LAB:
mov x,al
jmp over
NO:
mov x,-1
over: ;
mov ax,4c00h
int 21h
code ends
end start
代码优化:
由于ASCII值可能是大写字符,也可能是小写字符,所以在示例代码35中,同时考虑了这两种情形,并且分别做了小写字符和大写字符的比较和判断。
可以考虑一种简化判断的方法,在判断’A’~’F’之前,先执行语句“AND ASCII,1101 1111B”,将x值第5位置0,不论大小写字符均统一转换为大写字符,然后只需要判断大写字符即可。
请读者独立完成上述代码优化。
●例4:利用地址表实现多向分支
任何复杂的多向分支总可以分解成多个简单分支。汇编语言实现多向分支的源程序结构如下:
......
cmp ah,1 ;假设x在ah寄存器中
jz yes_1
jmp not_1
yes_1:
处理语句1
......
jmp ok
not_1:
cmp ah,2
jz yes_2
jmp not_2
yes_2:
处理语句2
......
jmp ok
not2:
cmp ah,3
jnz not_3
yes_3:
处理语句3
......
jmp not_3
not_3:
cmp ah,4
jnz not_4
yes_4:
处理语句4
......
not_4:
处理语句5
......
ok: ......
上述代码片段中根据x的值是否为1~4,使用了4个条件判断+5个处理语句的结构,流程图如图10-11所示。
图10-11 多分支语句流程图
谨慎
多分支语句中的JCC指令跳转的地址需要注意是否会超出跳转范围(JCC指令跳转范围是-128~127)。负数向前跳转,正数向后跳转。
如果确实超出JCC指令跳转范围,解决方案有两种:
1.修改该条JCC指令为相反的JCC指令。例如将ja指令改为jbe指令,交换语句块位置。
2.使用JMP指令接续。例:
ja next1
next1:
Jmp next2
next2:
如果多分支语句过于繁琐,可以考虑采用建立地址表的方案简化代码。请看下面的实验。
动手实验69:假设有一个命令选择程序,每次只接收一个单键命令'A'至'H',然后根据命令进行相应的处理。
算法分析:
1.需要根据字符'A'至'H'编写一个入口地址表又称为散转表;
2.需要接收键盘输入单个字符,使用DOS系统的1号功能调用,接收键盘输入单个字符;
3.需要考虑接收字符的大小写情况;
4.根据输入字符,判断对应表项;
5.跳转到正确的命令地址;
在理解下面示例程序的基础上,自己独立编写源程序。编译完成后,在debug调试器中单步跟踪调试,以验证程序的正确性。
示例代码36:
;设程序每次只接收一个单键命令'A'至'H',然后根据命令进行相应的处理,
;如果接受到的不是规定的命令字母,则不处理。
;分析:编写一个入口地址表又称为散转表。如果各处理程序均在同一代码段,
;则入口地址只需要用偏移表示,入口地址表宽度为‘字’。
;程序名:t10-12.asm
;算法:查表
;===================================================================
assume cs:code,ds:data
data segment
;入口地址表
comtab dw COMA,COMB,COMC,COMD
dw COME,COMF,COMG,COMH
data ends
code segment
start:
mov ah,1
int 21h ;接收键盘命令在AL中
and al,11011111b ;小写转大写
cmp al,'A'
jb ok
cmp al,'H'
ja ok
sub al,'A' ;把命令字母转换成序号(从0开始)
xor ah,ah ;清零
add ax,ax ;ax*2,转换成comtab地址,宽度为dw
mov bx,ax ;bx基址寄存器
jmp comtab[bx] ;跳转对应的处理分支程序
ok: mov ax,4c00h
int 21h
;----------------------------------------------------
COMA: ;....
JMP OK
COMB: ...
JMP OK
......
COMH: ......
JMP OK
code ends
end start
在上述示例代码中,通过入口地址表实现多分支结构,入口地址由用户从键盘输入,实现用户与程序之间的互动。
注意
1.用户输入的ASCII字符'A'至'H'为db类型,入口地址表项的数据类型为dw,转换成comtab地址时需要乘以2。
2.建立入口地址表时,将表项与字符'A'至'H'的顺序项对应。
练习
1、请在t10-12.asm的基础上,新建一个完整的实例。
2、如果条件转移超出转移范围,应该如何处理?
3、 写一个把字符串中的小写字母变换为对应的大写字母的程序,假设字符串以0结尾。
4、写一个统计字符串长度的程序,假设字符串以0结尾。
5、写一个滤去某个字符串中空格符号(ASSCII码20h)的程序,字符串0结尾。
6、请写一个把两个字符串合并的示例程序。
7、请写一个把某个十进制数ASCII码串转换为对应的非压缩BCD和压缩BCD的示例程序。
8、请写一个把某个十进制数ASCII码串转换为BCD码对应的二进制数的示例程序。
9、请写一个把某个十六进制数转换为对应的二进制数ASCII码串的示例程序。
10、请写一个数据块拷贝的示例程序。将数据块复制到另外一个指定的地址处。
本文摘自编程达人系列教材《X86汇编语言基础教程》。