系统栈的工作原理
1.内存的不同用途
简单来说,缓冲区溢出就是在大缓冲区的数据复制到小缓冲区中,由于没注意小缓冲区的边界,”撑爆“了小缓冲区。从而冲掉了小缓冲区相邻内存区域的数据。
根据不同的操作系统,一个进程可能被分配到不同内存区域中去执行,但是不管什么样的系统,什么计算机架构,进程使用的内存都可以按照功能分为4部分:
代码区:可执行指令
数据区:用于存储全局变量
堆区: 进程可以在堆区动态的请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点
栈区: 用于动态的存储函数之间的调用关系,以保证被调用函数返回时恢复到母函数中继续执行
这只是简单的内存划分,如果想了解关于内存更详细的论述,请参考《深入理解计算机系统》,windows下,PE文件代码段中包含的二进制机器代码会被装入内存的代码区,处理器将到这里一条一条的取出指令和操作数,送入算术逻辑单元运算,如果代码请求开辟动态内存,则会在内存的堆区分配一块区域返回给代码区的代码使用,当函数调用发生时,函数的调用关系等信息会动态的保存在栈区。
程序中所使用的缓冲区可以是在堆区、栈区、数据区,不同地方的缓冲区利用方法不同。
2.栈与系统栈
栈是一种数据结构,是一种先进后出的数据表,用于标识栈的属性两个:栈顶、栈底 。内存中的栈区指的就是系统栈,由系统自动维护。
3.函数调用时发生了什么
请看如下C代码:
int B(int b1,int b2){ int var_b1=b1+b2; int var_b2=b1-b2; return var_b1*var_b2; } int A(int a1,int a2){ int var_a1; var_a=B(a1,a2)+a1; return a1; } int main(int argc,char **argv,char **envp){ int var_main; var_main=A(4,3); return var_main; }
根据操作系统的不同、编译器和编译选项的不同,同一文件不同函数的代码在内存代码区中的分布可能相邻,也可能不相邻,可能有先后顺序,也可能没有,但他们都是在代码所映射的节里
函数调用时,伴随的系统栈中的操作如下:
在main函数调用A的时候,首先在自己的栈帧中压入函数返回地址,然后位A创建新栈帧并压入系统栈 ,
在函数A调用B的时候,同样先在自己的栈帧中压入返回地址,然后为B创建新栈帧并压入系统栈
B返回时,B的栈帧被弹出系统栈,A栈帧的返回地址被露在栈顶,处理器跳到返回地址处执行
在A返回时,A的栈帧被弹出系统栈,main函数栈帧中的返回地址被露在栈顶。处理器跳到返回地址执行
4.寄存器与函数栈帧
每一个函数独占自己的栈帧空间,当前正在运行的函数总是在栈顶,win32系统提供两个寄存器用于标识位于系统栈顶端的栈帧
ESP:栈指针寄存器,存放一个指针,该指针永远指向系统栈最上面的栈帧的栈顶
EBP:基址指针寄存器,该指针永远指向系统栈最上面的栈帧的底部
函数栈帧:ESP和EBP之间内存空间为当前栈帧
在函数栈帧中一般包含以下几种信息:、
局部变量:为函数举报变量开辟的内存空间
栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡得到)
函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置
函数栈帧的大小不固定,一般和局部变量的多少有关
5.函数调用约定与相关指令
调用约定描述了函数传递参数的方式和栈协同工作的技术细节,下面列出几种调用方式:
C Syscall Stdcall BASIC FORTRAN PASCAL
参数入栈顺序 右→左 右→左 右→左 左→右 左→右 左→右
谁恢复栈平衡 母函数 子函数 子函数 子函数 子函数 子函数
对于Visual C++,支持3种函数调用约定:
调用约定声明 参数入栈顺序 谁恢复栈平衡
__cdecl 右→左 母函数
__fastcall 右→左 子函数
__stdcall 右→左 子函数
除了入栈方向和恢复平衡不同之外,参数传递有时也会有所不同。例如,每一个C++类成员函数都有一个this指针,在windows下,这个指针保存在ECX中
,但如果用GCC编译,这个指针会作为最后一个参数入栈
函数调用大致包括以下几个步骤:
参数入栈
返回地址入栈
代码区跳转
栈帧调整:具体包括
保存当前栈帧状态值(push ebp)
将当前栈帧切换到新栈帧(mov ebp,esp)
给新栈帧分配空间(把ESP减去所需空间大小,抬高栈顶)
函数返回大致包括以下几个步骤:
保存返回值(通常保存在EAX中)
弹出当前栈帧,恢复上一个栈帧:具体包括
在堆栈平衡的基础上给ESP加上栈帧的大小,降低栈顶,回收当前栈帧空间
将当前栈帧底部保存的前栈帧EBP值弹入EBP,恢复出上一个栈帧
将函数返回地址弹给EIP
跳转