call和ret指令都是转移指令,都可修改IP,或同时修改CS和IP,它们经常被共同用来实现子程序的设计
ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移
ret指令操作:
(IP)=((ss)*16+(sp))(sp)=(sp)+2
retf指令操作:
(IP)=((ss)*16+(sp))(sp)=(sp)+2(CS)=((ss)*16+(sp))(sp)=(sp)+2
call指令
call指令的两步操作:
将当前的
IP或CS和IP压入栈中转移
call指令不能实现短转移
依据位移进行转移的call指令
格式:call 标号(将当前IP压栈后,转到标号处执行指令)
call指令的操作:
(sp)=(sp)-2((ss)*16+(sp))=(IP)(IP)=(IP)+16位位移
16位位移=标号处的地址-call指令后的第一个字节的地址
16位位移的范围-32768–32767,以补码表示
16位位移由编译程序在编译时算出
转移的目的地址在指令中的call指令
格式:call far ptr 标号,实现段间转移
操作:
(sp)=(sp)-2((ss)*16+(sp))=(CS)(sp)=(sp)-2((ss)*16+(sp))=(IP)(CS)=标号所在段的段地址(IP)=标号在段中的偏移地址
转移地址在寄存器中的call指令
格式:call 16位reg
操作:
(sp)=(sp)-2((ss)*16+(sp))=(IP)(IP)=(16位reg)
转移地址在内存中的call指令
格式:
call word ptr 内存单元地址操作:
(sp)=(sp)-2((ss)*16+(sp))=(IP)(IP)=(内存单元地址)
相当于进行:
1
2push IP
jmp word ptr 内存单元地址1
2
3
4
5
6mov sp,10h
mov ax,0123h
mov ds:[0],ax
call word ptr ds:[0]
执行后,(IP)=0123H,(sp)=0EH
call dword ptr 内存单元地址操作:
(sp)=(sp)-2((ss)*16+(sp))=(CS)(sp)=(sp)-2((ss)*16+(sp))=(IP)(CS)=(内存单元地址+2)(IP)=(内存单元地址)
相当于进行:
1
2
3push CS
push IP
jmp dword ptr 内存单元地址1
2
3
4
5
6
7mov sp,10h
mov ax,0123h
mov ds:[0],ax
mov word ptr ds:[2],0
call dword ptr ds:[0]
执行后,(CS)=0,(IP)=0123h,(sp)=0Ch
call和ret的配合使用
使用call和ret实现子程序的机制
程序返回前,bx的值?
1 | assume cs:code |
分析:
CPU将call s指令的机器码读入,此时IP指向call s后的指令mov bx,ax。CPU执行call s指令,将当前IP值(指令mov bx,ax的偏移地址)压栈,并将IP的值改变为标号s处的偏移地址CPU从标号s处执行指令,loop循环完成后,(ax)=8CPU将ret指令的机器码读入,此时IP指向ret指令后的内存单元。CPU执行ret指令,从栈中弹出一个值(即call s先前压入栈的mov bx,ax指令的偏移地址)送入IP中,则CS:IP指向指令mov bx,axCPU从mov bx,ax开始执行指令,直至完成
结论
可以写一个具有一定功能的程序段,称之为子程序
在需要执行子程序时,用call指令转去执行,此时,会将call指令后面的指令的地址存储在栈中。当子程序执行完成后,在子程序后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行
子程序框架
1 | assume cs:code |
mul指令
乘法指令
注意:
两个乘数:
必须同为8位或同为16位
若为8位,一个默认放在
al中,另一个放在8位reg或内存字节单元中若为16位,一个默认放在
ax中,另一个放在16位reg或内存字单元中结果:
若为8位乘法,结果默认在
ax中若为16位乘法,结果高位默认在
dx中存放,低位在ax中存放
格式:
mul regmul 内存单元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位
例:
计算100*10
分析:100和10小于255,可做8位乘法
1
2
3mov al,100
mov bl,10
mul bl结果:
(ax)=1000(03E8H)计算100*10000
分析:100小于255,但10000大于255,所以必须做16位乘法
1
2
3mov ax,100
mov bx,10000
mul bx结果:
(ax)=4240H,(dx)=000FH(F4240H=1000000)
模块化程序设计
利用call和ret指令,可以用简捷的方法,实现多个互相联系、功能独立的子程序来解决一个复杂的问题
参数和结果传递的问题
子程序一般都要根据提供的参数处理一定的
事务,处理后,将结果(返回值)提供给调用者
讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值
用寄存器来存储参数和结果是最常用的方法
对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作相反:调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器
计算data段中第一组数据的3次方,结果保存到后一组dword单元中
1 | assume cs:code |
批量数据的传递
当进行传递的数据过多时,寄存器的数量有限,不可能简单的用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题
可以使用内存空间,将批量数据放到内存中,然后将数据所在的内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可使用同样的方法
编程,将字母字符串转换大写字母
1 | assume cs:code |
寄存器冲突的问题
编程,将一个全是字母,以0结尾的字符串,转换为大写
1 | assume cs:code |
此程序中存在问题,在于cx,在主程序中使用了cx记录循环次数,可在子程序中也使用了cx,并且改变了cx的值,会使得主程序的循环出错
一般化问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突
设想:
编写要调用子程序的程序的时候不必关心子程序到底使用了哪些寄存器
编写子程序的时候不必关心调用者使用了哪些寄存器
不会发生寄存器冲突
解决方案:在子程序的开始将子程序中所用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以使用栈来保存寄存器中的内容
改进子程序capital:
1 | capital:push cx |
编写子程序的标准框架:
1 | 子程序开始: |
编写子程序
解决除法溢出的问题
在使用div指令做除法的时候,会出现:结果的商过大,超出了寄存器所能存储的范围。这将引起CPU的一个内部错误,称为除法溢出
子程序描述
名称:divdw
功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型
参数:
(ax)=dword型数据的低16位(dx)=dword型数据的高16位(cx)=除数
返回:
(dx)=结果的高16位(ax)=结果的低16位(cx)=余数
提示
公式:
X/N=int(H/N)*65536+[rem(H/N)*65536+L]/N
X:被除数,范围[0,FFFFFFFF]
N:除数,范围[0,FFFF]
H:X高16位,范围[0,FFFF]
L:X低16位,范围[0,FFFF]
int():描述性运算符,取商
rem():描述性运算符,取余数
此公式将可能产生溢出的除法运算X/N转变为多个不会产生溢出的除法运算
公式等号右边的所有除法运算都可以用div指令来做,肯定不会导致除法溢出