系统栈原理

原址在此


这篇文章是摘录自《软件漏洞分析入门》,作者failwest,在此对作者表示感谢和膜拜,\(^o^)/~


 

根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下四个部分:

代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域来取指并执行。
数据区:用于存储全局变量等。
堆区:进程可以在堆区动态的请求一定大小的内存,并在用完之后归还给堆区。动态分配和回收是堆区的特点
栈区:用于动态的存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行

     注意:这种简单的内存划分方式是为了让您能够更容易地理解程序的运行机制。《深入理解计算机系统》一书中有更详细的关于内存使用的论述,如果您对这部分知识有兴趣,可以参考之 

  
windows平台下,高级语言写出的程序经过编译链接,最终会变成各位同学最熟悉不过的PE文件。当PE文件被装载运行后,就成了所谓的进程。

 

系统栈原理【转】 - zhongcong386 - Let Dream Fly 
1

                      
  
如果把计算机看成一个有条不紊的工厂的话,那么可以简单的看成是这样组织起来的:

CPU
是完成工作的工人;
数据区,堆区,栈区等则是用来存放原料,半成品,成品等各种东西的场所;
存在代码区的指令则告诉 CPU 要做什么,怎么做,到哪里去领原材料,用什么工具来做,做完以后把成品放到哪个货舱去;
值得一提的是,栈除了扮演存放原料,半成品的仓库之外,它还是车间调度主任的办公室。
  
  
程序中所使用的缓冲区可以是堆区、栈区、甚至存放静态变量的数据区。缓冲区溢出的利用方法和缓冲区到底属于上面哪个内存区域密不可分,本讲座主要介绍在系统栈中发生溢出的情形。堆中的溢出稍微复杂点,我会考虑在中级班中给予介绍

  
以下内容针对正常情况下的大学本科二年级计算机水平或者计算机二级水平的读者,明白栈的飘过即可。

  
从计算机科学的角度来看,栈指的是一种数据结构,是一种先进后出的数据表。栈的最常见操作有两种:压栈 (PUSH) ,弹栈 (POP) ;用于标识栈的属性也有两个:栈顶 (TOP) ,栈底( BASE

  
可以把栈想象成一摞扑克牌:

  PUSH
:为栈增加一个元素的操作叫做 PUSH ,相当于给这摞扑克牌的最上面再放上一张;
  POP
:从栈中取出一个元素的操作叫做 POP ,相当于从这摞扑克牌取出最上面的一张;

  TOP
:标识栈顶位置,并且是动态变化的。每做一次 PUSH 操作,它都会自增 1 ;相反每做一次 POP 操作,它会自减 1 。栈顶元素相当于扑克牌最上面一张,只有这张牌的花色是当前可以看到的。
  BASE
:标识栈底位置,它记录着扑克牌最下面一张的位置。 BASE 用于防止栈空后继续弹栈,(牌发完时就不能再去揭牌了)。很明显,一般情况下 BASE 是不会变动的。

  
内存的栈区实际上指的就是系统栈。系统栈由系统自动维护,它用于实现高级语言中函数的调用。对于类似 C 语言这样的高级语言,系统栈的 PUSH POP 等堆栈平衡细节是透明的。一般说来,只有在使用汇编语言开发程序的时候,才需要和它直接打交道。

  
注意:系统栈在其他文献中可能曾被叫做运行栈,调用栈等。如果不加特别说明,我们这里说的栈都是指系统栈这个概念,请您注意与求解“八皇后”问题时在自己在程序中实现的数据结构区分开来。


  
我们下面就来探究一下高级语言中函数的调用和递归等性质是怎样通过系统栈巧妙实现的。请看如下代码:

int  func_B(int arg_B1, int arg_B2)
{
  int var_B1, var_B2;
  var_B1=arg_B1+arg_B2;
  var_B2=arg_B1-arg_B2;
  return var_B1*var_B2;
}

int  func_A(int arg_A1, int arg_A2)
{
  int var_A;
  var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
  return var_A;
}

int main(int argc, char **argv, char **envp)
{
  int var_main;
  var_main=func_A(4,3);
  return var_main;
}

  
这段代码经过编译器编译后,各个函数对应的机器指令在代码区中可能是这样分布的:


 
系统栈原理【转】 - zhongcong386 - Let Dream Fly
 
2


  
根据操作系统的不同、编译器和编译选项的不同,同一文件不同函数的代码在内存代码区中的分布可能相邻也可能相离甚远;可能先后有序也可能无序;但他们都在同一个 PE 文件的代码所映射的一个“区”里。这里可以简单的把它们在内存代码区中的分布位置理解成是散乱无关的。

  
CPU 在执行调用 func_A 函数的时候,会从代码区中 main 函数对应的机器指令的区域跳转到 func_A 函数对应的机器指令区域,在那里取指并执行;当 func_A 函数执行完闭,需要返回的时候,又会跳回到 main 函数对应的指令区域,紧接着调用 func_A 后面的指令继续执行 main 函数的代码。在这个过程中, CPU 的取指轨迹如下图所示:
  
 

系统栈原理【转】 - zhongcong386 - Let Dream Fly
 
3


  
那么 CPU 是怎么知道要去 func_A 的代码区取指,在执行完 func_A 后又是怎么知道跳回到 main 函数(而不是 func_B 的代码区)的呢?这些跳转地址我们在 C 语言中并没有直接说明, CPU 是从哪里获得这些函数的调用及返回的信息的呢?

  
原来,这些代码区中精确的跳转都是在与系统栈巧妙地配合过程中完成的。当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。

系统栈原理【转】 - zhongcong386 - Let Dream Fly
 
4

  
  
如图所示,在函数调用的过程中,伴随的系统栈中的操作如下:

  
main 函数调用 func_A 的时候,首先在自己的栈帧中压入函数返回地址,然后为 func_A 创建新栈帧并压入系统栈
  
func_A 调用 func_B 的时候,同样先在自己的栈帧中压入函数返回地址,然后为 func_B 创建新栈帧并压入系统栈
  
func_B 返回时, func_B 的栈帧被弹出系统栈, func_A 栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到 func_A 代码区中执行
  
func_A 返回时, func_A 的栈帧被弹出系统栈, main 函数栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址跳到 main 函数代码区中执行

  
注意:在实际运行中, main 函数并不是第一个被调用的函数,程序被装入内存前还有一些其他操作,上图只是栈在函数调用过程中所起作用的示意图


  
每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。 WIN32 系统提供两个特殊的寄存器用于标识位于系统栈栈顶的栈帧:

  ESP
:栈指针寄存器 (extended stack pointer) ,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶
  EBP
:基址指针寄存器 (extended base pointer) ,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部


  
寄存器对栈帧的标识作用如下图所示:

系统栈原理【转】 - zhongcong386 - Let Dream Fly
 
 
5



  
函数栈帧: ESP EBP 之间的内存空间为当前栈帧, EBP 标识了当前栈帧的底部, ESP 标识了当前栈帧的顶部。
  
  
在函数栈帧中一般包含以下几类重要信息:

  
局部变量:为函数局部变量开辟内存空间。
  
栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后,恢复出上一个栈帧。
  
函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便函数返回时能够恢复到函数被调用前的代码区中继续执行指令。


  
注意:函数栈帧的大小并不固定,一般与其对应函数的局部变量多少有关。在以后几讲的调试实验中您会发现,函数运行过程中,其栈帧大小也是在不停变化的。


  
除了与栈相关的寄存器外,您还需要记住另一个至关重要的寄存器:

  EIP
:指令寄存器 (extended instruction pointer)   其内存放着一个指针,该指针永远指向下一条待执行的指令地址

系统栈原理【转】 - zhongcong386 - Let Dream Fly
 

 
6


  
可以说如果控制了 EIP 寄存器的内容,就控制了进程——我们让 EIP 指向哪里, CPU 就会去执行哪里的指令。下面的讲座我们就会逐步介绍如何控制 EIP ,劫持进程的原理及实验。


函数调用约定与相关指令
  

函数调用约定描述了函数传递参数方式和栈协同工作的技术细节。不同的操作系统、不同的语言、不同的编译器在实现函数调用时的原理虽然基本类同,但具体的调用约定还是有差别的。这包括参数传递方式,参数入栈顺序是从右向左还是从左向右,函数返回时恢复堆栈平衡的操作在子函数中进行还是在母函数中进行。下面列出了几种调用方式之间的差异。

                              C          SysCall  StdCall  BASIC  FORTRAN  PASCAL
参数入栈顺序                    ->    ->    ->    ->    ->    ->
恢复栈平衡操作的位置    母函数    子函数    子函数    子函数    子函数    子函数


  
具体的,对于 Visual C ++来说可支持以下三种函数调用约定
调用约定的声明    参数入栈顺序    恢复栈平衡的位置
__cdecl  
->    母函数
__fastcall  
->    子函数
__stdcall  
->    子函数

  
要明确使用某一种调用约定的话只需要在函数前加上调用约定的声明就行,否则默认情况下 VC 会使用 __stdcall 的调用方式。本篇中所讨论的技术,在不加额外说明的情况下,都是指这种默认的 __stdcall 调用方式。

  
除了上边的参数入栈方向和恢复栈平衡操作位置的不同之外,参数传递有时也会有所不同。例如每一个 C++ 类成员函数都有一个 this 指针,在 windows 平台中这个指针一般是用 ECX 寄存器来传递的,但如果用 GCC 编译器编译的话,这个指针会做为最后一个参数压入栈中。

  
同一段代码用不同的编译选项、不同的编译器编译链接后,得到的可执行文件会有很多不同。

  
函数调用大致包括以下几个步骤:

  
参数入栈:将参数从右向左依次压入系统栈中
  
返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行
  
代码区跳转:处理器从当前代码区跳转到被调用函数的入口处
  
栈帧调整:具体包括
  
保存当前栈帧状态值,已备后面恢复本栈帧时使用( EBP 入栈)
  
将当前栈帧切换到新栈帧。(将 ESP 值装入 EBP ,更新栈帧底部)
  
给新栈帧分配空间。(把 ESP 减去所需空间的大小,抬高栈顶)
  
  
对于 __stdcall 调用约定,函数调用时用到的指令序列大致如下:

  ;
调用前
push 
参数 3    ;  假设该函数有 3 个参数,将从右向左依次入栈
push 
参数 2    
push 
参数 1    
call 
函数地址   ; call 指令将同时完成两项工作: a )向栈中压入当前指令在内存中的位置,           ;  即保存返回地址; b )跳转到所调用函数的入口地址

  ;
