9/01/2010

Linux 平台下函数调用的堆栈分析

先给一个很简单的函数调用的程序,如下:

#include <stdio.h>

int func(int a)
{
int i = a;
return i;
}

int main()
{
int i = 2;
func(i);
i = 1;
printf("i=%d\n", i);
return 0;
}

这个程序运行的结果是打印"i=1",我们要做的实验就是通过改变堆栈 ebp 的值,使得调用函数func 后返回到main 函数中跳过"i =
1;",从而该程序运行结果变为"i=2"。

先看看main 和 func两个函数的汇编代码:

(gdb) disassemble main
Dump of assembler code for function main:
0x080483c1 <main+0>: lea 0x4(%esp),%ecx
0x080483c5 <main+4>: and $0xfffffff0,%esp
0x080483c8 <main+7>: pushl -0x4(%ecx)
0x080483cb <main+10>: push %ebp
0x080483cc <main+11>: mov %esp,%ebp
0x080483ce <main+13>: push %ecx
0x080483cf <main+14>: sub $0x24,%esp
0x080483d2 <main+17>: movl $0x2,-0x8(%ebp)
0x080483d9 <main+24>: mov -0x8(%ebp),%eax
0x080483dc <main+27>: mov %eax,(%esp) /* argument */
0x080483df <main+30>: call 0x80483b0 <f>
0x080483e4 <main+35>: movl $0x1,-0x8(%ebp) /* return address */
0x080483eb <main+42>: mov -0x8(%ebp),%eax
0x080483ee <main+45>: mov %eax,0x4(%esp)
0x080483f2 <main+49>: movl $0x80484d0,(%esp)
0x080483f9 <main+56>: call 0x8048300 <printf@plt>
0x080483fe <main+61>: mov $0x0,%eax
0x08048403 <main+66>: add $0x24,%esp
0x08048406 <main+69>: pop %ecx
0x08048407 <main+70>: pop %ebp
0x08048408 <main+71>: lea -0x4(%ecx),%esp
0x0804840b <main+74>: ret
End of assembler dump.

(gdb) disassemble func
Dump of assembler code for function func:
0x080483b0 <func+0>: push %ebp
0x080483b1 <func+1>: mov %esp,%ebp
0x080483b3 <func+3>: sub $0x10,%esp
0x080483b6 <func+6>: mov 0x8(%ebp),%eax
0x080483b9 <func+9>: mov %eax,-0x4(%ebp)
0x080483bc <func+12>: mov -0x4(%ebp),%eax
0x080483bf <func+15>: leave
0x080483c0 <func+16>: ret
End of assembler dump.

当调用func函数时,该程序进程的堆栈发生了哪些变化呢?
首先需要传给func 的参数依次入栈,然后是函数的返回地址入栈,也就是call指令的下一条地址,进入func函数后,先把当前的 ebp
入栈,也就是上一次的栈帧地址入栈,其次分配栈空间,为函数的局部变量腾出地方。此时堆栈情况大致如下:
_________________
| int i = 2 |
| ---------------------|
| argument |
| ---------------------|
| return address |
| ---------------------|
| previous frame |
| pointer address |
| ---------------------|
| int i = a |
|________________|

我们只要把这里的 return address 改为0x080483eb即可,也就是跳过0x080483e4处的赋值语句,这样就变量 i
的值就没有改变,因此可达到实验要求的效果。怎么改呢?很明显,可以通过func函数的局部变量 i,取它的地址,就可以访问堆栈了,可以在gdb
中做这样的测试:

p /x *(&i) -----> 局部变量 i 的值
p /x *(&i+1) -----> 之前的栈帧地址
p /x *(&i+2) -----> 函数返回地址
p /x *(&i+3) -----> 函数参数


以下完整代码如下:

#include <stdio.h>

int func(int a)
{
int i = a;
*(&i+2) = *(&i+2)+7; /* 0x080483eb -0x080483e4 = 0x7 */
return i;
}

int main()
{
int i = 2;
func(i);
i = 1;
printf("i=%d\n", i);
return 0;
}


注:以上代码均在x86, Linux 2.6.34, gcc 4.2.3的环境中调试运行。