汇编语言个人学习笔记——第十章 call和ret指令

10.1ret和retf

ret指令用栈中的数据,修改IP的内容,从而实现近转移。

CPU执行ret指令时,进行下面两步操作:

(1)(IP)=((ss)*16+(sp))

(2)(sp)=(sp)+2

retf指令用栈中的数据,修改CS和IP的内容,从而实现远转移。

CPU执行ret指令时,相当于进行

pop IP

CPU执行retf指令时,进行下面4步操作:

(1)(IP)=((ss)*16+(sp))

(2)(sp)=(sp)+2

(3)(CS)=((ss)*16+(sp))

(4)(sp)=(sp)+2

CPU执行retf指令时,相当于进行

pop CS

 

10.2call指令

call指令经常跟ret指令配合使用,因此CPU执行call指令,进行两步操作:

(1)将当前的IP或CS和IP压入栈中;

(2)转移(jmp)。

call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同。

 

10.3依据位移进行转移的call指令

call 标号(将当前的IP压栈后,转到标号处执行指令)

CPU执行此种格式的call指令时,进行如下的操作。

(1)(sp)=(sp)-2

         ((ss)*16+(sp))=(IP)

(2)(IP)+(IP)+16位位移

call 标号

16位位移="标号"chu处的地址-call指令后的第一个字节的地址;

16位位移的范围为-32768~32767,用补码表示。

CPU执行"call 标号"时,相当于进行:

push IP

jmp short ptr 标号

 

10.4 转移的目的地址在指令中的call指令

前面学习的call指令,其对应的机器指令中并没有转移的目的地址,而是相对于当前IP的转移位移。

指令"call far ptr 标号"实现的是段间转移。

CPU执行“call far ptr 标号”这种格式的call指令时的操作:
(1)(sp)=(sp)-2

         ((ss)x16+(sp))=(CS)

         (sp)=(sp)-2

         ((ss)x16+(sp))=(IP)

(2)(CS)=标号所在的段地址

       (IP)=标号所在的偏移地址

从上面的描述中可以看出,如果我们用汇编语法来解释此种格式的call指令,则:

CPU执行指令"call far ptr 标号"时,相当于进行:

push CS

push IP

jmp far ptr 标号

 

10.5转移地址在寄存器中的call指令

指令格式:call 16位寄存器

功能:

(sp)=(sp)-2

((ss)*16+(sp))=(IP)

(IP)=(16位寄存器)

汇编语法解释此种格式的call指令,CPU执行call 16位reg时,相当于进行:

push IP

jmp 16位寄存器

 

10.6转移地址在内存中的call指令

转移地址在内存中的call指令有两种格式:

(1)call word ptr 内存单元地址

(2)call dword ptr 内存单元地址

(1)call word ptr 内存单元地址

CPU执行”call word ptr 内存单元地址“时,相当于进行:

push IP

jmp word ptr 内存单元地址

比如下面的指令:

mov sp,10h

mov ax,0123h

mov ds:[0],ax

call word ptr ds:[0]

执行后,(IP)=0123H,(sp)=0EH

(2)call dword ptr 内存单元地址

汇编语法解释:

push CS

push IP

jmp dword ptr 内存单元地址

比如下面的指令:

mov sp,10h

mov ax,0123h

mov ds:[0],ax

mov word ptr ds:[2],0

call dword ptr ds:[0]

执行后,IP=0123H,CS=0,(sp)=0CH

 

10.7call和ret的配合使用

前面,我们已经分别学习了ret和call指令的原理。现在我们看一下,如何将它们配合使用来实现子程序的机制。

问题10.1

下面程序返回前,bx中的值是多少?

assume cs:code

code segment
start :mov ax,1
       mov cx,3
       call s
       mov bx,ax       ;(bx)=?
       mov ax,4c00h
       int 21h
     s:add ax,ax
       loop s
       ret
code ends
end start

问题10.1分析

我们来看一下CPU执行这个程序的主要过程:

(1)CPU将call s指令的机器码读入,IP指向了call s后的指令mov bx,ax,然后CPU执行call s指令,将当前的IP值(指令mov bx,ax的偏移地址)压栈,并将IP的值改变为标号s处的偏移地址;

