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 IP
pop CS
下面程序中,ret
指令执行后,(IP)=0
,CS:IP指向代码段的第一条指令
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
mov ax,4c00h
int 21h
start:mov ax,stack ; ax=076A
mov ss,ax ; ss=076A
mov sp,16 ; sp=16
mov ax,0 ; ax=0
push ax ; 076a:e ~ 076a:f处的值为0(使用栈来管理)
mov bx,0 ; bx=0
ret ; 将076A:E 对应的数据传向IP
code ends
code start
执行完
mov bx,0
这句话后内存情况之后再执行
RET
,就将对应的SS:SP的内容给了IP,即IP=0000之后程序就可以跳转到最一开始执行
下面的程序中,retf
指令执行后,CS:IP指向代码段的第一条指令
assume cs:code
stack segment
db 16 dup (0)
stack ends
code 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
code ends
end start
1.2 检测点10.1
补全程序,实现从内存1000:0000处开始执行指令
assume cs:code
stack segment
db 16 dup (0)
stack ends
code segment
start:mov ax,stack ; ax=076A
mov ss,ax ; ss=076A
mov sp,16 ; sp=16
mov ax,1000H ; ax=1000H
push ax ; 076A:e ~ 076A:f处的值设为1000
mov ax,0000H
push ax ; 076A:c ~ 076A:d处的值为0000
retf
code ends
end 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 IP
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 |
将该程序拿出调试
assume cs:code
code segment
start:mov ax,0
call s
inc ax
s:pop ax
code ends
end start
调试如下所示:
调试完,结果为AX=0006
下面的程序执行后,ax中的数值是 6,要搞清楚如果是跳转指令,什么时候修改IP的值。
学过计算机组成原理的肯定知道,指令读取后,IP值自动加1(这儿的1是指下一条指令,不是指下一个字节),指向下一条指令,如果经CPU分析后是跳转指令,则再修改IP的值。这两者原理是一样
1000:0 mov ax,0 ;读取此条指令后IP=3 ,执行完该指令后IP=3
1000:3 call s ;读取此条指令后IP=6 ,所以IP=6入栈,执行完该指令后IP=7,跳转到s处
1000:6 inc ax
1000: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 CS
push IP
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 |
将上述代码进行调试
之后开始进行单步调试,当执行完
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 IP
jmp 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:SP
SS上放段地址,SP上放偏移地址。
寻址时,像[bp],相当于SS:[bp]
只要在[...]中使用寄存器bp,而指令中没有显示给出段地址,段地址就默认在ss中
像BX默认使用CS
2.4 转移地址在内存中的call指令
2.4.1 相关知识
转移地址在内存中的call
指令有两种格式
格式1:
call word ptr 内存单元地址
用汇编语法来解释此种格式的call
指令,则相当于进行
push IP
jmp word ptr 内存单元地址
比如下面的指令
mov sp,10h
mov ax,0123h
mov ds:[0],ax
call word ptr ds:[0] ; 将ds:[0]处的数据给IP
; 因为执行了一个push IP的动作,而且这个是一个字节,2个字,所以SP = 10H-2=0EH
执行后,(IP)=0123H, (SP)=0EH
格式2:
call dword ptr 内存单元地址
用汇编语法来解释此种格式的call
指令,则相当于进行
push CS
push IP
jmp dword ptr 内存单元地址
比如下面的指令:
mov sp,10h
mov ax,0123h
mov ds:[0],ax ; 075A:0000处插入数据0123
mov word ptr ds:[2],0 ; 075A:0004插入数据0000
call 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:code
stack segment ; 在076a:0处开始开辟8个字节,即16个字大小的空间
dw 8 dup (0)
stack ends
code segment
start:mov ax,stack ; ax=076a
mov ss,ax ; ss=076a
mov sp,16 ; sp = 16h
mov ds,ax ; ds=076a
mov ax,0 ; ax=0
call word ptr ds:[0EH] ;076a:0Eh处的数据赋值给IP
inc ax
inc ax
inc ax
mov ax,4c00h
int 21h
code ends
end start
这道题不太会,因为
call word ptr ds:[0EH]
这句话得到的IP=7302,对这句话的由来解释不清楚
(2)下面的程序执行后,ax和bx中的数值为多少?
assume cs:code
data segment
dw 8 dup (0)
data ends
code segment
start:mov ax,data
mov ss,ax
mov sp,16
mov word ptr ss:[0],offset s
mov ss:[2],cs
call dword ptr ss:[0]
nop
s:mov ax,offset s
sub ax,ss:[0cH]
mov bx,cs
sub bx,ss:[0eH]
mov ax,4c00h
int 21h
code ends
end 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:code
code segment
start: mov ax,1
mov cx,3
call s
mov bx,ax
mov ax,4c00h
int 21h
s: add ax,ax
loop s
ret
code ends
end 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,所以能够保证程序返回的时候不迷路。