call和ret指令都是转移指令,他们都修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序的设计,这一章讲解call和ret指令的原理。
一、ret和retf
1.1 相关知识
ret指令用栈中的数据,修改 IP 的内容,从而实现近转移;retf指令用栈中的数据,修改 CS 和 IP 的内容,从而实现远转移
CPU执行ret指令时,进行下面两步操作
%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)
%3D(SP)%2B2%7D#card=math&code=%5Ctext%7B%28SP%29%3D%28SP%29%2B2%7D&id=O3MdC)
CPU执行retf指令时,进行下面4步操作:
%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)
%3D(sp)%2B2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29%2B2%7D&id=TUAvp)
%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)
%3D(sp)%2B2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29%2B2%7D&id=yakdv)
可以看出,如果我们用汇编语法来解释ret和retf指令,则:
- CPU执行
ret指令时,相当于进行:pop IP - CPU执行
retf指令时,相当于进行:pop IPpop CS
下面程序中,ret指令执行后,(IP)=0,CS:IP指向代码段的第一条指令
assume cs:codestack segmentdb 16 dup (0)stack endscode segmentmov ax,4c00hint 21hstart:mov ax,stack ; ax=076Amov ss,ax ; ss=076Amov sp,16 ; sp=16mov ax,0 ; ax=0push ax ; 076a:e ~ 076a:f处的值为0(使用栈来管理)mov bx,0 ; bx=0ret ; 将076A:E 对应的数据传向IPcode endscode start
执行完
mov bx,0这句话后内存情况
之后再执行
RET,就将对应的SS:SP的内容给了IP,即IP=0000
之后程序就可以跳转到最一开始执行
下面的程序中,retf指令执行后,CS:IP指向代码段的第一条指令
assume cs:codestack segmentdb 16 dup (0)stack endscode segmentmov ax,4c00hint 21hstart:mov ax,stackmov ss,axmov sp,16mov ax,0push cspush axmov bx,0retfcode endsend start
1.2 检测点10.1
补全程序,实现从内存1000:0000处开始执行指令
assume cs:codestack segmentdb 16 dup (0)stack endscode segmentstart:mov ax,stack ; ax=076Amov ss,ax ; ss=076Amov sp,16 ; sp=16mov ax,1000H ; ax=1000Hpush ax ; 076A:e ~ 076A:f处的值设为1000mov ax,0000Hpush ax ; 076A:c ~ 076A:d处的值为0000retfcode endsend start
二、call指令
CPU执行call指令时,进行两步操作:
- 将当前的
IP或CS和IP压入栈中 - 转移
call指令不能实现短转移,除此之外,call指令实现转移的方法和jmp指令的原理相同,下面通过给出转移目的地址的不同方法为主线,讲解call指令的主要应用格式。
2.1 依据位移进行转移的call指令
2.1.1 相关知识
call标号(将当前的IP压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
%3D(SP)-2%7D#card=math&code=%5Ctext%7B%28SP%29%3D%28SP%29-2%7D&id=exMRP)
%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)
%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指令,其实就相当于push和jmp指令的结合
push IPjmp 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 |
将该程序拿出调试
assume cs:codecode segmentstart:mov ax,0call sinc axs:pop axcode endsend start
调试如下所示:

