wenmo8 发布的文章

在调试程序的过程中,可能遇到过一两次“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;
}

!cppexr

!cppexr扩展显示一个C++ 异常记录的内容。

语法

!cppexr Address 

参数

Address
指定要显示的C++ 异常记录的地址。

DLL

Windows 2000 Ext.dll
Windows XP 和之后 Ext.dll

注释

!cppexr扩展显示和目标遇到的C++ 异常相关的信息,包括发生异常处的代码、异常的地址和异常标志。必须是在Msvcrt.dll中定义的标准C++
异常。

一般可以使用!analyze -v命令来获得address参数。

!cppexr扩展用来查看C++异常的类型很有用。

附加信息

使用.exr命令来显示其它异常记录。

!mapped_file

!mapped_file扩展命令显示指定地址所在的内存映射文件所关联的文件名字。

语法

!mapped_file Address 

参数

Address
指定映射文件的地址。如果Address不在内存映射文件中,则命令失败。

DLL

Windows 2000 Uext.dll
Windows XP和之后 Uext.dll

!mapped_file 扩展命令只能用于活动、非远程调试。

注释

这里有三个例子。前两个地址是从文件映射出来的,第三个地址不是。

0:000> !mapped_file 4121ec 
Mapped file name for 004121ec: '\Device\HarddiskVolume2\CODE\TimeTest\Debug\TimeTest.exe'

0:000> !mapped_file 77150000 
Mapped file name for 77150000: '\Device\HarddiskVolume2\Windows\System32\kernel32.dll'

0:000> !mapped_file 80310000 
No information found for 80310000: error 87

.dump (Create Dump File)

.dump命令创建一个用户模式或内核模式崩溃转储文件。

语法

.dump Options FileName 
.dump /? 

参数

Options
指定下面这些选项的一个或多个。
/o
覆盖具有相同名字的dump文件。如果没有使用该选项又存在一个相同名字的文件,则dump文件不会被写入。
/f
(内核模式:) 创建一个完整内存dump。

 (用户模式:) 创建一个完整用户模式dump。不管怎么称呼,最大的minidump文件实际比完整用户模式dump文件包含更多信息。例如,.dump /mf.dump /ma会创建比.dump /f创建更大更完整的文件。在用户模式下,.dump
/m
[MiniOptions] 总是比.dump /f更可取。

/m[MiniOptions]
创建一个小内存dump(内核模式)或者
minidump
(用户模式)。如果没有指定/f/m/m
是默认选项。

用户模式下,/m 后面可以跟附加的MiniOptions
用来指定dump文件中包含的数据。如果没有使用MiniOptions
,dump文件包含模块、线程和调用堆栈信息,但是没有其他附加信息。可以使用下面的任意MiniOptions
来改变dump文件的内容,它们区分大小写。


MiniOption作用
a 创建一个包含所有附加选项的minidump。/ma选项相当于/mfFhut
—它会在minidump中添加完整的内存数据、句柄数据、已卸载模块信息、基本内存信息和线程时间信息。
f 在minidump中包含完整内存数据。目标程序拥有的所有 可访问的已交付的页面(committed pages)都会包含进去。
F 在minidump中添加所有基本内存信息。这会将一个流加入到包含完整基本内存信息的minidump中,而不单是可使用的内存。这样可以使得调试器能够重建minidump生成时进程的完整虚拟内存布局。
h 在minidump中包含和目标进程相关的句柄信息。
u 在minidump中包含已卸载模块信息。仅在Windows Server 2003和之后版本的Windows中可用。
t 在minidump中包含附加的线程信息。包括可以在调试minidump时使用!runaway扩展命令或.ttime (Display Thread Times)命令进行显示的线程时间。
i 在minidump中包含次级内存(secondary
memory)。次级内存是由堆栈中的指针或备份存储(backing store)中引用到的任何内存,加上该地址周围的一小段区域。
p 在minidump中包含进程环境块(PEB)和线程环境块(TEB)。这在想访问程序的进程和线程相关的Windows系统信息时很有用。
w 将所有已交付的可读写的私有页面包含进minidump。
d 在minidump中包含可执行映像中所有可读写的数据段。
c 加入映像中的代码段。
r 从minidump中去掉对重建调用堆栈无用的堆栈和存储内存部分。局部变量和其他数据类型值也被删除。这个选项不会使得minidump变小(因为这些内存节仅仅是变成0),但是当想保护其他程序中的机密信息时有用。
R 在minidump中去掉完整的模块路径。仅包含模块名。如果想保护用户的目录结构时该选项有用。

