6. CALL和RET指令

87 阅读8分钟

CALL和RET指令

1. ret和retf

ret和retf都是转移指令,它们都修改IP或同时修改CS和IP。它们经常被共同用来实现子程序的设计。

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

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

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

 pop IP

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

 pop IP
 pop CS

2. call指令

CPU执行call指令时,进行两步操作:

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

2)转移。

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

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

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

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

1)(sp) = (sp) - 2

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

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

如果我们用汇编语言来解释此种格式的call指令,则相当于:

 push IP
 jmp near ptr 标号

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

 call far ptr 标号

实现的是段间转移。CPU执行此种格式的call指令时,进行如下的操作。

1)(sp) = (sp) - 2

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

(sp) = (sp) - 2

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

用汇编语法解释这种格式的call指令,则:

 push CS
 push IP
 jmp far ptr 标号

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

指令格式: call 16位寄存器

用汇编语法来解释就是:

 push IP
 jmp 16位寄存器

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

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

 call word ptr 内存单元地址

功能相当于:

push IP
jmp word ptr 内存单元地址

另外一种格式为:

call dword ptr 内存单元地址

功能相当于:

push CS
push IP
call dword ptr 内存单元地址

2.5 mul指令

mul是乘法指令。有以下两点需要注意:

  • 两个相乘的数要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果是16位,一个默认在AX中,另一个放在16位reg或内存字单元中
  • 8位乘法,结果默认放在AX中;如果是16位乘法,结果高位默认在DX中存放,低位在AX中存放

3. 模块化程序设计

从上面我们可以看到,call和ret指令共同支持了汇编语言编程中的模块化设计。在实际编程中,程序的模块化是必不可少的。利用call和ret指令,我们可以用简捷的方法,实现多个相互联系、功能独立的子程序来解决一个复杂的问题。

3.1 参数和结果传递的问题

子程序一般都要根据传递的参数处理一定的事务,处理后,将结果提供给调用者。其实我们讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。

比如,设计一个子程序,可以根据提供的N,来计算N的三次方。这里就有两个问题:

  1. 将参数N存储在什么地方?
  2. 计算得到的数值,存储在什么地方?

很显然,可以用寄存器来存储,将参数放到bx中;因为子程序中将要计算N x N x N,可以使用多个mul指令,为了方便,可将结果放到dx和ax中。子程序如下:

;说明:计算N的3次方
;参数:(bx) = N
;结果:(dx:ax) = N^3
cube: mov ax, bx
	  mul bx
	  mul bx
	  ret

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

