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

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

yuyutoo 2024-10-24 17:50 4 浏览 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-

相关推荐

Mysql和Oracle实现序列自增(oracle创建序列的sql)

Mysql和Oracle实现序列自增/*ORACLE设置自增序列oracle本身不支持如mysql的AUTO_INCREMENT自增方式,我们可以用序列加触发器的形式实现,假如有一个表T_WORKM...

关于Oracle数据库12c 新特性总结(oracle数据库19c与12c)

概述今天主要简单介绍一下Oracle12c的一些新特性,仅供参考。参考:http://docs.oracle.com/database/121/NEWFT/chapter12102.htm#NEWFT...

MySQL CREATE TABLE 简单设计模板交流

推荐用MySQL8.0(2018/4/19发布,开发者说同比5.7快2倍)或同类型以上版本....

mysql学习9:创建数据库(mysql5.5创建数据库)

前言:我也是在学习过程中,不对的地方请谅解showdatabases;#查看数据库表createdatabasename...

MySQL面试题-CREATE TABLE AS 与CREATE TABLE LIKE的区别

执行"CREATETABLE新表ASSELECT*FROM原表;"后,新表与原表的字段一致,但主键、索引不会复制到新表,会把原表的表记录复制到新表。...

Nike Dunk High Volt 和 Bright Spruce 预计将于 12 月推出

在街上看到的PandaDunk的超载可能让一些球鞋迷们望而却步,但Dunk的浪潮仍然强劲,看不到尽头。我们看到的很多版本都是为女性和儿童制作的,这种新配色为后者引入了一种令人耳目一新的新选择,而...

美国多功能舰载雷达及美国海军舰载多功能雷达系统技术介绍

多功能雷达AN/SPY-1的特性和技术能力,该雷达已经在美国海军服役了30多年,其修改-AN/SPY-1A、AN/SPY-1B(V)、AN/SPY-1D、AN/SPY-1D(V),以及雷神...

汽车音响怎么玩,安装技术知识(汽车音响怎么玩,安装技术知识视频)

全面分析汽车音响使用或安装技术常识一:主机是大多数人最熟习的音响器材,有关主机的各种性能及规格,也是耳熟能详的事,以下是一些在使用或安装时,比较需要注意的事项:LOUDNESS:几年前的主机,此按...

【推荐】ProAc Response系列扬声器逐个看

有考牌(公认好声音)扬声器之称ProAcTablette小音箱,相信不少音响发烧友都曾经,或者现在依然持有,正当大家逐渐掌握Tablette的摆位设定与器材配搭之后,下一步就会考虑升级至表现更全...

#本站首晒# 漂洋过海来看你 — BLACK&amp;DECKER 百得 BDH2000L无绳吸尘器 开箱

作者:初吻给了烟sco混迹张大妈时日不短了,手没少剁。家里有了汪星人,吸尘器使用频率相当高,偶尔零星打扫用卧式的实在麻烦(汪星人:你这分明是找借口,我掉毛是满屋子都有,铲屎君都是用卧式满屋子吸的,你...

专题|一个品牌一件产品(英国篇)之Quested(罗杰之声)

Quested(罗杰之声)代表产品:Q212FS品牌介绍Quested(罗杰之声)是录音监听领域的传奇品牌,由英国录音师RogerQuested于1985年创立。在成立Quested之前,Roger...

常用半导体中英对照表(建议收藏)(半导体英文术语)

作为一个源自国外的技术,半导体产业涉及许多英文术语。加之从业者很多都有海外经历或习惯于用英文表达相关技术和工艺节点,这就导致许多英文术语翻译成中文后,仍有不少人照应不上或不知如何翻译。为此,我们整理了...

Fyne Audio F502SP 2.5音路低音反射式落地音箱评测

FyneAudio的F500系列,有新成员了!不过,新成员不是新的款式,却是根据原有款式提出特别版。特别版产品在原有型号后标注了SP字样,意思是SpecialProduction。Fyne一共推出...

有哪些免费的内存数据库(In-Memory Database)

以下是一些常见的免费的内存数据库:1.Redis:Redis是一个开源的内存数据库,它支持多种数据结构,如字符串、哈希表、列表、集合和有序集合。Redis提供了快速的读写操作,并且支持持久化数据到磁...

RazorSQL Mac版(SQL数据库查询工具)

RazorSQLMac特别版是一款看似简单实则功能非常出色的SQL数据库查询、编辑、浏览和管理工具。RazorSQLformac特别版可以帮你管理多个数据库,支持主流的30多种数据库,包括Ca...

取消回复欢迎 发表评论: