(十)汇编语言——CALL和RET指令
相信大家肯定在C语言里面接触过函数这个概念,或者是一些高级语言里面的方法,那么汇编语言有没有这样类似的概念呢,答案是当然的,接下来就让我们来介绍一下汇编的模块化程序设计。
CALL指令
这个CALL指令呢,我们是第一次接触,它主要的作用就是调用子程序,实质上就是进行流程转移,而且实现转移的方法和jmp指令的原理相似。使用的格式就是call 标号,这个就类似于,把当前位置保存起来,去执行其他地方的代码,执行完了之后再回到原处去执行。就类似于我们的中断。
功能
首先将当前的IP压入栈中,然后转移到标号处执行指令。相当于下面的语句。实现的是段间转移,如果我们要实现段内转移呢?我们继续讲解。
- (sp)=(sp)–2
- ((ss)*16+(sp))=(IP)
- (IP)=(IP)+16位位移
push IP
jmp near ptr 标号
寄存器
而如果转移地址在寄存器里面的话,就有一点不一样了,但是和我们之前介绍的 jmp 指令类似的,我们具体看看吧!就相当于下面的语句。
push IP
jmp 16位寄存器
内存
转移地址在内存中的call指令其实也是类似的,我们给出相应的语句,相信大家可以很清楚的理解到。
;call word ptr 内存单元地址
push IP
jmp word ptr 内存单元地址
;calld word ptr 内存单元地址
push CS
push IP
jmp dword ptr 内存单元地址
段间转移
我们如果想要实现段间转移的话,使用的指令就是call far ptr 标号,这个时候,我们就会把CS和IP同时入栈保存起来。具体步骤如下所示:
- (sp)=(sp)–2
- ((ss)×16+(sp))=(CS)
- (sp)=(sp)–2
- ((ss)×16+(sp))=(IP)
- (CS)=标号所在的段地址
- (IP)=标号所在的偏移地址
push CS
push IP
jmp far ptr 标号
“call 标号”类似”jmp near ptr 标号”,对应的机器指令中为相对于当前IP的转移位移,而不是转移的目的地址,实现段内转移。而指令“call far ptr 标号”实现的是段间转移!
返回指令
我们这里主要介绍ret指令和retf指令。
ret
用栈中的数据,修改IP的内容,从而实现近转移,相当于 pop IP。
retf
用栈中的数据,修改CS和IP的内容,从而实现远转移;
实例
这个程序就是计算一下ax的平方,但是值得注意的就是,这里我们用到了栈段,因为我们的call 指令和ret指令需要入栈和出栈操作。
assume cs:code, ss:stack
stack segment
db 8 dup(0)
db 8 dup(0)
stack ends
code segment
start :mov ax, stack
mov ss,ax
mov sp,16
mov ax,1000
call s
mov ax, 4c00h
int 21h
s:add ax,ax
ret
code ends
end start
MUL指令
mul指令就是我们的乘法指令,我们之前介绍过除法div指令,我们来简单回顾一下。
被除数 | AX | DX和AX |
---|---|---|
除数 | 8位内存或寄存器 | 16位内存或寄存器 |
商 | AL | AX |
余数 | AH | DX |
而接下来我们介绍的乘法指令与这个类似,我们来看看吧!
被乘数 | AL | AX |
---|---|---|
除数 | 8位内存或寄存器 | 16位内存或寄存器 |
结果 | AX | DX(高位)和AX(低位) |
我们来看一个例子吧!计算100乘10和100乘10000。
;100*10
mov al,100
mov bl,10
mul bl
;100*10000
mov ax,100
mov bx,10000
mul bx
模块化程序设计
我们知道,在程序设计中,模块化设计是十分重要的,那么,在汇编语言中有没有这要的设计呢?聪明的小伙伴们应该想到了,调用我们刚刚介绍的CALL 指令和RET指令即可。但是呢,需要我们去解决两个问题,那就是参数和返回值的问题,我们来看一下这两个问题我们如何来解决。
在这之前,我们来看一个问题,就是根据提供的N,计算N的3次方。我们可以考虑用循环去做,但是我们现在选择使用模块化程序设计的方法去解决,具体解决办法如下:
寄存器
把数据存储到寄存器里面是一个解决办法,我们来看看具体的操作。
- 首先把参数放到 bx 中,即(bx)=N;
- 然后子程序中用多个mul指令计算N3;
- 最后将结果放到dx和ax中:(dx:ax)=N3;
assume cs:code
data segment
dw 1,2,3,4,5,6,7,8
dd 0,0,0,0,0,0,0,0
data ends
code segment
start:mov ax,data
mov ds,ax
mov si,0 ;指向第一组单词
mov di,16 ;指向第二组单词
mov cx,8
s:mov bx,[si]
call cube
mov [di],ax
mov [di].2,dx
add si,2
add di,4
loop s ;循环处理
mov ax,4c00h
int 21h
cube:mov ax,bx
mul bx
mul bx
ret
code ends
end start
这样处理当然是没有问题的,但是会面临一个问题,就是我们的寄存器只有那么多,如果需要传递的东西比较多,寄存器不够用了怎么办,确实这是我们会遇到的问题。既然寄存器比较少,我们就换一个方法,使用内存单元去传递信息。
内存单元
为了避免寄存器不够的尴尬,于是我们现在使用内存单元去传递数据。我们的做法就是先将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。
比如我们看这个例子,将data段中的字符串转化为大写。
data segment
db 'conversation'
data ends
我们来看一下具体的代码段怎么去写。
code segment
start:mov ax,data
mov ds,ax
mov si,0
mov cx,12;循环次数
call capital
mov ax,4c00h
int 21h
capital: and byte ptr [si],11011111b;变大写
inc si
loop capital
ret
code ends
目前这样基本上是没有什么问题了。但是我们还要介绍另外一种方法,那就是通过我们的栈来实现传递参数。
栈
接下来我们就使用栈来进行参数传递,主要的原理就是由调用者将需要传递给子程序的参数压入栈中,子程序从栈中取得参数。同样的,我们介绍一个例子,那就是计算(a–b)3,a、b为word型数据。我们来看一下代码段部分。
code segment
start:
mov ax,1
push ax
mov ax,3
push ax
;首先将一些数据入栈
call difcube
;调用子程序
mov ax,4c00h
int 21h
difcube:
push bp
;先在栈中保存BP的值
mov bp,sp
mov ax,[bp+4]
sub ax,[bp+6]
mov bp,ax
mul bp
mul bp
;获得参数并计算,返回到AX中
pop bp
;恢复之前的BP值
ret 4
;放弃入栈的参数
code ends
好啦,这就是关于用栈来传递参数的方法,我们就介绍到这。
寄存器冲突问题
接下来我们将来解决有关寄存器冲突的问题,具体来说就是避免在子程序里面使用的寄存器与主程序里面使用的寄存器冲突了,导致程序无法运行。好啦,接下来我们就会来介绍如何解决这个问题。给大家一个提示,那就是使用我们经常使用的栈来保存数据。
方法
前面说了,我们是使用栈去解决这个问题,那么就让我们来卡看具体如何使用吧。
子程序开始:
- 子程序中使用的寄存器入栈
- 子程序内容
- 子程序使用的寄存器出栈
- 返回(ret、retf)
capital:
push cx
push si
change:
mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok:
pop si
pop cx
ret
大家看这个地方,原理就是用栈先去保存数据,然后在程序结束的时候再把原来的数据出栈。
子程序中使用的寄存器入栈
2. 子程序内容
3. 子程序使用的寄存器出栈
4. 返回(ret、retf)文章来源:https://www.toymoban.com/news/detail-753560.html
capital:
push cx
push si
change:
mov cl,[si]
mov ch,0
jcxz ok
and byte ptr [si],11011111b
inc si
jmp short change
ok:
pop si
pop cx
ret
大家看这个地方,原理就是用栈先去保存数据,然后在程序结束的时候再把原来的数据出栈。文章来源地址https://www.toymoban.com/news/detail-753560.html
到了这里,关于(十)汇编语言——CALL和RET指令的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!