assume cs:code, ds:data
  data segment
    dw 1, 2, 3, 4, 5, 6, 7, 8
    dd 8 dup (0)
  data ends
  
  code segment
    start: mov ax, data
           mov ds, ax
           mov si, 0        ;ds:si指向第一组word单元
           mov di, 16       ;ds:di指向第二组dwrod单元
           
           mov cx, 8
        s: mov bx, [si]
           call cube
           mov [di], ax
           mov [di+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

3.2 批量数据的传递

上面的程序只有一个参数,返回结果最多也就两个寄存器。那如果需要批量传递多个参数该怎么办呢?对于返回值,也有同样的问题。

在这种时候,我们将批量数据放到内存中,然后将它们所在内存空间的首地址放在寄存器中,传递给需要的子程序。对于具有批量数据的返回结果,也可以用同样的方法

下面看一个例子,设计一个子程序,功能:将一个全是字母的字符串转化为大写

这个子程序需要知道字符串的首地址和字符串的长度。因为子程序中是需要循环处理的,而循环的次数恰恰就是字符串的长度。出于方便考虑,可以将字符串的长度放到cx中。

assume cs:code

data segment
  db 'conversation'
data ends

code segment
  start: mov ax, data
         mov ds, ax
         mov si, 0         ;ds:si指向字符串所在空间的首地址
         mov cx, 12        ;cx存放字符串的长度
         call capital
         mov ax, 4c00H
         int 21H
         
capital: and byte ptr [si] 11011111B
		 inc si
		 loop capital
		 ret
code ends

3.3 寄存器冲突的问题

设计一个子程序,功能:将一个全是字母,以0结尾的字符串,转化为大写

字符串定义如下:

db 'conversation',0

子程序可以依次读取每个字符进行检测,如果不是0,就进行大写的转化;如果是0,就结束处理。所以程序不需要字符串的长度作为参数,可以用jcxz来检测0。

;说明:将一个全是字母,以0结尾的字符串,转化为大写
;参数:ds:si指向字符串的首地址
;结果:没有返回值
capital: mov cl, [si]
         mov ch, 0
         jcxz ok                            ;如果(cx) = 0,结束;如果不是0,处理
         and byte ptr [si] 11011111B        ;将ds:si所指单元中的字母转化为大写
         inc si                             ;ds:si指向下一个单元
         jmp short capital
     ok: ret

如果要处理的字符串是如下呢?

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

可以看到,所有字符串的长度都是5,使用循环,重复调用子程序capital,完成对4个字符串的处理。完整程序如下:

code segment

  start: mov ax, data
         mov ds, ax
         mov bx, 0
         
         mov cx, 4
      s: mov si, bx
         call capital
         add bx, 5
         loop s
         
         mov ax, 4c00H
         int 21H
         
capital: mov cl, [si]
         mov ch, 0
         jcxz ok
         and byte ptr [si], 11011111B
         inc si
         jmp short capital
     ok: ret

code ends

这个程序在思想上完全正确,但在细节上有些错误。

问题在于cx的使用,主程序要使用cx记录循环次数,可是子程序中也使用了cx,在执行子程序的时候,cx中保存的循环计数值被改变,使得主程序的循环出错。从这个问题中,实际引出了一个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突

那么,如何避免这种冲突呢?粗略地看,有以下两个方案。

  1. 在编写调用子程序的主程序时,注意看看子程序中有没有用到会产生冲突的寄存器,如果有,调用者使用别的寄存器。
  2. 在编写子程序的时候,不使用会产生冲突的寄存器。

我们来分析一下上面两个方案的可行性:

  1. 这将给调用子程序的程序编写造成很大的麻烦,因为必须要小心检查所调用的子程序中是否有将产生冲突的寄存器。
  2. 这个方案使不可能实现的,因为编写子程序的时候无法知道将来的调用情况。

可见,我们上面所设想的两个方案都不可行。我们希望:

  • 编写调用子程序的程序的时候不必关心子程序到底使用了哪些寄存器
  • 编写子程序的时候不必关心调用者使用了哪些寄存器
  • 不会发生寄存器冲突

解决这个问题的简捷方法是:在子程序的开始将子程序中所有用到的寄存器中的内容都保存起来,在子程序返回前再恢复。可以用栈来保存寄存器中的内容

以后,我们编写的子程序的标准框架如下:

子程序开始: 子程序中使用的寄存器入栈
           子程序内容
           子程序中使用的寄存器出栈
           返回(ret、retf)

我们改进一下子程序capital的设计:

capital: push cx
         push si
         
 change: mov cl, [si]
         mov ch, 0
         jcxz ok
         and byte ptr [si] 11011111B
         inc si
         jmp short change
         
     ok: pop si
         pop cx
         

4. 实验

4.1 解决除法溢出的问题

assume cs:code, ss:stack

stack segment
  db 16 dup(0)
stack ends

code segment
  start: mov ax, stack
         mov ss, ax
         mov sp, 10H
         
         mov ax, 4240H
         mov dx, 000FH
         mov cx, 0AH
         call divdw
         mov ax, 4c00H
         int 21H
         
         
  divdw: push si     ;保存现场
  
         push ax
         mov ax, dx 
         mov dx, 0
         div cx
         mov si, ax  ;暂存H/N的商
         pop ax
         div cx
         mov cx, dx  ;保存余数
         mov dx, si
         
         pop si      ;h
         ret
         
         
         
code ends

end start