ret指令与call指令的深入理解

背景

1、代码

2、编译

1
gcc Rj45.c -o Rj45 -m32


3、反编译

1
objdump -d Rj45 -M intel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0804840b <main>:
804840b: 8d 4c 24 04 lea ecx,[esp+0x4]
804840f: 83 e4 f0 and esp,0xfffffff0
8048412: ff 71 fc push DWORD PTR [ecx-0x4]
//从上述汇编代码可以看到,在底层中,main函数并不如我们接触的编程语言一样,为程序运行时第一个执行函数,还有函数在main函数前执行。
8048415: 55 push ebp
8048416: 89 e5 mov ebp,esp
8048418: 51 push ecx
8048419: 83 ec 04 sub esp,0x4
804841c: 83 ec 0c sub esp,0xc
804841f: 68 c0 84 04 08 push 0x80484c0//压入参数`hello,Rj45`所存储的地址
8048424: e8 b7 fe ff ff call 80482e0 <puts@plt>//调用puts函数进行打印
8048429: 83 c4 10 add esp,0x10//进行栈帧平衡
804842c: b8 00 00 00 00 mov eax,0x0
8048431: 8b 4d fc mov ecx,DWORD PTR [ebp-0x4]
8048434: c9 leave
8048435: 8d 61 fc lea esp,[ecx-0x4]
8048438: c3 ret //puts函数返回
8048439: 66 90 xchg ax,ax
804843b: 66 90 xchg ax,ax
804843d: 66 90 xchg ax,ax
804843f: 90 nop

4、问题
在反汇编的过程中,有两个比较关键的地方:callret
在《汇编语言(第3版)》中对这两个指令的概述为:

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

其中ret指令用栈中的数据,修改IP的内容,实现近转移。
而call指令将IP或者CS和IP压入栈中,实现转移。
还有retf指令,用栈中的数据,修改CS和IP的内容,实现远转移。

那么,如何理解这个概述,以及如何理解从概述延申出来的概念性的知识

什么是转移指令?

1、概念
转移指令是可以控制CPU执行内存中某处代码的指令,或者说,转移指令是可以修改IP,或同时修改CS和IP的指令。

根据转移行为可分为段内转移和段间转移,其中只修改IP的叫段内转移;同时修改CS和IP的,叫段间转移

由于转移指令对IP或CS和IP的修改范围的不同,又分为短转移、近转移和远转移。

2、分类

1
2
3
4
5
无条件转移指令,如jmp
条件转移指令,如jz
循环指令,如loop
过程
中断

什么是CS和IP?

CS是代码段寄存器、IP是指令指针寄存器。
(8086CPU有四个段寄存器,CS、DS、SS、ES,当CPU要访问内存是由这四个段寄存器提供内存单元的段地址。)
这两个寄存器是8086CPU中最关键的寄存器,它们指示了CPU当前要读取指令的地址。

CS和IP如何指示CPU读取指令的地址?

8086机器中,任意时刻,CPU将CS:IP指向的内容当作指令执行。
意思是,任意时刻,设CS中的内容是M,IP中的内容为N,8086CPU将从内存M*16+N单元开始,读取一条指令并执行。


解释:
CPU当前的状态是CS内数据为2000H,IP内为0000H
内存20000到20009H单元存放着可执行的机器码

1、CPU将从内存2000H*16+0000H出读取指令执行,其中将CS和IP内容送入地址加法器,地址加法器完成后,得到物理地址=段地址*16+偏移地址

2、地址加法器将物理地址送人输入输出控制电路,输入输出控制电路将物理地址20000H送上地址总线。

3、从内存20000H单元开始存放的机器指令B8 23 01即mov ax,0123H通过数据总线被送入CPU。

4、输入输出控制电路将机器指令送入指令缓冲器,接着到执行控制器。执行控制器执行指令后,AX内的内容为0123H。

5、读取一条指令后,IP中的值会自动增加,以使CPU可以读取下一个指令。因当前读取的指令B8 23 01的长度为3,故IP的值加3,CS:IP指向内存单元2000:0003。

6、以此依次执行。

什么是段地址和偏移地址?

什么是段?段的划分来自于CPU,而不是内存,内存没有分段。
由于8086CPU的内存的物理地址=段地址*16+偏移地址,故使得在管理内存的时候借助了分段的概念,将若干个地址连续的内存单元看作一个段。

什么是段地址和偏移地址?
这是8086CPU对内存读写图

CPU的两个相关部件提供两个16位地址、段地址和偏移地址。
段地址和偏移地址通过内部总线送到地址加法器。
地址加法器通过物理地址=段地址\*16+偏移地址,合成20位物理地址。
输入输出电路将物理地址送上地址总线。
故段地址和偏移地址也即CS内地址数据和IP内地址数据。

ret指令与call指令实现了什么功能?

1、ret指令用栈中的数据,修改IP的内容,返回代码段的第一条指令。
相当于

1
pop IP

2、call指令将IP或者CS和IP压入栈中,实现转移。
相当于

1
2
3
push IP
//push IP和push CS
jmp [address]

3、retf指令,用栈中的数据,修改CS和IP的内容,返回代码段的第一条指令。
相当于

1
2
pop IP
pop CS

以ret和call实现的子程序源程序的框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code
code segment
main: :
:
call sub1//调用子程序sub1
:
mov ax,4c00h
int 21h
sub1: :
:
call sub2//调用子程序sub2
:
ret//子程序返回
sub2: ://子程序sub2开始
:
ret//子程序返回
code ends
end main

解释:
一个写有一定功能的程序段叫子程序。
在子程序需要被执行的时候用call指令去调用。
当子程序执行完后,由于call指令后面的指令的地址已经存储在栈中(push操作),
使得在子程序后面再使用ret指令时候,栈中的数据会设置IP内的值,
IP内的值的设置实现了CPU继续执行call指令后面的代码指令,达到程序继续执行的目的。

参考

《汇编语言(第3版)》