距离上一次写文章已经过去3个月了,当初计划至少一个月一篇,不曾想这一拖就是三个月。一直不写的主要原因是当把一个问题弄清楚了,或者说掌握了一个东西,就觉得没有什么可值得写;另外写文章也会花费一定的时间。不过想想阮一峰和王建硕讨论的写文章一方面可以提高自己的表述能力,一方面可以加深自己对知识的理解,于是便又拿起笔写下今天这篇文章。
这篇文章主要对函数调用栈的理论进行讲解,然后通过一个简单的例子,通过GDP-peda对汇编代码进行断点跟踪,加深对函数调用栈的理解。此外也对32位CPU的寄存器做一个简单的介绍。
32位处理器有数据寄存器、变址寄存器、指针寄存器、段寄存器、指令寄存器和标志寄存器,上图中简单介绍了寄存器的名称和基本作用。如果想要更加详细的连接寄存器,请参考Daryl的文章通用32位CPU 常用寄存器及其作用。
关于函数调用栈我们需要知道的是栈空间是从高地址向低地址填充的,在进行函数调用的时候,首先将被调用函数的参数压入栈空间,压入参数的顺序是 a r g n arg_n argn, a r g n − 1 arg_{n-1} argn−1 … a r g 0 arg_0 arg0;接着压入函数的返回地址,函数的返回地址就是call指令的下一条指令的地址;接着压入被调用函数的基地址,基地址存放在EBP寄存器中;最后压入被调用函数的局部变量。关于函数调用栈的更多信息可以参考长亭科技的Jwizard的手把手教你栈溢出从入门到放弃。
通过一个简单的C语言程序,通过GCC编译器将其编译为32位的程序,然后使用GDB-peda对其进行跟踪调试。通过观察寄存器中值的变化,来更加深刻的理解函数调用栈。
#include
int sum(int a, int b) {
int c = 10;
int sum;
sum = a + b + c;
return sum;
}
int main() {
int a, b, res;
a = 2;
b = 3;
res = sum(a, b);
printf("%d\n", res);
return 0;
}
gcc -g sum.c -o sum -m32
通过gdb sum
命令对sum文件进行调试,在gdb-peda中使用l命令可以列出源代码。
在gdp-peda中输入start
命令开始调试,从下图中可以看到寄存器(registers),汇编代码(code),栈空间数据(stack)。此时基地址EBP寄存器的值是0xffffd5a8
,栈顶寄存器ESP的值是0xffffd590
,而指令寄存器EIP的值则是代码区中正准备执行的指令地址0x804843e
。从图中可以看出0x804843b
汇编指令分配了0x14
个字节的空间供局部变量使用。
使用ni
命令,执行0x804843e
地址的汇编代码,可以发现把2
(a的值)赋值给地址为[ebp-0x14] = 0xffffd594
。
使用ni
命令,执行0x8048445
地址的汇编代码,可以发现把3
(b的值)赋值给地址为[ebp-0x10] = 0xffffd598
。
在压入之前,esp的值为0xffffd590
,所以b
在栈空间的位置是0xffffd590 - 4 = 0xffffd58c
。
使用si
命令,单步进入sum函数。从下图可以看出执行call指令的时候把call指令的下一条指令的地址0x8048457
(见3.4.4图)作为返回地址压入了栈空间。接着需要将main函数的基地址(存放在ebp寄存器)压入栈空间。
使用ni
命令执行0x804840c
地址的指令,从上面的理论部分得知在压入调用函数的基地址之后,需要将当前栈顶(esp寄存器的值)赋值给ebp作为被调用函数的基地址,因为接下来就是执行被调用函数中的汇编代码。这里需要注意被调用函数的基地址存储的值是调用函数的基地址。
此时我们已经完成了调用函数的入栈操作,通过stack
命令可以查看当前栈空间的内容0xffffd59c
我们暂时不管,这是main函数启动时的相关数据。从高地址到低地址分别存放的是0xffffd598
=> main的局部变量b,0xffffd594
=> main的局部变量a,0xffffd590
=> 暂未使用,
0xffffd58c
=> sum函数的参数b,0xffffd588
=> sum函数的参数a,0xffffd584
=> sum函数的返回地址,0xffffd580
=> main函数的基地址。
在压入main函数的基地址后,执行了sum函数内部的加法操作,这里不做详细介绍。在执行完成以后会把结果放在eax寄存器中。然后执行leave
指令,leave
指令相当于mov esp,ebp; pop ebp;
。执行mov esp,ebp
将栈顶地址设置为被调用函数(sum函数)的基地址,被调用函数的基地址就是存储main函数基地址的地址。执行pop ebp
,将ebp以下的地址包括ebp全部弹出栈空间(sum函数内部处理的时候也会把一些数据入栈,此时操作完成全部弹出),并把main函数的基地址赋值给ebp,成功完成函数的退栈。
最后执行ret
指令,类似pop eip
。把函数的返回地址赋值给eip,使其继续执行。