前言
点赞再看,养成习惯!
回顾
还记得我们之前讲过的ret
指令搭配call
指令实现的子程序模板吗?忘记了没关系,我们可以再看一遍:
assume cs:code
code segment
main :
call sub1 ; 调用子程序sub1 ,也可以说是方法1
...
mov ax,4c00h
int 21h
sub1:
...
ret ;返回主程序
code ends
end main
这个模板记住哦,等下要用的。
一、 模块化程序设计
1.1 子程序
如果屏幕前的你有其它语言的基础,那么子程序这个概念对于你来说是很好理解的。它就像是其它语言里的method,是为了处理主程序中某一块特定业务而存在的。这部分子程序通常都有以下的特点:
- 通常是可复用的,允许多个主程序重复使用的逻辑程序块。
- 一般如果某段特定逻辑需要向主程序隐藏实现细节,也可以使用子程序。
- 具有一定的独立性,相较于其他代码,更偏向于完成默写特定逻辑。
而子程序这个概念在不同的计算机语言中也会被称为不同的名称,比如函数,程序,方法等等都是属于子程序的一种。
1.2 子程序的参数和返回值
当我们知晓了子程序是为主程序处理特定业务场景的职责后,新的问题就接踵而至。由于子程序的代码段并没有功能主程序的数据段,因此我们需要为子程序提供特定的参数(形参),并且在子程序运算结束后还要将结果返回给主程序(返回值)。那么在这个前提下,我们接下来需要探讨的问题就很简单了,我们如何为子程序提供参数并存储返回值?
比如: 我们设计一个子程序用以计算N的三次方
思路剖析:
- 第一个关注点就是我们如何将N传递给子程序
- 第二个关注点是我们需要设计一个N的次方的逻辑代码
- 第三个关注点就是如何为主程序提供结果
那么这里面我们首先去做可以解决的问题:计算N的三次方的结果。 我们知道N的三次方就是将N自乘三次,很巧的是我们在上一章节曾经讲过如何进行乘法,那么这里我们先将这部分的代码设计出来。
assume cs:code
code segment
mov ax,n
mov cx,3
cobe: mul ax
loop cobe
code ends
end
接下来我们需要解决的问题就是参数传递的问题了,首先按照我们已有的知识体系推断,数据一定会被存放进某个内存单元或寄存器,首先我们来考虑一下参数放到哪里合适呢?比如当前我们的需求只有一个参数,如果放到内存中也是要将内存地址放入寄存器中,这样就不如直接放到寄存器中简易。而可以存放通用数据的寄存器有四种:ax
bx
cx
dx
,其中cx
又是状态寄存器,ax
,dx
要参与乘法运算,想来用bx寄存器是最合适的了。而返回值由于我们需要的就是乘法运算的结果,因此使用ax
,dx
即可。接下来我们只需要补充完整整个程序:
assume cs:code
code segment
main: mov bx,n ; n代表变量
call cube
cube: mov ax, bx
mul ax
mul ax
ret
code ends
end
好,让我们来验证下我们的程序。让我们来定义一个data段,data段分为两组,第一组负责提供参数,第二组负责保存结果。
assume cs:code
data segment
dw 0,1,2,3,4,5,6,7
dd 8 dup(0)
data ends
code segment
main : mov ax,data
mov ds,ax
mov si,0 ; 指向第一组内存单元 即变量
mov di,16 ; 指向第二组内存单元 即返回值
mov cx,8
getAndSave: mov bx,[si]
call cube ; 进行立方运算
mov [di],ax
mov [di].2 ,dx
add si,2
add di,4
loop getAndSave
mov ax,4c00h ;中断语句
int 21h
cube: mov ax,bx
mul ax
mul ax
ret
code ends
end main
结果验证:
1.3 批量数据的传递
那如果我们需要传递的参数不止一个呢?比如有3个?这个时候显然我们再想将参数放入到寄存器中是不合适的。在这种情况下,我们需要做的是将参数放入到内存中,然后将它们所在的内存空间的首地址放在寄存器中,传递给需要的子程序,对于具有批量数据的返回结果也可如此(用栈空间也可以)。让我们验证下:
设计一个程序,将一个全是字母的字符串转化为大写
这里我们需要关注两件事情:
- 字符串的内容和字符串的长度。这是因为我们需要告知子程序他们需要在内存中处理的数据的大小。假设我们设计的程序一次处理一个字符长度的字母,那我们完全可以使用
loop
循环进行大小写转换。这样的话我们可以将字符串长度存放到CX寄存器中。
- 还记得如何转换大小写么?如果忘记的同学请复习下逻辑运算章节。
程序:
assume cs:code
data segment
db 'conversation'
data ends
code segment
main:
mov ax,data
mov ds,ax
mov si,0
mov cx,12 ; 字符串长度为12
call uppercase
mov ax,4c00h
int 21h
uppercase:
and byte ptr [si] ,11011111b ; 这里看不懂的同学请复习下逻辑运算章节
inc si
loop uppercase
ret
code ends
end main
验证结果:
1.4 寄存器的冲突处理
我们先通过业务场景模拟一下冲突场面。现在有几段字符串,均是以0结尾,请将这些字符串转化为大写。
让我们先看下数据段:
assume cs:code
data segment
db 'hello',0
db 'world',0
data ends
由于数据段都会以0结尾,因此我们不再需要将长度作为判断标准,只需要判断0即可。我们以此设计程序:
assume cs:code
data segment
db 'hello',0
db 'world',0
data ends
code segment
main:
mov ax,data
mov ds,ax
mov si,0
call upperCase
mov ax,4c00h
int 21h
upperCase: and byte ptr [si] ,11011111b
inc si
mov ch,0 ; 高位赋0,低位赋原有值 ,若原有值位0则自动中断循环
mov cl,si
loop upperCase
ret
code ends
end main
但是到这里还是有个问题,我们的程序只能够处理完第一个字符串,但是第二个字符串还是没有办法处理。这是为什么呢?其实主要还是由于我们两个需求都太过依赖cx寄存器
。有咩有什么改进的办法呢?其实也很简单,我们只需要将我们冲突的寄存器的值在冲突覆盖前保存下来,然后再冲突结束后再恢复即可。
assume cs:code
data segment
db 'hello',0
db 'world',0
data ends
stack segment
db 16 dup(0)
stack ends
code segment
main:
mov ax,data
mov ds,ax
mov si,0
mov ax,stack
mov ss,ax
mov sp,16
mov cx ,2
change:
push cx
call upperCase
pop cx
loop change
mov ax,4c00h
int 21h
upperCase: and byte ptr [si] ,11011111b
inc si
mov ch,0
mov cl,[si]
loop upperCase
ret
code ends
end main
这样对吗?还是不对,因为loop在判断cx = 0的时候会将cx-1 = FFFF ,此时不等于0,程序会进入死循环。我们该怎么办呢?很简单,用我们之前学到过的判断指令jcxz
即可。
assume cs:code
data segment
db 'hello',0
db 'world',0
data ends
stack segment
db 16 dup(0)
stack ends
code segment
main:
mov ax,data
mov ds,ax
mov si,0
mov ax,stack
mov ss,ax
mov sp,16
mov cx ,2
change:
push cx
call upperCase
pop cx
loop change
mov ax,4c00h
int 21h
upperCase: and byte ptr [si] ,11011111b
inc si
mov ch,0
mov cl,[si]
jcxz return
loop upperCase
return: ret
code ends
end main
验证结果:
结语
今天的内容就到此结束了,有疑问的小伙伴欢迎评论区留言或者私信博主,博主会在第一时间为你解答。
码字不易,感到有收获的小伙伴记得要关注博主一键三连,不要当白嫖怪哦~
如果大家有什么意见和建议请评论区留言或私聊博主,博主会第一时间反馈的哦。