一、认识栈帧
先来看一段神奇的代码:
(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编译的程序的栈帧存放规则不同)
转载于:https://blog.51cto.com/zhweizhi/1790120