我想大家平时拿到crash文件首先都会看看栈的调用情况,如果问题简单直接就能从栈上定位出问题。但是有的时候我们可能没有办法从栈上找到我们想要的信息,这个时候堆是一个很好的地方来找到我们需要的信息。下面的两个案例就是利用这个方法解决问题的。
1. 由于内存泄漏,程序终止。终止过程中,主线程栈已经析构,堆信息依然完整,这时出现crash。
2. 对于打开优化编译选项的C++程序,当分析类的成员函数时,由于默认调用方式是__thiscall,this指针用ecx传递,所以当查看非顶端栈帧时,ecx值都不是正确的this指针值,所以对象状态很难找到。
由于我的应用中都是ATL实现的COM对象,所以对象的首地址都是该对象的虚函数表,所以可以通过从遍历所有堆块,查找虚函数表的方式来找到对象的首地址或者对象的个数,从而找到相应的对象或者定位何种对象用光内存。下面就用一个例子来说明具体如何操作。
假设COM对象的声明如下:
class ATL_NO_VTABLE CFileStreamer :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<CFileStreamer, &CLSID_FileStreamer>,
public IStream,
public IFileStreamer,
public ICompressionCtrl
{
...
};
那么函数对应的虚函数表如下:
使用如下的测试程序:
int _tmain(int argc, _TCHAR* argv[])
{
CComPtr<IFileStreamer> spIUnknown;
CoInitializeEx(NULL,COINIT_MULTITHREADED);
if( FAILED(CoCreateInstance(CLSID_FileStreamer,
NULL,
CLSCTX_ALL,
IID_IFileStreamer,
(void**)&spIUnknown))
)
{
printf("failed to create filestreamer object/n");
}
_getch();
spIUnknown.Release();
CoUninitialize();
return 0;
}
当程序执行起来之后使用windbg附到程序上,然后运行下面的命令:
0:001> !heap
Index Address Name Debugging options enabled
1: 00150000
2: 00010000
3: 00370000
4: 008e0000
5: 009f0000
6: 00be0000
7: 00b00000
这里列出了系统中所有的堆,对于每个堆执行下面的命令:
!heap <heap handle>。例如:
0:001> !heap 00150000
Index Address Name Debugging options enabled
1: 00150000
Segment at 00150000 to 00250000 (00034000 bytes committed)
这里的输出就是堆00150000中包含的堆段,对于每个堆段执行下面的命令:
dds <segment start address> L<commited bytes>。
这里的输出是对每个堆上的地址进行符号解析,如果找到一个虚函数表,那么就会显示如下的样子:
003c3de0 00000000
003c3de4 00000000
003c3de8 0013000c
003c3dec 0018075c
003c3df0 10008630 FileStreamerBox!ATL::CComObject<CFileStreamer>::`vftable'
003c3df4 100085f0 FileStreamerBox!ATL::CComObject<CFileStreamer>::`vftable'
003c3df8 100085c8 FileStreamerBox!ATL::CComObject<CFileStreamer>::`vftable'
003c3dfc 00000001
003c3e00 0015eff8
003c3e04 ffffffff
这里地址003c3df0就是对象CComObject<CFileStreamer>的首地址,即this指针的值。
如果把所有输出通过.logopen/.logclose命令收集起来,然后统计其中各种虚函数表的个数就能到各种对象的数量分布,从而定位问题。