你拿到了一个 crash dump 文件, 之前你部署了你自己的 symstore, 所以, 你可以轻而易举的看到完美的调用栈. 如果只是一个简单的访问违例, 你看到调用栈的时候基本上问题就已经解决了. 但是, 更多的时候问题不是这么简单. 你不得不查看底层的调用栈来查看每个参数是如何传进来的. 但这时, 你也发现要查看每一个调用的参数并不是那么简单. 为什么会出现这种情况呢? 原因在于优化. 在调试中遇到的比较烦的优化包括 inline, FPO 等, 这里我要说的是 this 指针丢失. 教科书里说 C++ 传递 this 指针一般是作为最后一个参数. 压栈传到 callee 中. 但现实生活中更多的是用 ecx 来传递 this 指针, 这样就有一个问题. ecx 每次会调用时都会被覆盖. 就像这样:

DemoFunc:
mov ecx, pOneObj
call OneMemberFunc
...

OneMemberFunc:
mov ecx, pOtherObj
call OtherMemberFunc

在 OneMemberFunc 中, ecx 被覆盖. 如果在 OtherMemberFunc 中出现 crash, 查看调用栈时, 已经看不到在 DemoFunc 中的 ecx 的值了. 但我们在 IDE 中调试 debug 版程序时, VC 调试器能够查看 callstack 每个 frame 的 this 指针, 它是如何做到的呢? 看看这个例子程序:

 cpp
struct foo_t
{
	foo_t() : name_("foo_t"){}
	char* name_;
	void func()
	{
		int i;
		i = 0;
	}
};

struct bar_t
{
	bar_t() : name_("bar_t"){t_ = new foo_t();}
	char* name_;
	void func()
	{
		t_->func();
	}
	foo_t* t_;
};

struct zoo_t
{
	zoo_t() : name_("zoo_t"){}
	char* name_;
	void func()
	{
		t.func();
	}
	int n;
	bar_t t;
};

void main(){zoo_t z; z.func();}

其中的 zoo_t::func() 反汇编如下:

26:       char* name_;
27:       void func()
28:       {
00401210   push        ebp
00401211   mov         ebp,esp
00401213   sub         esp,44h
00401216   push        ebx
00401217   push        esi
00401218   push        edi
00401219   push        ecx
0040121A   lea         edi,[ebp-44h]
0040121D   mov         ecx,11h
00401222   mov         eax,0CCCCCCCCh
00401227   rep stos    dword ptr [edi]
00401229   pop         ecx
0040122A   mov         dword ptr [ebp-4],ecx	// check this
29:           t.func();
0040122D   mov         ecx,dword ptr [ebp-4]
00401230   add         ecx,8
00401233   call        @ILT+5(bar_t::func) (0040100a)
30:       }

看 0040122A 地址的指令. mov dword ptr [ebp-4],ecx 原来就是这么简单, 编译器在堆栈中给 ecx 留了一个位置. 调用函数之前先把 ecx 保存在其中. 这样, 调试器在调试 debug 版程序的时候, 每次都可以用 [ebp-4] 来访问 this 指针. 测试发现, 在 VC6 中, 如果不启用 Global Optimization. 编译器会在每个成员函数的开始处保存 ecx 到堆栈中.