调试完,结果为AX=0006
下面的程序执行后,ax中的数值是 6,要搞清楚如果是跳转指令,什么时候修改IP的值。
学过计算机组成原理的肯定知道,指令读取后,IP值自动加1(这儿的1是指下一条指令,不是指下一个字节),指向下一条指令,如果经CPU分析后是跳转指令,则再修改IP的值。这两者原理是一样
1000:0 mov ax,0 ;读取此条指令后IP=3 ,执行完该指令后IP=31000:3 call s ;读取此条指令后IP=6 ,所以IP=6入栈,执行完该指令后IP=7,跳转到s处1000:6 inc ax1000:7 s:pop ax ;所以POP后,ax=6
2.2 转移的目的地址在指令中的call指令
2.2.1 相关知识
前面讲的call指令,其对应的机器指令中并没有转移目的地址,而是相对于当前IP的转移位移。
call far ptr 标号 ; 实现的是段间转移
call标号(将当前的IP压栈后,转到标号处执行指令)
CPU执行此种格式的call指令时,进行如下的操作:
%3D(sp)-2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29-2%7D&id=v4vsV)
*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)
%3D(sp)-2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29-2%7D&id=d3X0j)
%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)
%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指令,则相当于进行
; 执行call far ptr 标号时; 相当于进行push CSpush IPjmp 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 |
将上述代码进行调试
之后开始进行单步调试,当执行完
call far ptr s这句话后,查看栈中(SS和SP)被压入的数据,从而得到压入的数据为 08 00 6A 07 (这里有问题了)按理来说,插入的数据为09 00 才对,为什么会成为08 00
之后的问题就很简单了,只需要将栈中的数据进行弹出即可,最后的答案如下
2.3 转移地址在寄存器中的call指令
2.3.1 相关知识
指令格式:call 16位reg
功能:
%3D(sp)-2%7D#card=math&code=%5Ctext%7B%28sp%29%3D%28sp%29-2%7D&id=SOZuX)
*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)
%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时,相当于进行
push IPjmp 16位reg
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] |
现在充能
时间到了
最后得到的结果如下所示,这个结果是由于
call ax指令将IP地址压入到栈中,而add ax,[bp]就是用的栈中的数据
bp寄存器,跟其它什么BX,AX一样的用法,SP是用在栈上的,配合SS使用,像SS:SPSS上放段地址,SP上放偏移地址。寻址时,像[bp],相当于SS:[bp]只要在[...]中使用寄存器bp,而指令中没有显示给出段地址,段地址就默认在ss中像BX默认使用CS
2.4 转移地址在内存中的call指令
2.4.1 相关知识
转移地址在内存中的call指令有两种格式
格式1:
call word ptr 内存单元地址
用汇编语法来解释此种格式的call指令,则相当于进行
push IPjmp word ptr 内存单元地址
比如下面的指令
mov sp,10hmov ax,0123hmov ds:[0],axcall word ptr ds:[0] ; 将ds:[0]处的数据给IP; 因为执行了一个push IP的动作,而且这个是一个字节,2个字,所以SP = 10H-2=0EH
执行后,(IP)=0123H, (SP)=0EH
格式2:
call dword ptr 内存单元地址
用汇编语法来解释此种格式的call指令,则相当于进行
push CSpush IPjmp dword ptr 内存单元地址
比如下面的指令:
mov sp,10hmov ax,0123hmov ds:[0],ax ; 075A:0000处插入数据0123mov word ptr ds:[2],0 ; 075A:0004插入数据0000call dword ptr ds:[0] ; sp = 10H-4 = 0cH

执行后,(CS)=0, (IP)=0123H, (SP)=0CH
2.4.2 检测点10.5
(1)下面的程序执行后,ax中的数值为多少?(注意:用call指令的原理分析,不要在Debug中单步跟踪来验证你的结论。对于此程序,在Debug中单步跟踪的结果,不能代表CPU的实际执行结果。)
assume cs:codestack segment ; 在076a:0处开始开辟8个字节,即16个字大小的空间dw 8 dup (0)stack endscode segmentstart:mov ax,stack ; ax=076amov ss,ax ; ss=076amov sp,16 ; sp = 16hmov ds,ax ; ds=076amov ax,0 ; ax=0call word ptr ds:[0EH] ;076a:0Eh处的数据赋值给IPinc axinc axinc axmov ax,4c00hint 21hcode endsend start
这道题不太会,因为
call word ptr ds:[0EH]这句话得到的IP=7302,对这句话的由来解释不清楚
(2)下面的程序执行后,ax和bx中的数值为多少?
assume cs:codedata segmentdw 8 dup (0)data endscode segmentstart:mov ax,datamov ss,axmov sp,16mov word ptr ss:[0],offset smov ss:[2],cscall dword ptr ss:[0]nops:mov ax,offset ssub ax,ss:[0cH]mov bx,cssub bx,ss:[0eH]mov ax,4c00hint 21hcode endsend start
三、call和ret的配合使用
ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移。CPU 执行 ret 指令时,进行下面两步操作:
- (IP) = ((ss)*16 + (sp))
- (sp) = (sp) + 2

call 指令将当前的 IP 压栈后,转到标号处执行指令。CPU 执行此种格式的 call 指令时,进行如下的操作:
- (sp) = (sp) - 2, ((ss)*16+(sp)) = (IP)
- (IP) = (IP) + 16位位移

可以看到上述的 call 指令和 ret 指令互为逆操作,而我们可以配合这两个指令达到某些操作。
程序如下所示,请问下面程序返回前,bx 中的值是多少
assume cs:codecode segmentstart: mov ax,1mov cx,3call smov bx,axmov ax,4c00hint 21hs: add ax,axloop sretcode endsend start
进入 Debug 模式调试,可以看到对应的地址和指令:
我们开始调试程序,同时注意栈寄存器 SS 和 SP 的变化情况
- 执行
mov ax,1,如下

- 执行
mov cx,3

- 执行
call s

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

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

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

- 后续…

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