(2)CPU从标号s处kai'开始执行指令,loop循环完毕,(ax0=8;

(3)CPU将ret指令的机器码读入,IP指向了ret指令后的内存单元,然后CPU执行ret指令,从栈中弹出一个值(即call先前压入的mov bx,ax指令的偏移地址)送入IP中。则CS:IP指向指令mov bx,ax;

(4)CPU从mov bx,ax开始执行指令,直至完成。

程序返回前,(bx)=8。我们可以看出,从标号s到ret的程序段的作用是计算2的N次方,计算前,N的值由CX提供。

再看下面的程序:

看一下程序的主要执行过程。

(1)前3条指令执行后,栈的情况如下:

(2)call指令读入后,(IP)=000EH,CPU指令缓冲器中的代码为:E8 05 00;

CPU执行E8 05 00,首先栈中的情况变为:

然后,(IP)=(IP)+0005=0013H。

(3)CPU从cs:0013H处(即标号s处)开始执行。

(4)ret指令读入后;

(IP)=0016H,CPU指令缓冲器中的代码为:C3

CPU执行C3,相当于进行pop IP,执行后,栈中的情况为:

(5)CPU回到cs:000EH处(即call指令后面的指令处)继续执行。

从上面的分析中我们发现,可以写一个具有一定功能的程序段,我们称其为子程序,在需要的时候,用call指令转去执行。

执行完子程序后,我们可以用ret指令让CPU接着call指令向下执行。

call指令后面的指令的地址将存储在栈中,所以可以在子程序的后面使用ret指令,用栈中的数据设置IP的值,从而转到call指令后面的代码处继续执行。

这样我们可以利用call和ret来实现子程序的机制。

子程序的框架如下:

标号:

指令

ret

具有子程序的源程序框架如下:

 

10.8mul指令

mul是乘法指令,使用mul做乘法的时候,注意下面两点:

(1)相乘的两个数要么都是8位,要么都是16位。

8位:一个默认放在AL中,另一个放在8位寄存器或内存字节单元中;

16位:一个默认放在AX中,另一个放在16位寄存器或内存字单元中。

(2)结果:

8位:结果默认放在AX中

16位:结果高位默认在DX中存放,低位在AX中放。

格式如下:

mul 寄存器

mul 内存单元

内存单元可以用不同的寻址方式给出,比如:

mul byte ptr ds:[0]

含义为:(ax)=(al)*((ds)*16+0);

mul word ptr [bx+si+8]

含义为:

(ax)=(al)*((ds)*16+(bx)+(si)+8)结果的低16位;

(dx)=(al)*((ds)*16+(bx)+(si)+8)结果的高16位;

例如:

(1)计算100*10

100和10小于255,可以做8位乘法,程序如下:

mov al,100

mov bl,10

mul bl

结果:(ax)=1000(03E8H)

又例如:

(2)计算100*10000

100小于255,可10000大于255,所以必须做16位乘法,程序如下

mov ax,100

mov bx,10000

mul bx

结果:(ax)=4240H,(dx)=000FH            (F4240H=1000000)

 

10.9模块化程序设计

从上面我们看到,call与ret指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。因为现实的问题比较复杂,对现实问题进行分析时,把它转化成为互相联系、不同层次的子问题,是必须的解决方法。

而call和ret指令对这种分析方法提供了程序实现上的支持。利用call和ret指令,我们可以用简洁的方法,实现多个互相联系、功能独立的子程序来解决一个复杂的问题。

 

10.10参数和结果传递的问题

子程序一般都要根据提供的参数处理一定的事务,处理后,将结果(返回值)提供给调用者。

其实,我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。

思考:

设计一个子程序,可以根据提供的N,来计算N的3次方

这里有两个问题:

(1)我们将参数N存储在什么地方?

(2)计算得到的数值,我们存储在什么地方?

很显然,我们可以用寄存器来存储,可以将参数放到bx中;

因为子程序中要计算N*N*N,可以使用多个mul指令,为了方便,可将结果放到dx和ax中。

