基础知识备用:
栈帧的定义:为单个过程分配的那部分栈。
栈帧的作用:传递过程参数;存储返回信息;保存寄存器内容用于以后恢复;本地存储。
栈底指针(帧指针):ebp寄存器。
栈顶指针(栈指针):esp寄存器。
栈的生长方向:向低地址方向增长。
调用者的栈帧存储内容:
A:被调用者的参数。
B:调用者的返回地址。
被调用者的栈帧存储内容是:
A:从保存ebp的值开始的。
在被调用过程中第一个参数的位置放在相对于ebp偏移量为8的位置处。(先存储参数,再存储返回地址,最后是ebp的值,返回地址是四个字节,参数是四个字节)。
call的作用:(函数调用时)
A:将返回地址入栈。
B:跳转到被调用函数过程的起始过程。
以上内容参考《深入理解计算机组成系统》。
实践部分:
#include<stdio.h>
#include<windows.h>
//隐式调用函数改变函数的返回值。
void Debug()
{
printf("哈哈哈哈,修改main函数的返回值!\n");
Sleep(3000);
}
int *p_resever = NULL;
//通过栈帧结构修改参数的值。(函数内部局部变量和参数)
int fun(int x, int y)
{
int c = 30;
int *p_y = &x;//修改形参的内容。
int *p_c = &x;//修改局部变量的内容。
int *p_ret = &x;//修改函数的返回值。
p_ret--;
*p_resever = *p_ret;
*p_ret =Debug;
p_c = p_c - 4;
*p_c = 66;
p_y++;
*p_y = 66;
printf("y的值:%d\n", y);
printf("c的值:%d\n", c);
return c;
}
int main()
{
int a = 20;
int b = 10;
//调用函数
int ret = fun(a, b);
printf("you should running here!\n");
//平衡栈帧
_asm{
sub esp,4
}
system("pause");
return 0;
}
A:建立函数的栈帧
建立main函数的栈帧结构如图红色部分所示,此时红色的ebp表示main 函数栈帧的栈底,esp表示main函数栈帧的栈顶。
j建立fun函数的栈帧结构:使esp先减二,后压入mian函数ebp的内容。然后让esp的内容覆盖ebp的内容,esp减去某个值,给fun函数
开辟栈帧结构。
B:局部变量的存储
局部变量存储有两种方式:第一种是存储在调用函数的栈帧结构中,第二种是存储在被调用函数的栈帧结构中,并且在返回前恢复该值。
C:修改形参的值。
原理:由于形参实例化的时候,第一个参数是最后一个入栈的,所以第一个参数的地址向高地址的方向分别是各个形参的内容。所以通过第一个形参便可以修改函数的形参值。
D:修改函数的返回值
原理:由于形参实例化的时候,第一个参数是最后一个入栈的,所以第一个参数的地址向低地址的方向分别是,main函数栈帧的栈顶(main的返回值),和fun函数栈帧的栈底(
ebp的内容)。所以通过第一个形参便可以修改函数的返回值。
作用:可以隐式的调用函数。由于函数没有被显示调用,所以没有形成Debug函数的栈帧结构。
修改函数的返回值缺陷:由于修改的是main 函数的返回值,所以程序在释放栈帧结构,返回程序的时候,由于Debug函数并不知道返回值在哪里,所以无法返回到main函数中,导致程序出错。
第一层改进方法:当在修改函数的返回值时,可以使用一个全局变量去保存修改前函数的返回值。然后在Debug函数中设置一个变量,(在vs2013下第一个变量和函数返回值之间相差16个字节)通过这个变量可以找到Debug函数的返回值,将Debug函数的返回值修改成全局变量的内容,便可以时程序重新回到main函数中!
第一层改进缺陷:在函数执行完之后依然会报错。
理由:因为Debug函数并没有调用,所以没有保存返回值,但是却返回了保存值,也就是push的内容比pop的内容少了一次。所以栈帧结构上移了
4个字节。
第二层改进方法:采用平衡栈帧的方法。
_asm{
sub esp 4
}
E:函数调用结束。
函数调用结束,局部变量被释放。ebp的值被弹出,ebp回到main函数,程序返回到保存的返回值所在的地址处。main函数中的局部变量被释放,esp回到局部变量(main局部变量)之前。