callret指令都是转移指令,他们都修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序的设计,这一章讲解callret指令的原理。

一、ret和retf

1.1 相关知识

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

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

  1. § 第10章 CALL和RET指令 - 图1%3D((SS)*16%2B(SP))%7D#card=math&code=%5Ctext%7B%28IP%29%3D%28%28SS%29%2A16%2B%28SP%29%29%7D&id=IbXJH)
  2. § 第10章 CALL和RET指令 - 图2%3D(SP)%2B2%7D#card=math&code=%5Ctext%7B%28SP%29%3D%28SP%29%2B2%7D&id=O3MdC)

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

  1. § 第10章 CALL和RET指令 - 图3%3D((ss)*16%2B(sp))%7D#card=math&code=%5Ctext%7B%28IP%29%3D%28%28ss%29%2A16%2B%28sp%29%29%7D&id=AJO9O)
  2. § 第10章 CALL和RET指令 - 图4%3D(sp)%2B2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29%2B2%7D&id=TUAvp)
  3. § 第10章 CALL和RET指令 - 图5%3D((ss)*16%2B(sp))%7D#card=math&code=%5Ctext%7B%28CS%29%3D%28%28ss%29%2A16%2B%28sp%29%29%7D&id=ZWvRA)
  4. § 第10章 CALL和RET指令 - 图6%3D(sp)%2B2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29%2B2%7D&id=yakdv)

可以看出,如果我们用汇编语法来解释retretf指令,则:

  • CPU执行ret指令时,相当于进行:pop IP
  • CPU执行retf指令时,相当于进行:
    1. pop IP
    2. pop CS

下面程序中,ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令

  1. assume cs:code
  2. stack segment
  3. db 16 dup (0)
  4. stack ends
  5. code segment
  6. mov ax,4c00h
  7. int 21h
  8. start:mov ax,stack ; ax=076A
  9. mov ss,ax ; ss=076A
  10. mov sp,16 ; sp=16
  11. mov ax,0 ; ax=0
  12. push ax ; 076a:e ~ 076a:f处的值为0(使用栈来管理)
  13. mov bx,0 ; bx=0
  14. ret ; 076A:E 对应的数据传向IP
  15. code ends
  16. code start

执行完mov bx,0这句话后内存情况

§ 第10章 CALL和RET指令 - 图7

之后再执行RET,就将对应的SS:SP的内容给了IP,即IP=0000

§ 第10章 CALL和RET指令 - 图8

之后程序就可以跳转到最一开始执行

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

  1. assume cs:code
  2. stack segment
  3. db 16 dup (0)
  4. stack ends
  5. code segment
  6. mov ax,4c00h
  7. int 21h
  8. start:mov ax,stack
  9. mov ss,ax
  10. mov sp,16
  11. mov ax,0
  12. push cs
  13. push ax
  14. mov bx,0
  15. retf
  16. code ends
  17. end start

1.2 检测点10.1

补全程序,实现从内存1000:0000处开始执行指令

  1. assume cs:code
  2. stack segment
  3. db 16 dup (0)
  4. stack ends
  5. code segment
  6. start:mov ax,stack ; ax=076A
  7. mov ss,ax ; ss=076A
  8. mov sp,16 ; sp=16
  9. mov ax,1000H ; ax=1000H
  10. push ax ; 076A:e ~ 076A:f处的值设为1000
  11. mov ax,0000H
  12. push ax ; 076A:c ~ 076A:d处的值为0000
  13. retf
  14. code ends
  15. end start

二、call指令

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

  1. 将当前的IPCS和IP压入栈中
  2. 转移

call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同,下面通过给出转移目的地址的不同方法为主线,讲解call指令的主要应用格式。

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

2.1.1 相关知识

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

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

  1. § 第10章 CALL和RET指令 - 图9%3D(SP)-2%7D#card=math&code=%5Ctext%7B%28SP%29%3D%28SP%29-2%7D&id=exMRP)
    § 第10章 CALL和RET指令 - 图10%3D((ss)*16%2B(sp))%7D#card=math&code=%5Ctext%7B%28IP%29%3D%28%28ss%29%2A16%2B%28sp%29%29%7D&id=PUBQn)
  2. § 第10章 CALL和RET指令 - 图11%3D(IP)%2B16%E4%BD%8D%E4%BD%8D%E7%A7%BB%7D#card=math&code=%5Ctext%7B%28IP%29%3D%28IP%29%2B16%E4%BD%8D%E4%BD%8D%E7%A7%BB%7D&id=Q3U8Z)

其中,

  • 16位位移 = 标号处的地址 - call指令后的第一个字节的地址
  • 16位位移的范围为-32768~32767,用补码表示
  • 16位位移由编译程序在编译时算出

从上面的描述,call指令,其实就相当于pushjmp指令的结合

  1. push IP
  2. jmp near ptr 标号

2.1.2 检测点10.2

下面的程序执行后,ax中的数值为多少?

内存地址 机器码 汇编指令
1000: 0 b8 00 00 mov ax,0
1000: 3 e8 01 00 call s
1000: 6 40 inc ax
1000: 7 58 s : pop ax

