C语言06-函数的调用过程II、数组

[亡魂溺海] 提交于 2020-01-13 18:20:18

函数调用过程

函数调用过程中的步骤:

  • 按照调用约定传参
  • 保存返回地址
  • 流程转移
  • 保存上一层栈帧地址
  • 开辟局部变量空间
  • 开始执行被调用函数的代码
    详细得展示各个步骤。

栈帧的概念

我们已经知道,因为每个函数被调用过程,那个函数的参数、局部变量、返回地址, 都会放在一断特定的栈区域中,并且每个被调用的函数,都对应了一段特定的栈区 域,那一段特定的栈区域,称为那一个被调用函数的栈帧

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;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!