百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

C|函数调用的栈帧机制与数组越界、缓冲区溢出

yuyutoo 2024-10-24 17:50 3 浏览 0 评论

0 前置知识

0.1 程序加载和数据存储

程序运行前要将代码加载到内存的代码区,包括全局变量和静态变量也要同时加载。

堆区内存可以在程序运行时动态申请。

栈区是由程序重复利用的存储区域,通过两个寄存器ebp和esp存储栈区的相对地址来控制栈区空间的重复使用。函数调用时,开辟一个函数需要的栈区空间,称为一个栈帧。函数返回时,偏移ebp和esp的值,让栈空间可以重新使用此前函数调用占用的空间。

以下是windows平台一个程序运行时的内存分配机制:

0.2 函数调用

函数调用看似简单,其实是一个挺复杂的过程。当涉及到逐级调用时,代码需要能够回到最初的调用点。

例如一个人旅行,当经过若干十字路口时,如何正确回到最初开始位置?一个可行的方案是,每经过一个路口,用一张扑克牌记下当时路口的状态,每经过一个路口即使用一张牌,并放在已使用的扑克牌上面,当返回时,依次从上面拿牌读取信息即可回溯到原来位置。这种扑克牌的放与取就是所谓的栈的“后进先出”机制。

函数调用也要使用类似的机制,由编译器完成。每一级函数调用对应一个栈帧(如同扑克牌),记录参数值,返回地址,一些寄存器状态,局部变量值。主调函数与被调函数如何分工完成这些任务,不同的调用约定有不同的分工。

0.3 数组名在一定的上下文中转换为指向数组首元素的指针

数组也是如此,看似简单,实则复杂。

数组名在一定上下文中会转换为一个指向数组首元素的指针,为什么会这样,当然有其合理性。

首先了解一下指针的算术运算。(栈内存空间地址是逆增长的)

void foo(int a)
{
    int f = 45;
    int *ptr = &f;
    *(ptr+1) = 12;  // 编译通过,运行时错误,试图修改ebp
    *(ptr+2) = 34;  // 编译通过,运行时错误,试图修改函数返回地址
    *(ptr+3) = 56;  // 试图修改保存函数实参值的栈内存单元
    int *p = (int*)malloc(sizeof(int)*5);
    *(p+3) = 23;    // p[3] = 23;
}

p+n,是一个相对于ptr的偏移n个int地址空间的地址值。

编译器会计算p+sizeof(int),至于偏移的是否是有效合法的地址,C编译器不管。

ptr+n也是如此。

如有数组int arr[64] = {0};

数组名是一个基地址,其索引表示地址的偏移量。arr[i]写法是指针算术运算*(arr+i)的语法糖。编译器要考虑指针的算术运算有实用的意义,C编译器的规定是arr+i,其地址不是简单偏移 i 个字节,这样没有意义,而是sizeof(指向的类型)的长度的字节数,这样才有意义,其计算由编译器完成,当指针类型是数组时,偏移 i 个数组的长度没有任何意义,偏移 i 个数组元素的长度才有实际意义,这也是C编译器的规定。如:

    int a[3][4][5]; // a的类型是int[3][4][5],元素的类型是int[4][5],int (*p)[4][5] = a;
    a + 2;      // 偏移的地址是a + 2* sizeof(int)*4*5
    int b[4][5]; // b的类型是int[4][5],元素的类型是int[5],int (*p)[5] = b;
    b + 2;      // 偏移的地址是b + 2* sizeof(int)*5
    int c[5];// c的类型是int[5],元素的类型是int,int *p = c;
    c + 2;      // 偏移的地址是b + 2* sizeof(int)

C编译器只负责偏移地址的计算,对于数组是否越界,地址是否合法并不在编译时检查。

arr[i]的 i 可以是任意signed int,越界时就会访问到相邻栈内存。

0.4 一些简单的汇编代码

1、mov——传送指令

定义:把一个字节、字或双字的操作数从源位置传送到目的位置,可以实现立即数到通用寄存器或主存的传送,通用寄存器与通用寄存器、主存或段寄存器之间的传送,主存与段寄存器之间的传送。

举例:mv ebp, esp

解释:相当于C语言中赋值语句=,ebp=esp

2、push——进栈指令

定义:进栈指令push先将ESP减小作为当前栈顶,然后可以将立即数、通用寄存器和段寄存器或存储器操作数传送到当前栈顶。

