# 第10章 CALL和RET指令
10.1 ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移。
retf指令用栈中的数据,修改CS和IP内容,从而实现远转移。
CPU执行ret指令时,进行下面两步操作:
- (IP)=((SS)*16+(SP))
- (SP)=(SP)+2
这是将栈顶的数据存入IP,然后将该数据进行出栈操作。
CPU执行retf指令时,进行下面四步操作:
- (IP)=((SS)*16+(SP))
- (SP)=(SP)+2
- (CS)=((SS)*16+(SP))
- (SP)=(SP)+2
通过上面可以看出,段地址存储在高位,故要先存储CS,之后在存储IP,这种操作不要忘记。
例子:
push cs
push ax
retf
执行完毕后,程序指向 CS:[ax]的地址,先存入段地址,之后存入偏移地址,这种操作你应该理解。
10.2 call指令
CPU执行call指令时,进行两步操作:
- 将当前的IP或CS和IP压入栈中;
- 转移。
call指令不能实现短转移,除此之外,和jmp指令的原理相同。
10.3 根据位移进行转移的call指令
call 标号(将call指令的下一条指令压入栈后,转到标号处之行指令)
还记得CPU读取指令的顺序吗?读取一条指令进入指令缓冲区后,IP立刻指向下一个指令。所以,在缓冲区的call指令执行存储IP时,是指向下一条指令而不是本身指令,这个概念应该理解。
另外,call指令就相当于完成上面所讲的入栈操作。
CPU执行call指令时,进行如下的操作:
- (sp)=(sp)-2 |((SS)*16+(SP))=(IP)
- (IP)=(IP)+16位位移
16位位移由编译程序算出。
相当于:
push IP
jmp near ptr 标号
10.4 转移的目的地址在指令中的call指令
call far ptr 标号
相当于:
push CS
push IP
jmp far ptr 标号
10.5 转移地址在寄存器中的call指令
call 16位reg
注意,一个寄存器大小为16位,一个字,仅能代表一个地址,所以,这里自然就是偏移地址了。
push IP
jmp 16位 reg
10.6 转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式:
- call word ptr 内存单元地址
- call dword ptr 内存单元地址
这个的区别一目了然。
前者针对偏移地址,而后者针对目的地址和偏移地址。
后者相当于:
push cs
push ip
jmp dword ptr 内存单元
其中根据栈的规则,必须先存放CS,这个概念是你要理解的。
举个例子:
mov sp,0h
mov ax,0123h
mov ds:[0],ax ;低位,这个是IP地址。
mov word ptr ds:[2],0 ;高位,这个是段地址。
call dword ptr ds:[0]
我们应该着重去理解其内存中有如何向内存中写入地址,很简单,就是手动写入,然后声明地址的开头,程序就会自动读取,就是这个样子。
10.7 call和ret的配合使用
我们可以通过call和ret指令,来实现子程序的机制
举个例子:
start: mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
通过这个程序,可以看出,这里使用call 标号的指令,这里的标号可以直接使用段标记。
这里进入子程序s,然后算其三次方,结果存储在ax中,最后ret返回到call下一条指令。
这些逻辑顺序其实是很好理解的。
10.8 mul指令
介绍一下mul指令,mul是乘法指令,使用mul指令时,注意以下两点:
- 两个相乘的数,要么都是8位,要么都是16位。如果时8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果是16位,一个默认在AX中,另一个放在16位reg或内存字单元中。
- 结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认放在DX中,低位放在AX中。
格式如下:
mul reg
mul 内存单元
内存单元可以用不同的寻址方式给出,比如:
mul byte ptr ds:[0]
10.9 模块化程序设计
从上面我们看到,call和ret指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序的模块化是比不可少的。因为实现的问题比较复杂,对现实问题进行分析时,把它转化为相互联系,不同层次的子问题,是必须解决的办法,而利用call和ret指令则是很好的解决方法
10.10 参数和结果传递的问题
这里设计子程序传递参数时存在的两个问题:
- 将参数N存储在什么地方?
- 计算得到的值,存储在什么地方?
用寄存器来存储参数和结果是最常使用的方法。
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
cube: mov
往内存中写,其实就是用mov指令,这里还有点“恐惧”,其实就是这点样子,你随便指明个内存单元,然后下标的形式来表示出相对地址,后面是要存储的数据。
其中,si有数据,所以指向栈界。而di无数据,指向栈顶。
在建立相关内存空间时,利用 关键字 dd,dw之类的就能很好的建立出你所想要建立的内存。
而往内存中写入结果,则利用[di]就行,[di].2这种相对地址的给出办法,你不用在去改变di的值,在最后地址改变两个字,即4位,这种程序的执行方式其实很好理解的。
10.11 批量数据的传递
寄存器的数量终究是有限的,当数据量过大,显然不能放在寄存器中存取,这是,你可以利用[di],[si]这种来表示内存地址,直接在内存中进行修改。
对于内存地址的修改,我们似乎不再那么恐惧,我们可以使用[di]的形式来指向内存,然后对di加法运算之类的来更好的。我们需要明确的就是开始让(di)==0,这种思路就会很好理解的。
举个例子:将data中的字符串转化为大写
assume cs:code
data segment
db'conversation'
data ends
code segment
start: mov ax,data
mob 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
end start
从代码中可以看出,其实际上使用[si]来表示出相关内存的,直接在内存中进行有关运算。当处理好一个字节之后,inc si,让si继续增加来指向下一个字节,这种逻辑是你需要明确的。
”运算;增加地址;循环到运算“,这种步骤是你需要明确的。
10.12 寄存器冲突问题
如jcxz和loop指令都会用到寄存器cx,这样如果在一起使用,很可能会发生冲突,这是你需要明确的。
我们一个常用的解决办法就是将你的数据压入栈中,等之后再推出来,这种操作是需要你明确的。
举个例子:要程序处理的字符串以0位结尾符,小写换大写,这个字符串可以如下定义:
data segment
db 'conversation',0
db 'conversation',0
db 'conversation',0
segment ends
code segment
strat: mov ax,data
mov ds,ax
mov bx,0
mov cx,4
s: mov si,bx
call capital
add bx,5
loop s
mov ax,4c00h
int 21h
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
其中哪些地方是你需要处理的,一定要注意好程序的入栈和出栈顺序
还有要确保所有内容都出栈时才使用ret,否则可能导致数据提取的不对。
该程序是双循环的架构,里面处理的是一行中的内容,这个逻辑是你要明确的!
编写子程序的标准框架:
子程序开始: 子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret,retf)
本文详细介绍了汇编语言中的CALL和RET指令,包括它们在转移控制中的作用,如ret用于近转移,retf用于远转移,以及各种形式的call指令,如根据位移、内存地址和寄存器的转移。此外,还讨论了call和ret如何配合实现子程序机制,以及在模块化程序设计中的应用。同时,提到了mul乘法指令和参数、结果的传递问题,以及批量数据处理和寄存器冲突的解决方案。
964

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



