在C语言中,函数、局部变量等等数据都是存放在栈上的,而栈的大小一般在程序运行时就已经确定,难以改变其大小。想要修改栈的大小,往往需要修改系统设置或者编译参数。对于修改系统参数,会使我们的程序在其他计算机上运行变得困难。因此在这里探讨在程序内将自己申请的内存作为栈使用。
在了解修改栈之前,先来了解一下在Windows、Linux等系统上,一个正在运行的C语言程序内存分配情况是什么样的。
在现在的Windows、Linux等系统中,每个程序都有独立的地址空间,他们被成为虚拟地址,这些虚拟地址在经过页部件转换后才会得到实际的物理地址。因此,即使两个程序同时访问了同一个虚拟地址,因为其运行在自己独立的地址空间中,因此他们最终访问的物理是不同的。这样的虚拟地址的管理方式是通过分页完成的。具体的虚拟地址与物理地址的映射方法与分页模式具体实现这里不做过多讨论。我们只需要了解,内存会被按照4KB的大小分成若干页,当我们使用一个地址时,需要保证该地址所处的页已经被映射到物理地址中。另外,对于一个页,其还具有一些属性,例如该页是只读还是可读写。
当我们运行一个程序时,操作系统会为该程序创建独立的虚拟地址空间,并将程序的代码部分存放在其虚拟地址空间中。注意,将程序的代码放在其独立的虚拟地址空间之前,需要将稍后保存程序用到的虚拟地址映射到物理页中。因为我们在这段内存中存放的是代码,因此只要将这几个页设置为只读即可,这样就可以做到保护代码不被修改。
接下来,一个程序还需要使用栈,因此操作系统再将程序所在的虚拟地址空间的一段地址映射到物理地址上,用来当作程序的栈。程序还有一些其他的段,例如用于存放常量的段、存放各种全局变量的段等。其均为程序在加载时就创建好的。
在我们的程序运行时,我们只能读写那些已经映射到物理地址上的地址,当我们尝试去读写没有映射到物理地址的虚拟地址时会引发错误。而申请内存的原理也是将一段虚拟地址映射到物理地址上并将这段虚拟地址返回给程序使用。
接下来我们再来了解一下C语言程序是如何访问栈的
当我们的程序被加载好后,就可以执行程序了。在执行程序之前,操作系统还需要将SP/ESP/RSP(栈指针寄存器,可以把它当作一个指针)寄存器指向先前创建的栈的最高地址处。为什么是最高地址呢,因为当我们向栈中压入数据时,栈是向低地址方向增长的。
例如,假设一开始ESP寄存器指向了0x7C00,当我们向栈中压入一个4字节的整数后,ESP寄存器就会指向0x7BFC
另外,栈指针寄存器一般总是指向栈顶的,也就是自ESP指向的内存开始向内存地址增大方向,均属于程序的栈。而C语言为了更方便维护栈的结构,以及为了方便调试,C语言使用BP/EBP/RBP(帧指针寄存器)来保存栈底的地址。因此在C语言中,一个函数用到的栈是从ESP寄存器到EBP寄存器之间的区域,这个区域称作栈帧。
当C语言程序进入一个新的函数时,C语言会先通过让EBP=ESP的操作让EBP寄存器指向新函数栈的栈底,再减少ESP寄存器(扩大栈)来为该函数分配所需的栈帧。而退出函数时只需要执行ESP=EBP将该函数所使用的栈帧清空,再将EBP恢复为执行该函数前的值,此时由ESP与EBP构成的区域就再次恢复到调用该函数之前的栈帧了。关于EBP的值如何恢复,只需要在修改其值之前将其压入栈即可,具体方法不做讨论,整个过程均由C语言编译器处理。
由此可见,想要改变函数执行时使用的栈,只需要修改ESP和EBP,让其指向自己分配的内存,就可以实现切换栈的操作。而修改寄存器,需要通过汇编语言来完成,C语言不能直接完成,因此我们需要通过简单的内联汇编来完成操作。这里通过MSVC编译器完成,因此内联汇编使用的是MASM格式的语法。
接下来简单的介绍几个简单的汇编指令
MOV ESP,EBP;将EBP的值复制到ESP中
MOV ESP,[EBX];将EBP指向的内存中的值复制到ESP中
完成切换栈的操作,只需要了解mov指令即可。接下来就来实现切换栈的操作
使用自己创建的栈
要自己创建栈,需要有一段内存,因此需要先申请一段内存
char *new_stack = (char*)malloc(1024*1024*10);//分配10MB的空间
这样我们就有了10MB的空间,将其作为栈了
接下来只需要将ESP指向申请到的这段空间的末尾即可,因为栈是向低地址方向增长的\
new_stack+=1024*1024*10;//让指针指向栈的结尾
__asm
{
MOV ESP,new_stack;将new_stack的值复制给ESP
}
在此之后调用新的函数等操作,就会在新的栈里了完成了。
但是,在新的栈里执行完后,可能还需要切换回原来的栈。这个操作并不能借助局部变量完成,因为一旦切换了栈,原来的局部变量就不再可用,但我们可以借助全局变量完成。以下给出完整的切换栈的操作。
#include <stdio.h>
#include <stdlib.h>
void* old_stack;
void func()
{
//在使用Windows系统默认的栈时会发生栈溢出,但在新的栈中不会
char arr[1024 * 1024 * 9] = { 0 };
}
int main()
{
char* new_stack = (char*)malloc(1024 * 1024 * 10);
new_stack += 1024 * 1024 * 10;
__asm
{
MOV old_stack, ESP; 备份当前栈
MOV ESP, new_stack;切换到新栈
}
func();//在新的栈中执行该函数
__asm
{
MOV ESP, old_stack;还原旧的栈
}
return 0;
}