格式:push src

举例:push ebp

解释:相当于C语言中esp+=4, *esp=ebp

作用:为ebp当前存放的地址,在栈顶开辟空间存入它,用作调用子函数时的现场保护

3、pop——出栈指令

定义:与入栈指令相反,它先将栈顶的数据传送到通用寄存器、存储单元或段寄存器中,然后ESP增加作为当前栈顶。

格式:pop src

举例:pop ebp

解释:相当于C语言中 ebp=*esp, esp+=4

作用:调用子函数结束后,恢复主函数的ebp

4、add——加法指令

格式:add dest, src

解释:相当于dest+=src

5、sub——减法指令

格式:sub dest, src

解释:相当于dest-=src

6、call——函数调用指令

格式:call 函数名

作用:(1)将程序当前执行的位置IP压入堆栈中;(2)转移到调用的子程序。

其它:

RET 指令从堆栈把返回地址弹回到指令指针寄存器。ret 相当于 pop EIP。

rep stos dword ptr [edi] //rep是重复其上面的指令,ECX是重复的次数。

ILT是INCREMENTAL LINK TABLE的缩写,这个@ILT其实就是一个静态函数跳转的表,它记录了一些函数的入口然后跳过去,每个跳转jmp占一个字节,然后就是一个四字节的内存地址,加起为五个字节。

LEA(Load Effective Address )取有效地址指令,取源操作数地址的偏移量,并把它传送到目的操作数所在的单元

0.5 函数栈

0.5.1 栈空间增长方式:从高地址向低地址扩展,是一片让程序重复利用的数据空间;

栈空间由编译器维护(需要在Debug模式下才可以跟踪);

0.5.2 栈空间对齐方式:X86按4个字节来对齐,X64按8个字节来对齐;

0.5.3 两个跟踪栈空间的寄存器ESP和EBP

对栈的操作由ESP跟踪,用来指示栈顶。push、call时,esp -= 4,pop、ret时,esp += 4

EBP用来引用函数参数和局部变量。EBP相当于一个“基准指针”。从主调函数传递到被调函数的参数以及被调函数本身的局部变量都可以通过这个基准指针为参考,加上偏移量找到。

*ebp(表示栈地址ebp对应的值) = 上一个ebp的值(栈地址);

ebp-4 = 第一个局部变量地址;

ebp+4 = 函数返回地址;

ebp+8 = 函数第一个参数地址;

0.5.4 不同的函数调用约定,在参数的入栈顺序,堆栈的恢复(由caller还是callee负责)、函数的命名上会有所不同。

0.5.5 一个完整的函数帧包括:函数参数、返回地址,上一个EBP的值,局部变量空间,3个寄存器。在函数内部代码执行前,一个完整的函数帧已经建立。

下面通过一个完整实例来理解函数调用的栈帧机制与数组向栈底方向越界的分析:

看以下代码:

#include <stdio.h>
int arrayBound(int a)
{
    int b = -1;         // [ebp-4]
    int arr[98] = {0};  // [ebp-18Ch]
    int c = 1;          // [ebp-190h],栈是逆增长,栈顶地址值>栈顶
    arr[-1] = c*a;      // 数组向栈顶方向越界访问,arr[-1]对应int c
    arr[98] = b*a;      // 数组向栈底方向越界访问,arr[98] 对应int b
    //printf("%d %d\n",b,c); // -5 5
    return b;
}
int main()
{
    int e = arrayBound(5); // -5
    //printf("%d\n",e);
    return 0;
}

主函数main()的栈帧:

主函数main()调用arraybound(),此时的汇编代码:

15:       int e = arrayBound(5); // -5
0040D4D8   push        5
0040D4DA   call        @ILT+10(arrayBound) (0040100f)
0040D4DF   add         esp,4
0040D4E2   mov         dword ptr [ebp-4],eax
16:       //printf("%d\n",e);
17:       return 0;
0040D4E5   xor         eax,eax
18:   }

1 主调函数调用被调函数时函数参数压栈

此时的栈指针值:

ebp 0x0012ff48 // ebp是栈底指针

esp 0x0012fef8 // esp是栈底指针,栈的push和pop操作会同时改变esp的值(esp移动)

此时的栈顶指针附近的内存映像:

0012FEEC 30 2F 42 00 83 00 00 00 68 20 1F 00 0/B.....h ..

0012FEF8 00 00 00 00 00 00 00 00 00 F0 FD 7F .........瘕.

0012FF04 CC CC CC CC CC CC CC CC CC CC CC CC 烫烫烫烫烫烫

执行汇编:

0040D4D8   push        5  // esp += 4 = 0x0012fef4 , *esp = 5

栈帧内存:

0012FEEC 30 2F 42 00 83 00 00 00 05 00 00 00 0/B.........

2 返回地址压栈

汇编指令call对应两个操作:push 返回地址和jmp指令。

0040D4DA   call        @ILT+10(arrayBound) (0040100f) 

0012FEEC 30 2F 42 00 DF D4 40 00 05 00 00 00 0/B.咴@..... // 返回地址是0040D4DF

esp = 0x0012fef0,*esp = 0040D4DF

0040100F   jmp         arrayBound (0040d820)

代码跳转:

0040D820   push        ebp
0040D821   mov         ebp,esp
0040D823   sub         esp,1D0h
0040D829   push        ebx
0040D82A   push        esi
0040D82B   push        edi
0040D82C   lea         edi,[ebp-1D0h]
0040D832   mov         ecx,74h
0040D837   mov         eax,0CCCCCCCCh
0040D83C   rep stos    dword ptr [edi]
5:        int b = -1;         // [ebp-4]
0040D83E   mov         dword ptr [ebp-4],0FFFFFFFFh

ebp值压栈:

0040D820   push        ebp // ebp赋值前先压栈保存先前状态 esp = 0x0012feec, *esp = ebp

0012FEEC 48 FF 12 00 DF D4 40 00 05 00 00 00 H...咴@.....

0040D821   mov         ebp,esp // ebp = esp = 0x0012feec

栈区:

3 函数栈帧空间分配

0040D823   sub         esp,1D0h // 1D0h = 464 = 400+64 , esp = 0x0012fd1c

此时栈顶指针附近的内存随机值:

0012FD10 FE FF FF FF FE 60 75 77 76 A3 71 77 ....uwvw

0012FD1C 00 00 1F 00 63 01 00 50 D3 5D 6E 77 ....c..P覿nw

0012FD28 CF 79 14 75 00 00 00 00 00 00 00 00 蟳.u........

3.1 寄存器压栈

寄存器状态保持(压栈)

0040D829   push        ebx
0040D82A   push        esi
0040D82B   push        edi  // esp = 0012FD10, *esp = edi

栈内存:

0012FD10 48 FF 12 00 00 00 00 00 00 F0 FD 7F H........瘕.

此时esp = 0x0012fd10,三个寄存器使用的栈内存是464个字节以外的栈内存。

栈区:

3.2 栈帧分配的空间每个字节全部置为0xCC

0040D82C   lea         edi,[ebp-1D0h]
0040D832   mov         ecx,74h
0040D837   mov         eax,0CCCCCCCCh
0040D83C   rep stos    dword ptr [edi]

此时esp和ebp之间的栈空间:

0012FD10 48 FF 12 00 00 00 00 00 00 F0 FD 7F H........瘕.

0012FD1C CC CC CC CC CC CC CC CC CC CC CC CC 烫烫烫烫烫烫

……

0012FEE0 CC CC CC CC CC CC CC CC CC CC CC CC 烫烫烫烫烫烫

0012FEEC 48 FF 12 00 DF D4 40 00 05 00 00 00 H...咴@.....

3.3 栈空间为局部变量从ebp处开始偏移进行初始化操作

5:        int b = -1;         // [ebp-4]
0040D83E   mov         dword ptr [ebp-4],0FFFFFFFFh

此时的栈空间:

0012FEE0 CC CC CC CC CC CC CC CC FF FF FF FF 烫烫烫烫....

0012FEEC 48 FF 12 00 DF D4 40 00 05 00 00 00 H...咴@.....

汇编代码继续:

6:        int arr[98] = {0};  // [ebp-18Ch]
0040D845   mov         dword ptr [ebp-18Ch],0
0040D84F   mov         ecx,61h
0040D854   xor         eax,eax
0040D856   lea         edi,[ebp-188h]
0040D85C   rep stos    dword ptr [edi]
7:        int c = 1;          // [ebp-190h],栈是逆增长,栈顶地址值>栈顶
0040D85E   mov         dword ptr [ebp-190h],1

此时的栈空间:

