Visual C++ IDE 调试时 this 指针探究
你拿到了一个 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 到堆栈中.