一、认识栈帧
先来看一段神奇的代码:
(windows下,代码如下)
#include<stdio.h> #include<stdlib.h> #include<Windows.h> void fun() { printf("You Are Done\n"); Sleep(2000); printf("Suppose The Computer Will Shut Down~~~~\n"); //上面这行如果换成system("reboot")之类的呢? Sleep(2000); exit(1); } int fun1(int a, int b) { int *p = &a; p--; *p = fun; int c = 0xcccc; return c; } int main() { printf("begin run...\n"); int a = 0; int b = 1; fun1(a, b); printf("you should run here\n"); return 0; }
执行结果为:
linux下:
win下:
结果似乎都和我们预期的不一样啊
按理说,程序从main开始执行
中间调用fun1函数
调用完毕后应该继续执行下面的printf
然后输出:
you should run here
而实际上,程序最终却进入了fun函数
之所以这样,是因为栈帧的缘故。
如果你对上面发生的事情感到好奇,可以接着往下看
二、原理解释
关于栈帧,百度百科是这样解释的:
C语言中,每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。
也就是说,上面的代码,在内存方面可以这样理解:
简单解释一下,
我们知道C语言中函数中定义的变量是在栈上开辟的,这张图片就表示栈内存,
其地址从上往下表示从大到小
main函数中,先后将a b 入栈,
然后调用fun1(a, b)
图片中的这个fun1() 其实不准确,它应该是 返回地址
这个 返回地址 就是表示:执行fun1(a, b)完毕后,应该返回到这个地方接着执行main函数中剩下的代码。
此外,在fun1() 和 b 之间,还应该存放一个东西:栈指针ebp(图上没有表示出来)
然后参数 a b 是局部变量,也分别入栈,
借助调试工具,可以看到,a b 的地址分别为图中所示
然后定义一个指针p指向a
接下来p--,这时p指向的地址为0x0018fc20,也就是 刚刚说的 返回地址
这时候,应该能发现,返回地址已经变了, 变成了fun的地址
也就是说,当执行完fun1()后,程序并不会返回到 main函数中调用它的地方,而是接着调用fun函数
这就导致程序不可思议地进入了我们没有预想到的地方,调用了我们本不想调用的函数,
而且由于这个返回地址的丢失,在调用完毕fun后,程序也会因为找不到返回地址而挂掉。
(我在代码中执行了 exit(1); 这句话强行终止了程序)
以上就是代码的原理解释了。
接下来,利用刚刚所get到的栈帧方面的知识,可以做一个事情:
三、修改b的值
要求:不要直接修改a、b变量,而通过栈帧,实现修改a、b变量的值
代码:
void fun1(int a, int b) { int *p = &a; p -= 2; int ReturnAddr = *p; //返回值 //修改main中的a p = ReturnAddr - 4 * 2; *p = 11111; //修改main中的b p = ReturnAddr - 4 * 5; *p = 12345; } int main() { printf("begin run...\n"); int a = 0; int b = 1; fun1(a, b); printf("you should run here\n"); printf("%d\n", a); printf("%d\n", b); return 0; }
之前画的内存图比较简单,要想实现这个要求,必须进一步了解栈帧是怎么存放的了,下面是比较详细的栈内存图:
linux代码:
运行结果
(gcc 和 vs编译的程序的栈帧存放规则不同)