C|函数调用的栈帧机制与数组越界、缓冲区溢出
yuyutoo 2024-10-24 17:50 2 浏览 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 ....uwvw
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-
相关推荐
- jQuery VS AngularJS 你更钟爱哪个?
-
在这一次的Web开发教程中,我会尽力解答有关于jQuery和AngularJS的两个非常常见的问题,即jQuery和AngularJS之间的区别是什么?也就是说jQueryVSAngularJS?...
- Jquery实时校验,指定长度的「负小数」,小数位未满末尾补0
-
在可以输入【负小数】的输入框获取到焦点时,移除千位分隔符,在输入数据时,实时校验输入内容是否正确,失去焦点后,添加千位分隔符格式化数字。同时小数位未满时末尾补0。HTML代码...
- 如何在pbootCMS前台调用自定义表单?pbootCMS自定义调用代码示例
-
要在pbootCMS前台调用自定义表单,您需要在后台创建表单并为其添加字段,然后在前台模板文件中添加相关代码,如提交按钮和表单验证代码。您还可以自定义表单数据的存储位置、添加文件上传字段、日期选择器、...
- 编程技巧:Jquery实时验证,指定长度的「负小数」
-
为了保障【负小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【负小数】的方法。HTML代码<inputtype="text"class="forc...
- 一篇文章带你用jquery mobile设计颜色拾取器
-
【一、项目背景】现实生活中,我们经常会遇到配色的问题,这个时候去百度一下RGB表。而RGB表只提供相对于的颜色的RGB值而没有可以验证的模块。我们可以通过jquerymobile去设计颜色的拾取器...
- 编程技巧:Jquery实时验证,指定长度的「正小数」
-
为了保障【正小数】的正确性,做成了通过Jquery,在用户端,实时验证指定长度的【正小数】的方法。HTML做成方法<inputtype="text"class="fo...
- jquery.validate检查数组全部验证
-
问题:html中有多个name[],每个参数都要进行验证是否为空,这个时候直接用required:true话,不能全部验证,只要这个数组中有一个有值就可以通过的。解决方法使用addmethod...
- Vue进阶(幺叁肆):npm查看包版本信息
-
第一种方式npmviewjqueryversions这种方式可以查看npm服务器上所有的...
- layui中使用lay-verify进行条件校验
-
一、layui的校验很简单,主要有以下步骤:1.在form表单内加上class="layui-form"2.在提交按钮上加上lay-submit3.在想要校验的标签,加上lay-...
- jQuery是什么?如何使用? jquery是什么功能组件
-
jQuery于2006年1月由JohnResig在BarCampNYC首次发布。它目前由TimmyWilson领导,并由一组开发人员维护。jQuery是一个JavaScript库,它简化了客户...
- django框架的表单form的理解和用法-9
-
表单呈现...
- jquery对上传文件的检测判断 jquery实现文件上传
-
总体思路:在前端使用jquery对上传文件做部分初步的判断,验证通过的文件利用ajaxFileUpload上传到服务器端,并将文件的存储路径保存到数据库。<asp:FileUploadI...
- Nodejs之MEAN栈开发(四)-- form验证及图片上传
-
这一节增加推荐图书的提交和删除功能,来学习node的form提交以及node的图片上传功能。开始之前需要源码同学可以先在git上fork:https://github.com/stoneniqiu/R...
- 大数据开发基础之JAVA jquery 大数据java实战
-
上一篇我们讲解了JAVAscript的基础知识、特点及基本语法以及组成及基本用途,本期就给大家带来了JAVAweb的第二个知识点jquery,大数据开发基础之JAVAjquery,这是本篇文章的主要...
- 推荐四个开源的jQuery可视化表单设计器
-
jquery开源在线表单拖拉设计器formBuilder(推荐)jQueryformBuilder是一个开源的WEB在线html表单设计器,开发人员可以通过拖拉实现一个可视化的表单。支持表单常用控件...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)