将该程序拿出调试

  1. assume cs:code
  2. code segment
  3. start:mov ax,0
  4. call s
  5. inc ax
  6. s:pop ax
  7. code ends
  8. end start

调试如下所示:

image.png

调试完,结果为AX=0006

下面的程序执行后,ax中的数值是 6,要搞清楚如果是跳转指令,什么时候修改IP的值。
学过计算机组成原理的肯定知道,指令读取后,IP值自动加1(这儿的1是指下一条指令,不是指下一个字节),指向下一条指令,如果经CPU分析后是跳转指令,则再修改IP的值。这两者原理是一样

  1. 10000 mov ax,0 ;读取此条指令后IP=3 ,执行完该指令后IP=3
  2. 10003 call s ;读取此条指令后IP=6 ,所以IP=6入栈,执行完该指令后IP=7,跳转到s
  3. 10006 inc ax
  4. 10007 s:pop ax ;所以POP后,ax=6

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

2.2.1 相关知识

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

  1. call far ptr 标号 ; 实现的是段间转移

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

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

  1. § 第10章 CALL和RET指令 - 图13%3D(sp)-2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29-2%7D&id=v4vsV)
    § 第10章 CALL和RET指令 - 图14*16%2B(sp))%3D(IP)%7D#card=math&code=%5Ctext%7B%28%28ss%29%2A16%2B%28sp%29%29%3D%28IP%29%7D&id=kVSYF)
    § 第10章 CALL和RET指令 - 图15%3D(sp)-2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29-2%7D&id=d3X0j)
  2. § 第10章 CALL和RET指令 - 图16%3D%E6%A0%87%E5%8F%B7%E6%89%80%E5%9C%A8%E6%AE%B5%E7%9A%84%E6%AE%B5%E5%9C%B0%E5%9D%80%7D#card=math&code=%5Ctext%7B%28CS%29%3D%E6%A0%87%E5%8F%B7%E6%89%80%E5%9C%A8%E6%AE%B5%E7%9A%84%E6%AE%B5%E5%9C%B0%E5%9D%80%7D&id=qfRmC)
    § 第10章 CALL和RET指令 - 图17%3D%E6%A0%87%E5%8F%B7%E6%89%80%E5%9C%A8%E6%AE%B5%E7%9A%84%E5%81%8F%E7%A7%BB%E5%9C%B0%E5%9D%80%7D#card=math&code=%5Ctext%7B%28IP%29%3D%E6%A0%87%E5%8F%B7%E6%89%80%E5%9C%A8%E6%AE%B5%E7%9A%84%E5%81%8F%E7%A7%BB%E5%9C%B0%E5%9D%80%7D&id=C8imb)

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

  1. ; 执行call far ptr 标号时
  2. ; 相当于进行
  3. push CS
  4. push IP
  5. jmp far ptr 标号

2.2.2 检测点10.3

下面的程序执行后,ax中的数值为多少?

内存地址 机器码 汇编指令
1000: 0 b8 00 00 mov ax,0
1000: 3 9A 09 00 00 10 call far ptr s
1000: 8 40 inc ax
1000: 9 58 s : pop ax
add ax,ax
pop bx
add ax,bx

将上述代码进行调试

§ 第10章 CALL和RET指令 - 图18

之后开始进行单步调试,当执行完call far ptr s这句话后,查看栈中(SS和SP)被压入的数据,从而得到压入的数据为 08 00 6A 07 (这里有问题了§ 第10章 CALL和RET指令 - 图19)按理来说,插入的数据为09 00 才对,为什么会成为08 00

§ 第10章 CALL和RET指令 - 图20

之后的问题就很简单了,只需要将栈中的数据进行弹出即可,最后的答案如下

§ 第10章 CALL和RET指令 - 图21

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

2.3.1 相关知识

指令格式:call 16位reg

功能:

  • § 第10章 CALL和RET指令 - 图22%3D(sp)-2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29-2%7D&id=SOZuX)
  • § 第10章 CALL和RET指令 - 图23*16%2B(sp))%3D(IP)%7D#card=math&code=%5Ctext%7B%28%28ss%29%2A16%2B%28sp%29%29%3D%28IP%29%7D&id=wEZcF)
  • § 第10章 CALL和RET指令 - 图24%3D(16%E4%BD%8Dreg)%7D#card=math&code=%5Ctext%7B%28IP%29%3D%2816%E4%BD%8Dreg%29%7D&id=nJsMn)

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

  1. push IP
  2. jmp 16reg

2.3.2 检测点10.4

下面的程序执行后,ax中的数值为多少

内存地址 机器码 汇编指令
1000: 0 b8 06 00 mov ax,6
1000: 3 ff d0 call ax
1000: 5 40 inc ax
1000: 6 mov bp,sp
add ax,[bp]

现在充能§ 第10章 CALL和RET指令 - 图25时间到了

§ 第10章 CALL和RET指令 - 图26

最后得到的结果如下所示,这个结果是由于call ax指令将IP地址压入到栈中,而add ax,[bp]就是用的栈中的数据

§ 第10章 CALL和RET指令 - 图27

  1. bp寄存器,跟其它什么BXAX一样的用法,
  2. SP是用在栈上的,配合SS使用,像SS:SP
  3. SS上放段地址,SP上放偏移地址。
  4. 寻址时,像[bp],相当于SS:[bp]
  5. 只要在[...]中使用寄存器bp,而指令中没有显示给出段地址,段地址就默认在ss
  6. BX默认使用CS

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

2.4.1 相关知识

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

格式1:

  1. call word ptr 内存单元地址

用汇编语法来解释此种格式的call指令,则相当于进行

  1. push IP
  2. jmp word ptr 内存单元地址

比如下面的指令

  1. mov sp,10h
  2. mov ax,0123h
  3. mov ds:[0],ax
  4. call word ptr ds:[0] ; ds:[0]处的数据给IP
  5. ; 因为执行了一个push IP的动作,而且这个是一个字节,2个字,所以SP = 10H-2=0EH

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

格式2:

  1. call dword ptr 内存单元地址

用汇编语法来解释此种格式的call指令,则相当于进行

  1. push CS
  2. push IP
  3. jmp dword ptr 内存单元地址

比如下面的指令:

  1. mov sp,10h
  2. mov ax,0123h
  3. mov ds:[0],ax ; 075A:0000处插入数据0123
  4. mov word ptr ds:[2],0 ; 075A:0004插入数据0000
  5. call dword ptr ds:[0] ; sp = 10H-4 = 0cH

§ 第10章 CALL和RET指令 - 图28

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

2.4.2 检测点10.5

(1)下面的程序执行后,ax中的数值为多少?(注意:用call指令的原理分析,不要在Debug中单步跟踪来验证你的结论。对于此程序,在Debug中单步跟踪的结果,不能代表CPU的实际执行结果。)

  1. assume cs:code
  2. stack segment ; 076a:0处开始开辟8个字节,即16个字大小的空间
  3. dw 8 dup (0)
  4. stack ends
  5. code segment
  6. start:mov ax,stack ; ax=076a
  7. mov ss,ax ; ss=076a
  8. mov sp,16 ; sp = 16h
  9. mov ds,ax ; ds=076a
  10. mov ax,0 ; ax=0
  11. call word ptr ds:[0EH] ;076a:0Eh处的数据赋值给IP
  12. inc ax
  13. inc ax
  14. inc ax
  15. mov ax,4c00h
  16. int 21h
  17. code ends
  18. end start

这道题不太会,因为call word ptr ds:[0EH]这句话得到的IP=7302,对这句话的由来解释不清楚

(2)下面的程序执行后,ax和bx中的数值为多少?

  1. assume cs:code
  2. data segment
  3. dw 8 dup (0)
  4. data ends
  5. code segment
  6. start:mov ax,data
  7. mov ss,ax
  8. mov sp,16
  9. mov word ptr ss:[0],offset s
  10. mov ss:[2],cs
  11. call dword ptr ss:[0]
  12. nop
  13. s:mov ax,offset s
  14. sub ax,ss:[0cH]
  15. mov bx,cs
  16. sub bx,ss:[0eH]
  17. mov ax,4c00h
  18. int 21h
  19. code ends
  20. end start

三、call和ret的配合使用

ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移。CPU 执行 ret 指令时,进行下面两步操作:

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

image.png
call 指令将当前的 IP 压栈后,转到标号处执行指令。CPU 执行此种格式的 call 指令时,进行如下的操作:

  1. (sp) = (sp) - 2, ((ss)*16+(sp)) = (IP)
  2. (IP) = (IP) + 16位位移

image.png
可以看到上述的 call 指令和 ret 指令互为逆操作,而我们可以配合这两个指令达到某些操作。

程序如下所示,请问下面程序返回前,bx 中的值是多少

  1. assume cs:code
  2. code segment
  3. start: mov ax,1
  4. mov cx,3
  5. call s
  6. mov bx,ax
  7. mov ax,4c00h
  8. int 21h
  9. s: add ax,ax
  10. loop s
  11. ret
  12. code ends
  13. end start

进入 Debug 模式调试,可以看到对应的地址和指令:
image.png
我们开始调试程序,同时注意栈寄存器 SS 和 SP 的变化情况

  1. 执行 mov ax,1,如下

image.png

  1. 执行 mov cx,3

image.png

  1. 执行 call s

image.png
执行该命令后,指针 IP 跳转到 s 标号偏移地址,所以 IP=0010。而将原本的 call s 指令后的 mov bx,ax指令的偏移地址储存到栈中,我们看一下栈中的数据
image.png

  1. 执行 add ax,ax

image.png

  1. 下面是循环环节,所以就不再详细放图了
  2. 循环完成后,执行 **ret** 指令,此时神奇的一幕发生了

image.png
其中栈中的数据弹入到了 IP 中,所以 SP 重新指向栈底。而 IP 现在指向的位置就回到了原先的跳转指令 call s的下一条指令

  1. 执行 mov bx,ax 指令

image.png

  1. 后续…

image.png
所以我们可以看到利用 call 跳转并且在栈中保存回来时的位置,而 ret 又能把这个位置还给 IP,所以能够保证程序返回的时候不迷路。
image.png