0012FD50 CC CC CC CC CC CC CC CC CC CC CC CC 烫烫烫烫烫烫

0012FD5C 01 00 00 00 00 00 00 00 00 00 00 00 ............

0012FD68 00 00 00 00 00 00 00 00 00 00 00 00 ............

8: arr[-1] = c*a; // 数组向栈顶方向越界访问

0040D868   mov         eax,dword ptr [ebp-190h]
0040D86E   imul        eax,dword ptr [ebp+8]
0040D872   lea         ecx,[ebp-18Ch]
0040D878   mov         dword ptr [ecx-4],eax

此时的栈空间:

0012FD50 CC CC CC CC CC CC CC CC CC CC CC CC 烫烫烫烫烫烫

0012FD5C 05 00 00 00 00 00 00 00 00 00 00 00 ............

0012FD68 00 00 00 00 00 00 00 00 00 00 00 00 ............

9: arr[98] = b*a; // 数组向栈底方向越界访问

0040D87B   mov         edx,dword ptr [ebp-4]
0040D87E   imul        edx,dword ptr [ebp+8]
0040D882   mov         dword ptr [ebp-4],edx

此时的栈空间:

0012FEDC 00 00 00 00 00 00 00 00 00 00 00 00 ............

0012FEE8 FB FF FF FF 48 FF 12 00 DF D4 40 00 ....H...咴@.

0012FEF4 05 00 00 00 00 00 00 00 00 00 00 00 ............

栈区:

3.4 值返回

10:       //printf("%d %d\n",b,c); // -5 5
11:       return b;
0040D885   mov         eax,dword ptr [ebp-4] // 返回值保存在寄存器eax中

3.5 寄存器状态恢复

此时 esp = 0x0012fd10

0040D888   pop         edi   //esp += 4, edi = *esp
0040D889   pop         esi
0040D88A   pop         ebx

以上的栈空间是栈帧空间464以外的空间。

此时 esp = 0x0012fd1c

ebp = 0x0012feec

0040D88B   mov         esp,ebp  // 栈顶指针更新,栈空间回收

此时ebp对应的栈空间

0012FEEC 48 FF 12 00 DF D4 40 00 05 00 00 00 H...咴@.....

0040D88D   pop         ebp

此时 esp = 0x0012fef0

ebp = 0x0012ff48

0040D88E   ret

函数返回:

0040D4DF   add         esp,4 // 4是实参压栈时使用的字节数。如果压了3个int,则是ch
0040D4E2   mov         dword ptr [ebp-4],eax

0012FF3C CC CC CC CC CC CC CC CC FB FF FF FF 烫烫烫烫....

0012FF48 88 FF 12 00 F9 11 40 00 01 00 00 00 ......@.....

ebp-4是主调函数局部变量e的栈内存保存位置。

如果函数的返回值是一个复合类型,超过了两个寄存器所能容纳的大小,会在主调函数的栈帧开辟空间,以供值返回。

4 数组越界访问

向栈底方向越界1个int空间:对应局部变量int b;

向栈底方向越界2个int空间:对应ebp本身;

向栈底方向越界3个int空间:对应函数返回地址[ebp+4];

向栈底方向越界4个int空间:对应函数实参存储位置[ebp=8]:

#include <stdio.h>

void arrayBound(int a)
{
    int b = -1;
    int arr[98] = {0};
    int c = 1;
    arr[98] = a;
    arr[101] = 555;
    b=a;
    printf("%d\n",b); // 555
}
int main()
{
    int c = 5;
    arrayBound(c);
    printf("%d\n",c); // 5
    while(1);
    return 0;
}

向栈底方向越界3个int空间:对应函数返回地址:

arr[100] = 0040D4DF; // 数组向栈底方向越界访问

当arr[100] 被赋的值是一个合法的内存地址时,正常运行,否则,运行出错。

5 缓冲区溢出

有如下代码:

#include <cstdio>
void func()
{
    char buff[4] = {0};
    printf("some input:");
    gets(buff);
    puts(buff);
}
int main()
{
    func();
    return 0;
}

当运行到gets(buff)时的栈区:

如果输入"abc",则刚好填充buff,其中buff[3] = '\0'

如果输入“abcdefg”,则"efg"会填充[ebp]指向的值。

如果输入“abcdefghijk”,则“ijk”会填充[ebp+4]的值,也就是函数func()的返回地址。

-End-

相关推荐

