一、目的
最近在看汇编,对于函数调用时RBP/RSP寄存器的变化比较好奇,故用一个简单的c程序,在gdb下对其做一个探究。
二、环境
OS:Ubuntu18.04.1 x86_64
Kernel:5.4.0-149-generic
三、过程
1、源文件main.c
2、编译+gdb运行
3、加断点运行
4、查看CPU寄存器
关注rbp/rsp这两个寄存器。这里有个疑问:rsp寄存器指向的内存地址,比rbp低10h个字节,为什么?需要查看main函数的汇编代码。
5、查看汇编代码
使用"disassemble main"命令查看main函数的汇编代码。发现此时rip寄存器指向"movl $0x1,-0x8(%rbp)"这条语句,之前的三条语句是在跳转到main函数时自动完成的。下面来分析下最开始这三条语句:
push %rbp # 将rbp中存放的地址压入堆栈
mov %rsp,%rbp # 将rsp中存放的地址复制到rbp寄存器中
sub $0x10,%rsp # rsp自减10h,这16个字节空间是用来存放临时变量的,这就是为什么一进main函数rsp就比rbp低了10h。
这三行语句完成了当前函数的堆栈初始化,每个函数都有自己的堆栈帧(stack frame),我们来画下当前main函数的stack frame。从CPU寄存器中得知rbp指向0x7fffffffddc0,rsp指向0x7fffffffddb0。采用4字节对齐。rbp和rsp都是64位寄存器,占8个字节,所以stack frame如下:
至于内存中的值,可以使用如下命令去读:
6、单步调试
输入两次"n"命令,将会执行以下两条指令:
movl $0x1,-0x8(%rbp) # 把rbp-8即ddb8的地方存入变量a的值
movl $0x2,-0x4(%rbp) # 把rbp-4即ddbc的地方存入变量b的值
执行完后的stack frame如下:
继续输入"n",执行callq指令:
callq 0x5555555545fa <func> # 跳转到func,这一步会把原来的rip寄存器内容压栈,原来rip的值是0x0000555555554643
执行完后的stack frame如下:
查看dda8内存地址的内容:
查看func的汇编代码:
发现此时rip指向第三行指令,前两行指令在callq指令时已完成,分析下前两行指令:
push %rbp # rbp入栈。即将原来rbp的值0x7fffffffddc0存放到dda0的内存处,同时rsp移动到dda0
mov %rsp,%rbp # 将当前rsp的值即0x7fffffffdda0更新到rbp中
执行完后的stack frame如下:
此时完成了func函数的stack frame的初始化。
继续执行以下指令:
movl $0x1,-0x14(%rbp)
movl $0x2,-0x10(%rbp)
movl $0x3,-0xc(%rbp)
movl $0x4,-0x8(%rbp)
movl $0x5,-0x4(%rbp)
执行后的stack frame:
查看内存:
继续执行以下指令:
mov $0x0,%eax # 立即数0放入eax寄存器,作为返回值
pop %rbp # 从栈顶取出内容即0x7fffffffddc0赋给rbp,rsp自减即dda8
retq # 从栈顶取出内容即0x555555554643赋给rip,rsp自减即ddb0
执行后的stack frame:
rbp和rsp又回到了调用func之前的状态,即还原了main函数的stack frame。以上就是函数调用过程中堆栈及CPU寄存器的变化。
四、总结
1、函数的起始通过下面这两行指令,完成原函数堆栈基址的保存,并为新函数创建了新的stack frame。
push %rbp
mov %rsp,%rbp
2、在子函数stack frame刚初始化完时,rsp和rbp寄存器指向同一个内存地址(main函数为了存放临时变量,rsp进行了自减,这种情况先抛开不谈)。这个内存地址中存放的内容是原函数或者说父函数的堆栈基址。在子函数返回时,通过"pop %rbp",rbp又重新指向了父函数的堆栈基址。
五、GDB命令
1、查看CPU寄存器
i register [寄存器名]
2、查看内存的值
x/<n/f/u>
n: 要显示的内存单元的数量
f: 格式,x-16进制,d-10进制
u: 每个内存单元的字节长度,默认4。b-单字节,h-双字节,w-四字节,g-8字节
3、查看函数的汇编代码
disassemble [函数名]