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 到堆栈中.