函数入口处
push ebp      ; 
保存旧栈帧的底部
mov ebp
esp    ;  设置新栈帧的底部(栈帧切换)
sub esp
xxx    ;  设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间)

上面这段用于函数调用的指令在栈中引起的变化如下图所示:
 
 
系统栈原理【转】 - zhongcong386 - Let Dream Fly
 

  
系统栈原理【转】 - zhongcong386 - Let Dream Fly


注意:关于栈帧的划分不同参考书中有不同的约定。有的参考文献中把返回地址和前栈帧 EBP 值做为一个栈帧的顶部元素,而有的则将其做为栈帧的底部进行划分。在后面的调试中,您会发现 OllyDbg 在栈区标示出的栈帧是按照前栈帧 EBP 值进行分界的,也就是说前栈帧 EBP 值即属于上一个栈帧,也属于下一个栈帧,这样划分栈帧后返回地址就成为了栈帧顶部的数据。我们这里将坚持按照 EBP ESP 之间的位置做为一个栈帧的原则进行划分。这样划分出的栈帧如上面最后一幅图所示,栈帧的底部存放着前栈帧 EBP ,栈帧的顶部存放着返回地址。划分栈帧只是为了更清晰的了解系统栈的运作过程,并不会影响它实际的工作。

  
类似的,函数返回的步骤如下:

  
保存返回值:通常将函数的返回值保存在寄存器 EAX
  
弹出当前栈帧,恢复上一个栈帧:
  
具体包括
  
在堆栈平衡的基础上,给 ESP 加上栈帧的大小,降低栈顶,回收当前栈帧的空间
  
将当前栈帧底部保存的前栈帧 EBP 值弹入 EBP 寄存器,恢复出上一个栈帧
  
将函数返回地址弹给 EIP 寄存器
  
跳转:按照函数返回地址跳回母函数中继续执行

  
还是以 C 语言和 WIN32 平台为例,函数返回时的相关的指令序列如下:   

  
add xxx, esp  ;
降低栈顶,回收当前的栈帧
pop ebp    ;
将上一个栈帧底部位置恢复到 ebp
retn      ;
这条指令有两个功能: a) 弹出当前栈顶元素,即弹出栈帧中的返回地址。至此         ; 栈帧恢复工作完成。 b) 让处理器跳转到弹出的返回地址,恢复调用前的代码区


  
按照这样的函数调用约定组织起来的系统栈结构如下:
系统栈原理【转】 - zhongcong386 - Let Dream Fly
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值