1. 前言
在我们前期学习C语言时,可能会有很多疑问 ? 比如:
- 局部变量是怎么创建的?
- 为什么未初始化的局部变量的值是随机值?
- 函数是怎样传参的?传参的顺序是怎样的?
- 形参和实参是什么关系?
- 函数调用是怎样做的?
- 函数调用后是怎样返回的?
本章将在汇编层面讨论解释相关问题,环境为 CentOS7.6
, 使用编译器是 GCC
, 使用调试器为 GDB
2. 前置知识
我们都知道CPU中的寄存器,本章主要涉及的是
寄存器名称 | 用途 |
---|---|
eax | 通用寄存器,保留临时数据,常用于返回值 |
ebx | 通用寄存器,保留临时数 |
ebp | 栈底寄存器 |
esp | 栈顶寄存器 |
eip | 指令寄存器,保存当前指令的下一条指令的地址 |
其中重点关注的是 esp
和 ebp
这两个寄存器, 两个寄存器存放的都是地址, ebp
存放栈底地址, esp
存放栈顶地址,两个寄存器用来维护栈帧空间,这块空间是在虚拟内存中栈空间中的.
同时因为涉及汇编代码,我们也简单了解一下一点汇编的指令
汇编指令 | 用途 |
---|---|
mov | 数据转移指令 |
push | 数据入栈,同时esp栈顶寄存器也要发生变化 |
pop | 数据弹出至指定位置,同时esp栈顶寄存器也要发生变化 |
sub | 减法命令 |
add | 加法命令 |
call | 函数调用 1.压入返回地址;2.转入目标函数 |
jump | 通过修改eip,转入目标函数,进行调用 |
ret | 恢复返回地址,压入eip,类似pop eip命令 |
3. c语言函数调用过程
为了观察到程序运行时,栈空间是如何创建,esp
和ebp
这两个寄存器是如何维护的等等问题,这里,我创建了一个用来得到两数相加和的程序.
#include <stdio.h>
int Add(int num1, int num2)
{
return num1 + num2;
}
int main(void)
{
int a = 2;
int b = 3;
int c = 0;
c = Add(a, b);
printf("%d\n", c);
return 0;
}
通过 gcc 编译这个程序得到可执行程序test
,我们使用 gdb 对该程序进行调试,首先disass main
反汇编main函数, 得到 main
函数第一条指令的地址为0x0000000000400541
.然后使用这个地址作为断点,从头开始运行程序.
[root@VM-4-13-centos test]# gcc -g test.c -o test
[root@VM-4-13-centos test]# gdb test
(gdb) disass main
Dump of assembler code for function main:
0x0000000000400541 <+0>: push %rbp
0x0000000000400542 <+1>: mov %rsp,%rbp
0x0000000000400545 <+4>: sub $0x10,%rsp
0x0000000000400549 <+8>: movl $0x2,-0x4(%rbp)
0x0000000000400550 <+15>: movl $0x3,-0x8(%rbp)
0x0000000000400557 <+22>: movl $0x0,-0xc(%rbp)
0x000000000040055e <+29>: mov -0x8(%rbp),%edx
0x0000000000400561 <+32>: mov -0x4(%rbp),%eax
0x0000000000400564 <+35>: mov %edx,%esi
0x0000000000400566 <+37>: mov %eax,%edi
0x0000000000400568 <+39>: callq 0x40052d <Add>
0x000000000040056d <+44>: mov %eax,-0xc(%rbp)
0x0000000000400570 <+47>: mov -0xc(%rbp),%eax
0x0000000000400573 <+50>: mov %eax,%esi
0x0000000000400575 <+52>: mov $0x400620,%edi
0x000000000040057a <+57>: mov $0x0,%eax
0x000000000040057f <+62>: callq 0x400410 <printf@plt>
0x0000000000400584 <+67>: mov $0x0,%eax
0x0000000000400589 <+72>: leaveq
0x000000000040058a <+73>: retq
End of assembler dump.
(gdb) b *0x0000000000400541
Breakpoint 1 at 0x400541: file test.c, line 9.
(gdb) r
Starting program: /root/test/test
在进入main
函数之前,我们观察rip
, rbp
, rsp
的内容.
rip
记录了下一条指令的地址即main
函数的第一条指令的地址.rbp
的值是0x0
,说明这个时候还没有创建栈帧rsp
的值是0x7fffffffe468
,这是栈顶地址
Breakpoint 1, main () at test.c:9
(gdb) i r rsp rip rbp
rsp 0x7fffffffe468 0x7fffffffe468
rip 0x400541 0x400541 <main>
rbp 0x0 0x0
接着运行main
函数的指令,首先前三句是函数序言,主要作用是保存rbp
以及为当前函数分配栈空间
(gdb) disass
Dump of assembler code for function main:
=> 0x0000000000400541 <+0>: push %rbp ;将rbp的内容压入栈中进行保存
0x0000000000400542 <+1>: mov %rsp,%rbp ;将rsp的内容传递给rbp(此时原来的栈顶为栈底)
0x0000000000400545 <+4>: sub $0x10,%rsp ;分配主函数需要的栈空间
首先0x0000000000400541 <+0>: push %rbp
:将rbp的内容压入栈中,同时rsp-8,此时rsp的值为0x7fffffffe460
接着0x0000000000400542 <+1>: mov %rsp,%rbp
:将rsp此时的内容传递给rbp,开辟栈空间,此时栈底地址 rbp 为之前栈顶的地址0x7fffffffe460
最后0x0000000000400545 <+4>: sub $0x10,%rsp
:主函数需要用到0x10
的栈空间,将 rsp 减去这个值, 此时 栈顶地址 rsp 为 0x7fffffffe450
三条指令完成后,观察三个寄存器的状态
(gdb) i r rip rsp rbp
rip 0x400549 0x400549 <main+8>
rsp 0x7fffffffe450 0x7fffffffe450
rbp 0x7fffffffe460 0x7fffffffe460
接着运行下面三个指令
0x0000000000400549 <+8>: movl $0x2,-0x4(%rbp)
0x0000000000400550 <+15>: movl $0x3,-0x8(%rbp)
0x0000000000400557 <+22>: movl $0x0,-0xc(%rbp)
分别将 a, b, c 的值压入存放在之前开辟的栈空间内,观察内存,确实存放了进去
(gdb) x/ 0x7fffffffe45c
0x7fffffffe45c: 2
(gdb) x/ 0x7fffffffe458
0x7fffffffe458: 3
(gdb) x/ 0x7fffffffe454
0x7fffffffe454: 0
接着运行下面四条指令
0x000000000040055e <+29>: mov -0x8(%rbp),%edx
0x0000000000400561 <+32>: mov -0x4(%rbp),%eax
0x0000000000400564 <+35>: mov %edx,%esi
0x0000000000400566 <+37>: mov %eax,%edi
这是调用函数前的准备操作,将第一个参数的值存入esi
,第二个参数的值存入edi
.这就是为什么说形参是实参的一份临时拷贝.
参数准备好了之后,接着执行 call 指令调用 Add 函数
0x0000000000400568 <+39>: callq 0x40052d <Add>
call 指令, 执行的时候会先将此时 rip 存放的地址 即下一条指令的地址压入栈中,以便返回函数继续执行下一条指令
在本程序, 即将0x40056d
压入栈中
(gdb) x/x 0x7fffffffe448
0x7fffffffe448: 0x0040056d
接着转入 Add 函数
0x000000000040052d <+0>: push %rbp
0x000000000040052e <+1>: mov %rsp,%rbp
0x0000000000400531 <+4>: mov %edi,-0x4(%rbp)
0x0000000000400534 <+7>: mov %esi,-0x8(%rbp)
0x0000000000400537 <+10>: mov -0x8(%rbp),%eax
0x000000000040053a <+13>: mov -0x4(%rbp),%edx
0x000000000040053d <+16>: add %edx,%eax
0x000000000040053f <+18>: pop %rbp
0x0000000000400540 <+19>: retq
一样先是函数序言,将rbp的值压入栈中,同时rsp-8,因为在函数中没有创建临时变量,此时rsp
和rbp
的值都为0x7fffffffe440
(gdb) x/x 0x7fffffffe440
0x7fffffffe440: 0xffffe460
(gdb) i r rsp rbp
rsp 0x7fffffffe440 0x7fffffffe440
rbp 0x7fffffffe440 0x7fffffffe440
接着将两个参数寄存器的值压入栈中
0x0000000000400531 <+4>: mov %edi,-0x4(%rbp)
0x0000000000400534 <+7>: mov %esi,-0x8(%rbp)
然后执行相加指令,并把结果放入返回值寄存器eax
中
0x000000000040053d <+16>: add %edx,%eax
最后函数结束,归还栈空间
- 将当前rsp所指内存中的值
0x7fffffffe460
放入rbp寄存器,这样,rbp就恢复到了还未执行 Add 函数时的值,也就是 main 函数的栈帧起始地址 - 将rsp寄存器的值加8, 这样rsp就指向了包含 Add 函数下一条指令地址
0x000000000040056d
的内存
0x000000000040053f <+18>: pop %rbp
接着执行retq指令,该指令会把 rsp 指向的栈空间的 0x000000000040056d
去给 rip,同时rsp - 8 为 0x7fffffffe450
注意此时返回值寄存器 eax 的值为 5
(gdb) i r eax
eax 0x5 5
回到主函数,继续执行 printf 函数, 操作相似这里就不过多赘述.
最后主函数结束时,rbp 取到当前 rbp指向的内存空间的值, rsp重新加上 0x10
,最后再加上0x8
.回到最初的情况,程序结束
本章完.
d去给 rip,同时rsp - 8 为
0x7fffffffe450`
注意此时返回值寄存器 eax 的值为 5
(gdb) i r eax
eax 0x5 5
回到主函数,继续执行 printf 函数, 操作相似这里就不过多赘述.
最后主函数结束时,rbp 取到当前 rbp指向的内存空间的值, rsp重新加上 0x10
,最后再加上0x8
.回到最初的情况,程序结束
本章完.