call指令和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP,它们经常被共同用来实现子程序的设计
10.1 ret 和 retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移
CPU执行ret指令时,进行两步操作:
1,(IP) = ((ss)*16 + (sp))
2,(sp) = (sp) + 2
相当于:
pop IP
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移
CPU执行retf指令时,进行四步操作:
1,(IP) = ((ss)*16 + (sp))
2,(sp) = (sp) + 2
3,(CS) = ((ss)*16 + (sp))
4,(sp) = (sp) + 2
相当于:
pop IP
pop CS
下面的程序中,ret指令执行后,(IP) = 0,CS:IP指向代码段的第一条指令
10.2 call指令
CPU执行call指令,进行两步操作:
1,将当前的IP或CS和IP压入栈
2,转移
call指令不能实现短转移,call指令实现转移的方法和jmp指令的原理相同
10.3 依据位移进行转移的call指令
call 标号 (将当前的IP压入栈,转到标号处执行指令)
执行此种格式的call指令时,进行如下操作:
1,(sp) = (sp) -2
((ss)*16 + (sp)) = (IP)
2,(IP) = (IP)+16位位移
相当于进行:
push IP
jmp near ptr 标号
10.4 转移的目的地址在指令中的call指令
call far ptr 标号 实现的是 段间转移
执行此种格式的call指令时,进行如下操作:
1,(sp) = (sp) -2
((ss)*16 + (sp)) = (CS)
(sp) = (sp) -2
((ss)*16 + (sp)) = (IP)
2,(CS) = 标号所在段的段地址
(IP) = 标号所在段中的偏移地址
相当于进行:
push CS
push IP
jmp far ptr 标号
10.5 转移地址在寄存器中的call指令
call 16位reg
功能:
(sp) = (sp) - 2
((ss)*16 + (sp)) = (IP)
(IP) = (16位reg)
相当于进行:
push IP
jmp 16位reg
10.6 转移地址在内存中的call指令
转移地址在内存中的call指令有两种格式:
1,call word ptr 内存单元地址
相当于进行:
push IP
jmp word ptr 内存单元
2,call dword ptr 内存单元地址
相当于进行:
push CS
push IP
jmp dword ptr 内存单元
10.7 call 和 ret 的配合使用
考虑下面程序返回前,bx中的值为多少?
assume cs:code
code segment
start: mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s:add ax,ax
loop s
ret
code ends
end start
分析CPU执行该程序的主要过程:
1,CPU将 call s指令的机器码读入,IP指向了call s后的指令 mov bx,ax,然后CPU执行call s的指令,将当前的IP值(指令mov bx,ax的偏移地址)压栈,并将IP的值改变为标号处的偏移地址
2,CPU从标号s处开始执行指令,loop循环完毕后,(ax)=8
3,CPU将ret指令的机器码读入,IP指向了ret指令后的内存单元,然后CPU执行ret指令,从栈中弹出一个值(即call s先前压入的mov bx,ax指令的偏移地址)送入IP中,则CS:IP指向指令mov bx,ax
4,CPU从mov bx,ax开始执行指令,直到程序完成
10.8 mul 指令
mul是乘法指令,注意两点:
1,两个相乘的数,要么都是8位,要么都是16位,如果是8位,一个默认放在al中,另一个放在8位reg或内存字节单元中,如果是16位,一个默认放在ax中,另一个放在16位reg或内存字单元中
2,结果如果是8位乘法,结果默认放在ax中,如果为16位乘法,结果高位默认放在dx中,低位放在ax中
如:
mul byte ptr ds:[0]
含义:(ax) = (al) * ((ds)*16 + 0)
mul word ptr [bx+si+8]
含义:
(ax) = (ax)*((ds)*16 +(bx)+(si) +8)结果的低16位
(dx) = (ax)*((ds)*16 +(bx)+(si) +8)结果的高16位
10.9 模块化程序设计
可以看到,call 和 ret指令共同支持了汇编语言中的模块化设计,在实际编程中,程序的模块化必不可少,因为现实的问题比较复杂,对现实问题进行分析时,把它转化为相互联系,不同层次的子问题,是必须的解决方法
利用call和ret指令,可以用简捷的方法,实现多个相互关联,功能独立的子程序来解决一个复杂的问题
10.10 参数和结果传递的问题
子程序将结果(返回值)提供给调用者,那么如何存储子程序需要的参数和产生的返回值
如:设计一个子程序,提供N,计算N的3次方
那么考虑:
1,将参数N存储在什么地方?
2,将计算得到的数值存储在什么地方?
显然,可以用寄存器存储,可以将参数放到bx中,将结果放到dx和ax中
用寄存器来存储参数和结果是最常使用的方法,调用者将参数送入参数寄存器,从结果寄存器中取到返回值,子程序从参数寄存器中取到参数,将返回值送入结果寄存器
10.11 批量数据的传递
当子程序需要多个参数时,需要传递多个值,寄存器的数量始终是有限的,不可能简单地用寄存器存放多个需要传递的数据,对于返回值,也有同样的考量
这时,考虑将批量的数据放入到内存中,然后将它们所在的内存空间的首地址放在寄存器中,传递给需要的子程序,对于具有批量数据的返回值,也使用同样的方法
10.12 寄存器冲突的问题
设计一个程序,功能:将一个全是字母,以0结尾的字符串,转化为大写,程序要处理的字符串以0作为结尾符,这个字符串可以如下定义:
db ‘conversation’,0
字符串的内容后面一定要有一个0,标记字符串的结束,子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化,如果是0,就结束处理,由于可以通过检测0而知道是否已经处理完字符串,所以子程序可以不需要字符串的长度的作为参数,可以用jcxz来检测0
但上述程序中存在错误,问题在于cx的使用,主程序主要使用cx记录循环次数,可子程序中也使用了cx,在执行子程序时,cx中实际保存的数值被改变,使得主程序的循环出错
解决这个问题的便捷方法是:在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复,可以用栈来保存寄存器中的内容
对capatial段的改进使用栈避免寄存器冲突: