1、认识栈帧linux
先来看一段神奇的代码:windows
(windows下,代码以下)ide
#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下:spa
结果彷佛都和咱们预期的不同啊3d
按理说,程序从main开始执行指针
中间调用fun1函数调试
调用完毕后应该继续执行下面的printform
而后输出:
you should run here
而实际上,程序最终却进入了fun函数
之因此这样,是由于栈帧的缘故。
若是你对上面发生的事情感到好奇,能够接着往下看
2、原理解释
关于栈帧,百度百科是这样解释的:
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到的栈帧方面的知识,能够作一个事情:
3、修改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编译的程序的栈帧存放规则不一样)