野路子科技!2步教你把手机改造成一个FTP服务器,支持PC互传

哈喽,大家好,我是野路子科技,今天来给大家带来一个教程,希望大家喜欢。正如标题所言,就是教大家如何把售价改造成FTP服务器,而这个时候估计有朋友会问了,把手机改造成FTP服务器有什么用呢?现在有Q...

不得不看:别样于Server-U的群晖文件存储服务器的搭建与使用

我先前的作品中,有着关于Server-U的ftp文件存储服务器的搭建与访问的头条文章和西瓜视频,而且我们通过各种方式也给各位粉丝介绍了如何突破局域网实现真正意义上的公网访问机制技术。关于Server-...

Qt三种方式实现FTP上传功能_qt引入qftp库

FTP协议FTP的中文名称是“文件传输协议”,是FileTransferProtocol三个英文单词的缩写。FTP协议是TCP/IP协议组中的协议之一,其传输效率非常高,在网络上传输大的文件时,经...

Filezilla文件服务器搭建及客户端的使用

FileZilla是一个免费开源的FTP软件,分为客户端版本和服务器版本,具备所有的FTP软件功能。可控性、有条理的界面和管理多站点的简化方式使得Filezilla客户端版成为一个方便高效的FTP客户...

美能达柯美/震旦复印机FTP扫描怎么设置?

好多网友不知道怎么安装美能达/震旦复印机扫描,用得最多是SMB和FTP扫描,相对于SMB来说,FTP扫描安装步骤更为便捷,不容易出问题,不需要设置文件夹共享,所以小编推荐FTP来扫描以美能达机器为例详...

CCD(简易FTP服务器软件)_简单ftp服务器软件

CCD简易FTP服务器软件是一款很方便的FPT搭建工具,可以将我们的电脑快速变成一个FPT服务器。使用方法非常简单,只要运行软件就会自动生效,下载银行有该资源。该工具是不提供操作界面的,其他用户可以输...

Ubuntu系统搭建FTP服务器教程_ubuntu架设服务器

在Ubuntu系统上搭建FTP服务器是文件传输的一个非常实用方法,适合需要进行大量文件交换的场景。以下是一步步指导,帮助您在Ubuntu上成功搭建FTP服务器。1.安装vsftpd软件...

理光FTP扫描设置教程_理光ftp扫描设置方法

此教程主要用来解决WIN10系统下不能使用SMB文件夹扫描的问题,由于旧的SMB协议存在安全漏洞,所以微软在新的系统,WIN8/WIN10/SERVER201220162018里使用了新的SMB传...

纯小白如何利用wireshark学习网络技术

写在前面工欲善其事必先利其器!熟悉掌握一种神器对以后的工作必然是有帮助的,下面我将从简单的描述Wireshark的使用和自己思考去写,若有错误或不足还请批评指正。...

京东买13盘位32GB内存NAS:NAS系统安装设置教程

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:yasden你没有看错,我在京东自营商城购买硬件,组装了一台13盘位,32GB内存的NAS,硬盘有13个盘位!CPU是AMD的5500!本文...

FileZilla搭建FTP服务器图解教程_filezilla server搭建ftp服务器

...

python教程之FTP相关操作_python ftps

ftplib类库常用相关操作importftplibftp=ftplib.FTP()ftp.set_debuglevel(2)#打开调试级别2,显示详细信息ftp.connect(“I...

xftp怎么用,xftp怎么用,具体使用方法

Xftp是一款界面化的ftp传输工具,用起来方便简单,这里为大家分享下Xftp怎么使用?希望能帮到有需要的朋友。IIS7服务器管理工具可以批量管理、定时上传下载、同步操作、数据备份、到期提醒、自动更新...

树莓派文件上传和下载,详细步骤设置FTP服务器

在本指南中,详细记录了如何在树莓Pi上设置FTP。设置FTP可以在网络上轻松地将文件传输到Pi上。FTP是文件传输协议的缩写,只是一种通过网络在两个设备之间传输文件的方法。还有一种额外的方法,你可以用...

win10电脑操作系统,怎么设置FTP?windows10系统设置FTP操作方法

打印,打印,扫描的日常操作是每一个办公工作人员的必需专业技能,要应用FTP作用扫描文件到电脑上,最先要必须一台可以接受文件的FTP服务器。许多软件都需要收费标准进行,但人们还可以应用Windows的系...

取消回复欢迎 发表评论: