汇编语⾔⼦程序调⽤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压⼊栈中;
(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
mul指令
mul是乘法指令。
使⽤mul做乘法的时候:
1)两个相乘的数:两个相乘的数,要么都是8位,要么都是16位。
如果是8位,⼀个默认放在AL中,别⼀个放在8位寄存器或内存单元中;
如果是16位,⼀个默认在AX中,另⼀个放在16位寄存器或内存单元中。
2)结果:如果是8位乘法,结果默认放在AX中;如果是16位乘法,结果⾼位默认在DX中存放,低位在AX中存放。
格式如下:
mul reg
mul 内存单元
模块化程序设计
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)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论