ret 和 call 指令都是转移指令,它们都修改 IP,或同时修改 CS 和 IP,经常用它们来共同完成子程序的设计

ret 和 retf 指令

ret 指令用栈中的内容来修改 IP,实现近转移。

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

CPU 执行 ret 指令时相当于

  • pop ip

CPU 执行 retf 指令时相当于

  • pop ip
  • pop cs

下面的程序 ret 指令执行后,CS:IP 执行代码段的第一条指令

assume cs:codesg,ss:stack
stack segment
db 16 dup (0)
stack ends
codesg segment
mov ax,4c00h
int 21h
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push ax
mov bx,0
ret
codesg ends
end start

在 debug 中可以清楚的看到当执行完 ret 指令后,CS:IP 指向了程序的第一条指令。

1
1

下面的程序 retf 指令执行后,CS:IP 执行代码段的第一条指令

assume cs:codesg,ss:stack
stack segment
db 16 dup (0)
stack ends
codesg segment
mov ax,4c00h
int 21h
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,0
push cs
push ax
mov bx,0
retf
codesg ends
end start

在 debug 中可以清楚的看到当执行完 retf 指令后,CS:IP 指向了程序的第一条指令。

2
2

call 指令

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

  • 将当前的 IP 或 CS 和 IP 压入栈中
  • 转移

call 指令不能实现短转移

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

call 标号(将当前的 IP 压入栈后再跳到标号处执行)

执行 “call 标号” 相当于一下两步操作

  • push IP
  • jmp near ptr 标号

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

指令 “call far ptr 标号” 实现的段间转移,CPU 在执行时相当于以下 3 步操作:

  • push CS
  • push IP
  • jmp far ptr 标号

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

指令 “call 16位寄存器” 被 CPU 执行时,相当于以下 2 步操作:

  • push IP
  • jmp 16位寄存器

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

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

  1. call word ptr 内存单元地址

CPU 在执行上述指令的时候相当于以下 2 步:

  • push IP
  • jmp word ptr 内存单元地址

比如下面的代码:

assume cs:codesg
codesg segment
mov sp,10H
mov ax,0123H
mov ds:[0],ax
call word ptr ds:[0]

mov ax,4c00h
int 21h
codesg ends
end

在执行完 “call word ptr 内存单元” 后相当于执行了 “push ip” 和 “jmp word ptr 内存单元地址” 两条指令,push 后栈顶要向上移动 2 个单元,即 sp 中的值减 2(10H - 2 = 0EH)。“jmp word ptr 内存单元地址” 相当于用内存单元中的值去修改 IP,执行后 IP 为 0123H(“jmp word ptr 内存单元地址” 忘了的话返回前面的文章复习一下)。在 debug 中可以清楚的看到结果。

3
3
  1. call dword ptr 内存单元地址

CPU 在执行上述指令的时候相当于以下 2 步操作:

  • push cs
  • push ip
  • jmp dword ptr 内存单元地址

例如下面的代码

assume cs:codesg
codesg segment
mov sp,10H
mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
call dword ptr ds:[0]

mov ax,4c00H
int 21H
codesg ends
end

执行 “call dword ptr ds:[0]” 后,cs 的值为 0 ,ip 为 0123H,sp 为 0ch,在 debug 中显示如下:

4
4

call 和 ret 的配合使用

我们知道使用 call 指令的时候会先进行入栈操作,然后再跳到标号处去执行程序,ret 是用栈中的数据去修改 IP 或 CS 和 IP 来达到控制 CPU 执行指令的目的,这样就可以用 call 和 ret 指令来实现调用子程序的目的。例如下面的程序是调用子程序来实现 2 的 3 次方并将结果存放到 bx 中:

assume cs:codesg
codesg segment
mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21H
s: add ax,ax
loop s
ret
codesg ends
end

执行完后,我们看到 bx 中内容为 8 ,是我们希望得到的结果,在 debug 中显示如下:

5
5

一些思考

上面的程序中我们没有向子程序中传递参数,那怎么实现向子程序传递参数呢?我们可以用一个寄存器来传递,即调用者将要传递的参数放到某个合法的寄存器中,子程序执行的时候来修改这个寄存器,这样就实现了传递参数并返回的功能。那么要传递一堆参数给子程序该怎么办呢?其实可以把这堆参数放到内存里,寄存器只保留这个内存的首部地址即可。要是调用者和子程序都使用了相同的寄存器,这岂不是混乱了?一般的做法是在调用子程序的时候把寄存器的值都入栈,等到子程序返回时再把出栈来恢复主程序寄存器的值。