汇编语言--call和ret指令
call和ret指令
call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。
它们经常被共同用来实现子程序的设计。
ret和retf
ret指令用栈中的数据,修改IP的内容,从而实现近转移;
retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
CPU执行ret指令时,进行下面的两步操作:
(1)(IP) = ((ss)*16 +(sp))
(2)(sp) = (sp)+2
CPU执行retf指令时,进行下面四步操作:
(1)(IP) = ((ss)*16) + (sp)
(2)(sp) = (sp) + 2
(3)(CS) = ((ss)*16) + (sp)
(4)(sp) = (sp) + 2
用汇编语法来解释ret和retf指令,则:
CPU执行ret指令时,相当于进行:
pop IP
CPU执行retf指令时,相当于进行:
pop IP
pop CS
call指令
CPU执行call指令时,进行两步操作:
(1) 将当前的IP或CS和IP压入栈中;(注意是当前IP压入栈,不是下一条语句的IP地址压入栈)
(2) 转移。
call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。
依据位移进行转移的call指令
call 标号(将当前的IP压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
(1)(sp) = (sp)-2
((ss)*16 +(sp)) = (IP)
(2)(IP) = (IP)+16位位移。
16位位移=“标号”处的地址-call指令后的第一个字节的地址;
16位位移的范围为-32768~32767,用补码表示;
16位位移由编译程序在编译时算出。
用汇编语法来解释此种格式的call指令,则:
CPU执行指令“call 标号”时,相当于进行:
push IP
jmp near ptr 标号
转移的目的地址在指令中的call指令
前面讲的call指令,其对应的机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。
指令“call far ptr 标号”实现的是段间转移。
CPU执行此格式的call指令时,进行如下的操作:
(1)(sp)=(sp)-2
((ss)*16+(sp)) = (CS)
(sp)=(sp)-2
((ss)*16+(sp)) = (IP)
(2)(CS)=标号所在段的段地址
(IP)=标号在段中的偏移地址
用汇编语法来解释此种格式的call指令,则:
CPU执行指令“call far ptr 标号”时,相当于进行:
push CS
push IP
jmp far ptr 标号
转移地址在寄存器中的call指令
指令格式:call 16位寄存器
功能:
(sp) = (sp)-2
((ss)*16+(sp)) = (IP)
(IP) = (16位寄存器)
用汇编语法来解释此种格式的call指令,CPU执行call 16位reg时,相当于进行:
push IP
jum 16位寄存器
转移地址在内存中的call指令
有两种格式:
1) call word ptr 内存单元地址
相当于:
push IP
jum word ptr 内存单元地址
2) call dword ptr 内存单元地址
相当于:
push CS
push IP
jmp dword ptr 内存单元地址
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 maint
--------------------------------------------------------------------------------
模块化程序设计
call与ret指令共同支持了汇编语言编程中的模块化设计。
在实际编程中,程序的模块化是必不可少的。
因为实现的问题比较复杂,对现实问题进行分析时,把它转化成为相互联系、不同层次的子问题,是必须的解决方法。
而call与ret指令对这种分析方法提供了程序实现上的支持。
利用call和ret指令,我们可以用简捷的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。
参数和结果传递的问题
子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。
其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。
;说明:计算N的3次方
;参数:(bx)=N
;结果:(dx:ax)=N^3
cube:mov ax,bx
mul bx
mul bx
ret
注意,编程时的良好风格,应有有详细的注释。包含对子程序的功能、参数和结果的说明。
用寄存器来存储参数和结果是最常使用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入结果寄存器。
批量数据的传递
寄存器的数量终究有限,我们不可能简单地用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题。在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可用同样的方法。除了用寄存器传递参数外,还有一种通用的方法是用栈来传递参数。
寄存器冲突的问题
一个一般化的问题,子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突。
那么我们如何来避免这种冲突呢?粗略地看,可以有两个方案:
1) 在编写调用子程序的程序时,注意看看子程序中有没有用到会产生冲突的寄存器,如果有,调用者使用别的寄存器;
2) 在编写子程序的时候,不要使用会产生冲突的寄存器。
以上两个方案,不具可行性,第一种给调用子程序的程序的编写造成很大麻烦。第二种不可能实现,子程序无法知道将来的调用情况。
我们希望:
1) 编写调用子程序的程序的时候不必关心子程序到底使用了哪些寄存器;
2) 编写子程序的时候不必关心调用者使用了哪些寄存器;
3) 不会发生寄存器冲突。
解决这个问题的简捷方法是,在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。我们可以用栈来保存寄存器中的内容。
以后,我们编写子程序的标准框架如下:
子程序开始:子程序中使用的寄存器入栈
子程序内容
子程序中使用的寄存器出栈
返回(ret、retf)
要注意寄存器入栈和出栈的顺序。
实验10 编写子程序
1、 显示字符串
问题:显示字符串是现实工作中经常要用到的功能,应该编写一个通用的子程序来实现这个功能。我们应该提供灵活的调用接口,使调用者可以决定显示的位置(行、列)、内容和颜色。
子程序描述
名称:show_str
功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串。
参数:(dh)=行号(取值范围0~24),(dl)=列号(取值范围0~79),
(cl)=颜色,ds:si指向字符串的首地址
返回:无
应用举例:在屏幕的8行3列,用绿色显示data段中的字符串。
1) 子程序的入口参数是屏幕上的行号和列号,注意在子程序内部要将它们转化为显存中的地址,首先要分析一下屏幕上的行列位置和显存地址的对应关系。
2) 注意保存子程序中用到的相关寄存器。
3) 空上子程序的内部处理和显存的结构密切相关,但是向外提供了与显存结构无关的接口。通过调用这个子程序,进行字符串的显示时可以不必了解显存的结构,为编程提供了方便。在实验中,注意体会这种设计思想。
2、 解决除法溢出的问题
问题:div指令可以做除法。当进行8位除法的时候,用al存储结果的商,ah存储结果的余数;进行16位除法的时候,用ax存储结果的商,dx存储结果的余数。可是,现在有一个问题,如果结果的商大于ah或ax所能存储的最大值,那么将如何?
当CPU执行div等除法指令的时候,如果发生结果数据超出了寄存器所能存储的范围,将引发CPU的一个内部错误,这个错误被称为:除法溢出。
子程序描述
名称:divdw
功能:进行不会产生溢出的除法运算,被除数为dword型,除数为word型,结果为dword型。
参数:(ax)=dword型数据的低16位
(dx)=dword型数据的高16位
(cx)=除数
返回:(dx)=结果的高16位,(ax)=结果的低16位
(cx)=余数
应用举例:计算1000000/10(F4240H/0AH)
3、 数值显示
问题:编程:将data段中的数据以十进制的形式显示出来。
数据在内存中都是二进制信息,标记了数值的大小。要把它们显示到屏幕上,成为我们能够读懂的信息,需要进行信息的转化。
比如,数值12666,在机器中存储为二进制信息:11000101111010B(317AH),计算机可以理解它。而我们要在显示器上读到可以理解的数值12666,我们看到的应该是一串字符:“12666”,由于显卡遵循的是ASCII编码,为了让我们能在显示器上看到这串字符,它在机器中应以ASCII码的形式存储为:31H、32H、36H、36H、36H(字符“0”~“9”对应的ASCII码为30H~39H)。
通过上面的分析可以看到,在概念世界中,有一个抽象的数据12666,它表示了一个数值的大小。在现实世界中它可以有多种表示形式,可以在电子机器中以高低电平(二进制)的形式存储,也可以在纸上、黑板上、屏幕上以人类的语言“12666”来书写。现在,我们面临的问题就是,要将同一抽象的数据,从一种表示形式转化为另一种表示形式。
要将数据用十进制形式显示到屏幕上,要进行两步工作:
1) 将用二进制信息存储的数据转变为十进制形式的字符串;
2) 显示十进制形式的字符串。
子程序描述
名称:dtoc
功能:将word型数据转变为表示十进制数的字符串,字符串以0为结尾符。
参数:(ax)=word型数据
ds:si指向字符串的首地址
返回:无
应用举例:编程,将数据12666以十进制的形式在屏幕的8行3列,用绿色显示出来。
分析:要得到字符串“12666”,就是要得到一列表示该字符串的ASCII码:31H、32H、36H、36H、36H。
十进制数码字符对应的ASCII码=十进制数码值+30H。
要得到表示十进制数的字符串,先求十进制数每位的值。
例如,对于12666,先求得每位的值:1、2、6、6、6。再将这些数分别加上30H,便得到了表示12666的ASCII码串,31H、32H、36H、36H、36H。
那么,怎样得到每位的值呢?采用下列方法(除10取余法):
12666/10=1266……6
1266/10=126……..6
126/10=12………6
12/10=1………..2
1/10=0………..1
可见,用10除12666,共除5次,记下每次的余数,就得到了每位的值。
综合以上分析,可得出处理过程如下:
用12666除以10,循环5次,记下每次的余数;将每次的余数分别加30H,使得到了表示十进制数的ASCII码串。
只要是除到商为0,各位的值就已经全部求出。可以使用jcxz指令来实现相关的功能。