再谈FPO
在调试程序的过程中,可能遇到过一两次“FPO”这个词。FPO是指在x86上处理编译器如何访问本地变量和基于堆栈的参数的编译器优化的一个特定类型。对于使用局部变量(和/或基于堆栈的参数)的函数,编译器需要一种机制来引用堆栈上的这些值。通常,这是通过以下两种方式之一完成的:
- 直接从堆栈指针(esp)访问局部变量。这是启用FPO优化时的行为。虽然这不需要单独的寄存器来跟踪局部变量和参数的位置,但如果禁用了FPO优化,这会使生成的代码稍微复杂一些。特别是,由于函数调用或修改堆栈的其他指令等原因,esp中局部变量和参数的位移实际上会随着函数的执行而改变。因此,编译器必须在引用基于堆栈的值的函数中的每个位置跟踪当前esp值的实际位移。对于编译器来说,这通常不是什么大问题,但是在手工编写的汇编程序中,这可能会变得有点棘手。
- 指定一个寄存器指向堆栈上相对于局部变量和基于堆栈的参数的固定位置,并使用此寄存器访问局部变量和参数。这是禁用FPO优化时的行为。约定是使用ebp寄存器访问局部变量和堆栈参数。Ebp通常设置为第一个堆栈参数可以在[Ebp+08]中找到,而局部变量通常位于Ebp的负位移处。
禁用FPO优化的函数的典型情况可能如下所示:
push ebp ; save away old ebp (nonvolatile) mov ebp, esp ; load ebp with the stack pointer sub esp, sizeoflocals ; reserve space for locals ... ; rest of function
主要的概念是禁用FPO优化,一个函数将立即保存ebp(作为第一个接触堆栈的操作),然后用当前堆栈指针加载ebp。这时的堆栈布局:
[ebp-01] Last byte of the last local variable [ebp+00] Old ebp value [ebp+04] Return address [ebp+08] First argument...
此后,函数将始终使用ebp访问局部变量和基于堆栈的参数。(函数的汇编序列可能会有一些变化,特别是使用变化的函数设置初始SEH帧时,但相对于ebp,堆栈布局的最终结果始终相同。)
这确实(如前所述)使得ebp寄存器不可用于其他用途。但是,相对于打开FPO优化后编译的函数,此性能影响通常不足以成为一个大问题。此外,有许多情况下要求函数使用帧指针:
- 任何使用SEH的函数都必须使用帧指针,因为当发生异常时,无法从异常分派时的esp值(堆栈指针)中知道局部变量的位移(异常可能发生在任何地方,而诸如进行函数调用或为函数调用设置堆栈参数之类的操作会修改esp的值。
- 任何使用析构函数的C++对象都必须使用SEH来编译解压缩支持。这意味着大多数C++函数最终都被禁用了FPO优化。(可以改变编译器关于SEH异常和C++解卷的假设,但是默认的(和推荐的设置)是在出现SEH异常时取消对象。)
- 任何使用alloca在堆栈上动态分配内存的函数都必须使用一个帧指针(因此禁用了FPO优化),因为esp对局部变量和参数的位移可以在运行时更改,编译器在生成代码时不知道。
由于这些限制,您可能正在编写的许多函数将已经禁用FPO优化,而没有显式地将其关闭。但是,仍有可能许多不符合上述条件的函数启用了FPO优化,因此不使用ebp引用局部变量和堆栈参数。
既然您已经大致了解了FPO优化的功能,那么我将在本系列的下半部分介绍为什么在调试某些类问题时全局关闭FPO优化对您有利。(事实上,大多数微软系统代码也会关闭FPO,因此您可以放心,已经在FPO和非FPO优化代码之间进行了真正的成本效益分析,在一般情况下禁用FPO优化总体上更好。)
考虑下面的示例程序,其中有几个不做任何事情的函数,这些函数将堆栈参数乱放并相互调用。(在本文中,我禁用了全局优化和函数内联。)
__declspec(noinline) void f3( int* c, char* b, int a ) { *c = a * 3 + (int)strlen(b); __debugbreak(); } __declspec(noinline) int f2( char* b, int a ) { int c; f3( &c, b + 1, a - 3); return c; } __declspec(noinline) int f1( int a, char* b ) { int c; c = f2( b, a + 10); c ^= (int)rand(); return c + 2 * a; } int __cdecl wmain( int ac, wchar_t** av ) { int c; c = f1( (int)rand(), "test"); printf("%d\\n", c); return 0; }
- 上一篇: 仅通过转储来排除内存泄漏
- 下一篇: 使用Java中的InputStream读取文件数据