call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序的设计。
ret 和 retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移;
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移;
CPU执行ret指令时,进行下面两步操作:
- (ip)=((ss)*16+(sp))
- (sp)=(sp)+2
CPU执行retf指令时,进行下面4步操作:
- (ip)=((ss)*16+(sp))
- (sp)=(sp)+2
- (cs)=((ss)*16+(sp))
- (sp)=(sp)+2
可以看出,如果我们用汇编语法来解释ret和retf指令,则:
CPU执行ret指令时,相当于执行:
pop IP
CPU执行retf指令时,相当于执行:
pop IP
pop CS
依据位移进行转移的call指令
call 标号(将当前的IP压入栈后,转到目标处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
- (sp)=(sp)-2
- ((ss)*16+(sp))=(ip)
- (ip)=(ip)+16位位移。
- 16位位移=标号处的地址 - call 指令后的第一个字节的地址;
- near ptr 指明此处的位移为16位位移;
- 16位位移的范围为 -32768~32767,用补码表示;
- 16位位移由编译程序在编译时算出。
CPU执行 call 标号 时,相当于进行:
push IP
jmp near ptr 标号
转移的目的地址在指令中的call指令
call far ptr 标号 实现的是段间转移。
CPU执行此种格式的call指令时,进行如下的操作。
- (sp)=(sp)-2
- ((ss)*16+(sp))=(cs)
- (sp)=(sp)-2
- ((ss)*16+(sp))=(ip)
- (CS)=标号所在段的段地址
- (IP)=标号在段中的偏移地址
CPU执行 call far ptr 标号 时,相当于进行:
push CS
push IP
jmp far ptr 标号
转移地址在寄存器中的call指令
指令格式:call 16 位 reg
功能:
- (sp)=(sp)-2
- ((ss)*16+(sp))=(ip)
- (ip)=(16位reg)
CPU执行 call far ptr 标号 时,相当于进行:
push IP
jmp 16位 reg
转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式。
- call word ptr 内存单元地址
CPU执行 call word ptr 内存单元地址 时,相当于进行:
push IP
jmp word ptr 内存单元地址
- call dword ptr 内存单元地址
CPU执行 call dword ptr 内存单元地址 时,相当于进行:
push CS
push IP
jmp dword ptr 内存单元地址
mul 指令
mul 是乘法指令,使用mul 做乘法的时候注意以下两点:
- 两个相乘的数:两个相乘的数,要么都是8位,要么都是16位。如果都是8位,一个默认放在AL中,另一个放在8位reg或内存字单元中;如果都是16位,一个默认在AX中,另一个放在16位reg或内存字单元中。
- 结构:如果是8位乘法,结构默认放在AX中;如果是16位乘法,结构高位默认在DX中存放,低位在AX中放。
寄存器冲突的问题
我们利用call和ret来实现子程序的机制。子程序的框架如下。
标号:
指令
ret
具有子程序的源程序的框架如下。
assume cs:code
code segment
main:
:
call sub1 ;调用子程序sub1
:
:
mov ax,4c00h
int 21h
sub1: ;子程序sub1开始
:
call sub2 ;调用子程序sub2
:
:
ret ;子程序返回
sub2: ;子程序sub2开始
:
ret ;子程序返回
code ends
end main
但可能引出一个一般化的问题:子程序中使用的寄存器,很有可能在主程序中也要使用,造成了寄存器使用上的冲突。
解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的及寄存器中的内容都保存起来,在子程序返回前再恢复。 可以用栈来保存寄存器中的内容。
所以,我们编写子程序的标准框架:
子程序开始: 子程序中使用的寄存器入栈
子程序的内容
子程序中使用的寄存器出栈
返回(ret,retf)