汇编语言基础之七- 框架指针的省略(FPO)

框架指针省略(Frame Pointer Omission)(FPO)

FPO是一种优化,它压缩或者省略了在栈上为该函数创建框架指针的过程。这个选项加速了函数调用,因为不需要建立和移除框架指针(ESP,EBP)了。同时,它还解放出了一个寄存器,用来存储使用频率较高的变量。只在IntelCPU的架构上才有这种优化。

 

目前已经讨论过的任何一种调用约定都保存了前一函数中栈的信息(压栈ebp,然后让ebp = esp,再移动esp来保存局部变量)。一个FPO的函数可能会保存前一函数的栈指针(ESP,EBP),但是并不为当前的函数调用设立EBP。相反,他使用EBP来存储一些其他的变量。debugger 会计算栈指针,但是debugger必须得到一个使用FPO的提醒,该提醒是基于FPO类型的信息的来完成的。

 

这项特性可以在MS Visual C++专业版和企业版中开启。使用的是编译器的/Oy选项。

 

FPO的数据结构可以在Microsoft的SDK中的winnt.h中找到,其中包含了描述栈框架内容的信息。这些信息被使用在debugger上,或者其他的需要在栈中寻找FPO函数的程序中。KV命令可以显示出包括FPO信息在内的额外的运行时信息。

 

0:000> kv

ChildEBP RetAddr

0012ff74 00401009 addemup!Addemup (FPO: [2,0,0])

0012ff80 00401115 addemup!main+0x9 (FPO: [0,0,0])

0012ffc0 77e87903 addemup!mainCRTStartup+0xb4

0012fff0 00000000 KERNEL32!BaseProcessStart+0x3d (FPO: [Non-Fpo]

 

上面的例子中,括号括起来的FPO数据结构的意义分别是:

FPO数据表示形式 (FPO: [ebp addr][x,y,z])
x代表 作为参数压栈了的DWORDS个数
y代表 作为局部变量压栈了的DWORDS个数
z代表 在开场代码中(prologue)压栈了的寄存器个数
ebp addr代表 仅在EBP在开场代码中保存了的时候显示

上面的例子中,由于debugger有正确的symbol,所以debugger会计算出栈底(Frame Pointer)的位置,而不是在EBP之中保存它。比如说,第一个参数的位置是栈底+0x8,返回值的位置是栈底+0x4. 开启了FPO之后,这些值就不能通过ebp + 0x8这样拿到了,跟ebp等值的栈底(Frame Pointer)需要计算才能拿到。

 

仅仅靠上面的信息来理解FPO还是感觉有点云里雾里的。

关于FPO,有篇文章做了非常好的介绍,来龙去脉都讲的很清楚。链接列在下面:

http://blogs.msdn.com/larryosterman/archive/2007/03/12/fpo.aspx

我总结了一些要点在下面,方便大家更好的理解FPO的一些概念。

下表列出了同样功能,但是FPO选项不同的汇编代码。

未开启FPO的函数的汇编代码 开启了FPO的函数的汇编代码

MyFunction:
    PUSH    EBP
    MOV     EBP, ESP
    SUB      ESP, <LocalVariableStorage>
    MOV     EAX, [EBP+8]
      :
      :
    MOV     ESP, EBP
    POP      EBP
    RETD

MyFunction:
    SUB      SP, <LocalVariableStorage>
    MOV     EAX, [ESP+4+<LocalVariableStorage>]
      :
      :
    ADD     SP, <LocalVariableStorage>
    RETD

注意,这里访问第一个参数的代码是

MOV     EAX, [EBP+8]

注意,这里访问第一个参数的代码是

MOV     EAX, [ESP+4+<LocalVariableStorage>]

 

下两表分别列出了同样功能,但是FPO选项栈内容分配,以及参数的访问方式。

未开启FPO的指针指向 栈中的内容
[ebp-04]
[ebp-01]
[ebp+00] 
[ebp+04] 
[ebp+08]
第一个局部变量的首地址
第一个局部变量的最后一个字节
上一个EBP的值
返回值,即调用该函数之前的EIP寄存器的值
第一个参数的首地址
  说明:
     因为参数都是从右至左压栈的,所以ebp+8是最后一个压栈的参数,所以是第一个参数。
     因为被调用函数中,先将ESP向上移动出所有局部变量的尺寸,然后根据EBP的位置从下到上,局部变量从第一个往最后一个赋值的,所以ebp-1是第一个局部变量的最后一个字节。

 

开启了FPO的指针指向 栈中的内容
[esp]
[esp+08]
[esp+11]
[esp+12]
[esp+16]
[esp+20]
    最后一个局部变量的第一个字节
    … 
    第一个局部变量的首地址
    第一个局部变量的最后一个字节
    返回值
    第一个参数的首地址 
    …
    前一个函数的局部变量
  说明:
    假设当前FPO的数据为(FPO: [1,2,0])
    即参数有1个dwords(4字节)局部变量2个dwords(8字节)压栈的寄存器为0个(0字节)

我还参考了另两篇文章,有点难懂,不过出于对我理解这个概念的贡献,还是将文章链接列在下面。

Frame pointer omission (FPO) and consequences when debugging, part 1.

Frame pointer omission (FPO) and consequences when debugging, part 2.

 

观察FPO函数

只要当前函数和前一个函数的symbol被正确加载的话,debugger就可以计算出栈底指针的位置。

因为EBP被保留下来用作通用寄存器了,并没有用来建立栈框架,所以没有必要将这个寄存器压入栈中。这就是为什么它不再指向前一个EBP的原因。

如果FPO函数拥有局部变量,debugger计算出来的栈底位置指向第一个局部变量。

如果FPO函数没有任何的局部变量,debugger计算出来的栈底位置指向第一个被保留下来的寄存器。

如果FPO函数没有任何局部变量,也没有保留的寄存器,debugger计算出来的栈底位置指向没被使用的栈空间。

让人迷惑的地方是,当一个FPO函数,使用FASTCALL调用约定的时候。函数没有栈底指针,参数也没有被压在栈上。尽管如此,这些函数通常都比较小。反汇编前面的函数,就可以看到寄存器是如何被加载的了。

学友认为,上面的话可以总结如下:FPO函数没有保存EBP,所以访问参数等的时候就无法使用EBP了。所以FPO函数依靠symbol中的信息来计算一个类似于EBP指向的位置的指针。不同的是,ebp指向的是栈中保存的前一个栈底的位置。而现在是指向一个尽可能接近原始EBP的,在栈中尽可能靠下(大地址端)的位置。

posted on 2009-11-04 15:29  中道学友  阅读(3462)  评论(1编辑  收藏  举报

导航

技术追求准确,态度积极向上