函数调用过程
函数调用过程中的步骤:
- 按照调用约定传参
- 保存返回地址
- 流程转移
- 保存上一层栈帧地址
- 开辟局部变量空间
- 开始执行被调用函数的代码
详细得展示各个步骤。
栈帧的概念
我们已经知道,因为每个函数被调用过程,那个函数的参数、局部变量、返回地址, 都会放在一断特定的栈区域中,并且每个被调用的函数,都对应了一段特定的栈区 域,那一段特定的栈区域,称为那一个被调用函数的栈帧。
void Fun2(int arg)
{
int nValue = 0;
printf("fun2:%08X, %p\r\n", &arg, &nValue);
}
void Fun1(int arg)
{
int nValue = 0;
Fun2(0x22222222);
printf("fun1:%08X, %p\r\n", &arg, &nValue);
}
int main(int argc, char* argv[])
{
printf("main:%p\r\n", &argc);
Fun1(0x11111111);
return 0;
}
通过以上代码可以验证,紧挨着返回地址的“栈帧”,确实是指向了上一层调用方的栈帧,这种设计,使得当前函数结束后,程序流程都可以顺利地找到上一层栈帧。
通过以上的图和代码实验,也可以很容易的理解,为什么局部变量,即使同名,也不 能跨函数使用。
##函数调用的返回过程
当函数运行结束,或者遇到return时,会进行函数返回。
函数返回的过程,其实是函数调用的逆过程。
- 释放栈空间
- 从栈中,获取上一层栈帧的地址,恢复到上一层栈帧(可以通过监视窗口监视 esp寄存器观察)
- 从栈中,获取返回地址,并且转移流程到该地址(我们可以通过调试时,手工修 改栈中的返回地址验证这个事情。)
- 将返回值保存在eax中(之后调用方会从eax中取返回值)
- 平衡栈
##【小练习】
如下是一个密码验证的程序:
用户有3次机会输入密码,如果密码正确,则提示正确。如果密码错误三次,则打印错 误信息,结束程序。
以下程序,可以暴露出与栈有关的安全问题:
void repassword() //密码验证函数
{
char szPassword[16] = { 0 };
for (int time = 1; time < 4; time++)
{
printf("请输入密码(您仅有三次机会):");
scanf("%s", &szPassword);
if (strcmp(szPassword, "123") == 0)
{
printf("密码正确!"); //flag = 1;
break;
}
else
{
printf("密码错误,您还有%d次机会,", 3 ‐ time); //flag = 0;
}
if (time >= 3)
{
printf("密码错误三次"); //flag = 0;
}
}
}
int main(int argc, char* argv[])
{
repassword();
return 0;
}
以上,如果我们输入内容过多(超出16个字节),可以覆盖到返回地址处的内存,这样,就可以控制程序流程到自己构造的代码处。
如果精心构造输入内容,不仅可以改变流程,还可以决定改变流程后的功能。 这就是栈溢出漏洞(现在几乎不可能有了)。
欲知详细内容,可以搜索shellcode。
数组
数组的基本用法:
int main(int argc, char* argv[])
{
int nAry[5] = { 0, 1, 2, 3, 4 };//数组的定义
printf("%d\r\n", nAry[1]);//数组的引用
nAry[2] = 100;//数组成员的赋值
return 0;
}
关于数组的定义的特点(C89标准):
- 必须要声明指定固定长度
- 从语法角度而言,数组的引用下标可以越界
数组的两个特点: - 一致性:所有数组成员的类型必须是一致的
- 连续性:所有数组成员在内存中是连续存放的
##数组的寻址公式
数组要追求一致性和连续性,是由其寻址公式决定的。
int main(int argc, char* argv[])
{
int nAry[4] = { 0x0000,0x1111,0x2222,0x3333 };
printf("%p\r\n", &nAry[0]);
printf("%p\r\n", &nAry[16]);
printf("%p\r\n", &nAry[-3]);
printf("%p\r\n", &4[nAry]);
return 0;
}
所谓的寻址公式,就是:去确认Ary数组中,第i个元素的地址。从C语言的表达式的角度看,就是:
&Ary[i]
前期准备:
数组名其实就是数组的首地址。
//以下两行等价
printf("%p\r\n", &nAry[0]);
printf("%p\r\n", nAry);
数组引用的细节过程,如果:
printf("%p",nAry[5]);
nAry[5]的值其实经历以下两步被取出:
- 通过数组寻址公式,找到nAry[5]的内存地址
- 将该内存地址出的内容取出
可以通过以下代码验证:
int main(int argc, char* argv[])
{
int nAry[4] = { 0x0000,0x1111,0x2222,0x3333 };
printf("%p\r\n", &nAry[-3]);
return 0;
}
所以,寻址nAry[i]就变得非常重要,在编译器内部,其实是通过非常简单的公式
达成目的:
nAry[i]的地址 = nAry(首地址) + i * sizeof(数组成员的类型)
sizeof
sizeof是C语言中的一个运算符(地位是关键字级别),它的作用是计算类型或者变量的字节数。
int main(int argc, char* argv[])
{
int nAry[4] = { 0x0000,0x1111,0x2222,0x3333 };
printf("%d,%d,%d,%d,%d\r\n",
sizeof(int),
sizeof(short),
sizeof(char),
sizeof(double),
sizeof(float)
);
printf("%p\r\n", sizeof(nAry));
return 0;
}
来源:CSDN
作者:未北、
链接:https://blog.csdn.net/weixin_43365952/article/details/103957925