子程序:

说明:计算N的三次方

参数:(bx)=N

结果:(dx:ax)=N^3

cube:mov ax,bx

         mul bx

         mul bx

         ret

编程的时候要注意格式,对于程序应有详细的注释。子程序的注释信息应该包含对子程序的功能、参数和结果的说明。

用寄存器存储参数和结果是最常用的方法。对于存放参数的寄存器和存放结果的寄存器,调用者和子程序的读写操作恰恰相反:

调用者将参数送入参数寄存器,从结果寄存器中取到返回值;

子程序从参数寄存器中取到参数,将返回值送入结果寄存器。

编程:计算data段中第一组数据的3次方,结果保存在后面一组dword单元中。

data segment

 dw 1,2,3,4,5,6,7,8

 dd 0,0,0,0,0,0,0,0

data ends

代码如下:

assume cs:code

data segment
 dw 1,2,3,4,5,6,7,8
 dd 0,0,0,0,0,0,0,0
data ends

code segment

start :mov ax,data
       mov ds,ax
       mov si,0
       mov di,16
       
       mov cx,8
     s:mov bx,[si]
       call cube
       mov [di],ax
       mov [d].2,dx
       add si,2
       add di,4
       loop s
       
       mov ax,4c00h
       int 21h
       
  cube:mov ax,bx
       mul bx
       mul bx
       ret

code ends
end start

 

10.11批量数据的传递

     前面的例程中,子程序cube只有一个参数,放在bx中。如果有两个参数,那么可以用两个寄存器来放,可是如果需要传递的数据有3个、4个或更多直至N个,我们如何存放?

     寄存器的数量终究有限,我们不可能简单地用寄存器来存放多个需要传递的数据。对于返回值,也有同样的问题。

     在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器追踪,传递给需要的子程序。

     对于具有批量数据的返回结果,也可用同样的方法。

编程:将data段中的字符串转化为大写。

assume cs:code

data segment

 db 'coversation'

data ends

code segment

 start:……

code ends

end start

代码如下:

assume cs:code

data segment
 db 'conversation'
data ends

code segment

start : mov ax,data
        mov ds,ax
        mov si,0
        
        mov cx,12
        call capital
        
        mov ax,4c00h
        int 21h
        
capital:and byte ptr [si],11011111b
        inc si
        loop capital
        ret
        
code ends
end start

注意:除了寄存器、内存传递参数外,还有一种通用的方法使用栈来传递参数。关于这种技巧以后再单独提。

 

10.12寄存器冲突的问题

设计一个子程序:

功能:将一个全是字母,以0结尾的字符串,转化为大写。

程序要处理的字符串以0作为结尾符,这个字符串可以如下定义:

db 'conversation',0

分析:

应用这个子程序,字符串的内容后面定要有一个0,标记字符串的结束。子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化,如果是0,就结束处理。

由于可通过检测0而知道是否已经处理完整个字符串,所以子程序可以不需要字符串的长度作为参数。我们可以直接用jcxz来检测0。

 子程序代码:

capital:mov cl,[si]
        mov ch,0
        jcxz ok
        and byte ptr [si],11011111b
        inc si
        jmp short capital
     ok:ret

分析:如果(cx)=0,结束;如果不是0,将ds:si所指单元中的字母转化为大写ds:si指向下一个单元。

子程序的应用

将data段中字符串全部转化为大写

assume cs:code

 data segment

   db 'word',0

   db 'unix',0

   db 'wind',0

   db 'good',0

data ends

代码:

assume cs:code,ds:data

data segment
 db 'word',0
 db 'unix',0
 db 'wind',0
 db 'good',0
data ends

code segment
start :mov ax,data
       mov ds,ax
       mov si,0
       
       mov cx,4
    s1:push cx
       call s
       inc si
       pop cx
       loop s1
       
       mov ax,4c00h
       int 21h
     
     s:mov cl,ds:[si]
       mov ch,0
       jcxz ok
       and byte ptr ds:[si],11011111b
       inc si
       jmp short s
    ok:ret
code ends
end start

 


版权声明:本文为K17010原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。