这些MiniOptions 只能在创建用户模式minidump时使用。它们需要跟在 /m 修饰符后面。

/u
在dump文件名中添加日期、时间和PID。这可以确保dump文件名的唯一性。
/a
所有当前正在调试的进程创建dump。如果使用了/a,也必须使用/u
选项来保证每个文件有唯一的名字。
/b[a]
创建一个.cab 文件。如果包含该选项, FileName
会被当作CAB文件名,而不是dump文件名。会先创建一个临时的dump文件,然后将该文件打包为CAB,然后删除它。如果b后面跟了a,所有的符号和映像文件也会打包到CAB中。
/c "Comment"
指定要写入dump文件的注释字符串。如果Comment 中包含空格,则必须用双引号将它括起来。当dump文件加载时会显示这个Comment
字符串。
/xc Address
(仅用户模式minidump) 在dump文件中加入上下文记录。Address必须指定上下文记录的地址。
/xr Address
(仅用户模式minidump) 在dump文件中加入一个异常记录。Address 必须指定该异常记录开始的地址。
/xp Address
(仅用户模式minidumps) 在dump文件中加入一个上下文记录和异常记录。Address
必须指定一个包含上下文记录和异常记录指针的 EXCEPTION_POINTERS 结构的地址。
/xt ThreadID
(仅用户模式minidumps) 指定将会作为dump文件的异常线程的系统线程的ID。
/kpmf File
(仅创建内核模式完整内存dump) 指定一个包含物理内存页面数据的文件。
FileName
指定dump文件名。可以设置完整路径和文件名,或者只设置文件名。如果文件名中包含空格,则需要用引号将FileName
括起来。如果没有指定路径,会使用当前目录。
-?
显示该命令的帮助。在用户模式和内核模式下显示的文本是不同的。

环境

模式 用户模式、内核模式
目标 活动目标、崩溃转储
平台 所有

注释

该命令可以在各种情况下使用:

  • 进行用户模式活动调试时,该命令创建目标程序的dump文件,但是目标程序不会被结束。
  • 内核模式活动调试时,该命令创建目标机的dump文件,但是目标机不会崩溃。
  • 调试dump文件时,该命令从旧文件创建出一个新的dump文件。这在有一个大的dump文件但是又想创建一个更小的文件时有用。

可以控制dump文件的类型:

  • 内核模式下,使用/f选项来生成完整内存转储。使用/m选项(或不带选项)来生成小内存转储。.dump命令不能创建内核内存转储。
  • 用户模式下,使用.dump /m[MiniOptions] 是最好的选择。虽然"m" 代表着"minidump",但是使用MiniOption创建的dump文件可以很小也可以很大。通过指定合适的MiniOptions
    ,可以精确的控制包含的信息。例如,.dump /ma 产生包含大量信息的dumo文件。.dump /f命令产生大小适中的"标准dump"文件,并且不能进行自定义。

不能指定要转储的进程。所有运行中的进程都会转储下来。

/xc/xr/xp/xt
选项用来在dump文件中保存异常和上下文信息。这使得该dump文件可以使用
.ecxr (Display Exception Context Record)

下面的示例创建一个包含完整内存和句柄信息的用户模式minidump:

0:000> .dump /mfh myfile.dmp 

.dumpcab (Create Dump File CAB)

.dumpcab命令创建一个包含当前dump文件的CAB文件。

语法

.dumpcab [-aCabName 

参数

-a
使得当前加载的符号也包含在CAB文件中。对于minidump,所有以加载的映像也会包含进去。使用lml来查看加载了哪些符号和映像。
CabName
包含扩展名的CAB文件名。CabName 可以包含绝对或者相对路径;相对路径是相对于调试器启动的目录的。建议使用.cab扩展名。

环境

模式 用户模式、内核模式
目标 活动目标、崩溃转储
平台 所有

注释

该命令只有在调试dump文件时可以使用。(这句话似乎和前面的表格有冲突。—译者注)

如果在调试活动目标时想创建CAB中的dump文件,需要使用.dump (Create Dump File)命令。然后,打开一个使用该dump文件作为目标的新的调试会话,然后再使用.dumpcab

.dumpcab命令不能将多个dump文件放入一个CAB文件中。