Raymond Chen 2007年04月24日
COM接口指针背后的底层对象是什么?
简要
Raymond Chen在这篇文章中分享了在调试COM接口时确定底层对象的技巧,包括如何使用调试器命令和一些简单的数学来调整指针,以便正确地查看对象的内容。他还提到了一些在调试过程中应该注意的事项,比如vtable的位置、引用计数的值,以及字符串成员的内容。
正文
当你在调试时,你可能有一个指向COM接口的指针,并想知道它的底层对象是什么。有时,这个技巧可能不奏效,因为接口指针实际上指向一个存根或代理,但在没有涉及封送(marshalling)的情况下,这个方法效果很好。(这个技术同样适用于许多C++编译器,对于任何具有虚方法和因此具有虚函数表(vtable)的对象。)
回想一下,COM对象的布局要求指向COM接口的指针指向对象的vtable,而vtable是关键。
0:000> dv pstm = 0x000c7568 0:000> dt psf Local var @ 0x7cc2c Type IStream* 0x000c7568 +0x000 __VFN_table : 0x1c9c8e84
到目前为止,我们只知道我们的IStream *
位于0x000c7568
,它的vtable是0x1c9c8e84
。它是谁的流实现呢?
0:000> ln 0x1c9c8e84 (1c9c8e84) ABC!CAlphaStream::`vftable'
啊哈,它是来自ABC.DLL
的CAlphaStream
。让我们来看一下:
0:000> dt ABC!CAlphaStream 0x000c7568
+0x000 __VFN_table : 0x1c9c8e84 // our vtable
+0x004 m_cRef : 480022128
+0x008 lpVtbl : 0x1c9d2d30
+0x00c lpVtbl : 0x00000014
+0x010 m_pszName : 0x000c7844 "??????????"
+0x014 m_dwFlags : 0x3b8
+0x018 m_pBuffer : 0x00000005
+0x01c m_cbBuffer : 705235565
+0x020 m_cbPos : 2031674
“嘿,你是如何让调试器将m_pszName
作为字符串转储的呢?” 如果你执行.enable_unicode 1
命令,调试器将把指向unsigned short
的指针视为指向Unicode字符串的指针。(默认情况下,只有指向wchar_t
的指针被视为指向Unicode字符串的指针。)
好的,回到结构转储。它看起来根本不对。 引用计数是一个荒谬的值,偏移0x00c
处的vtable是一个错误的指针,m_pszName
中的名字是垃圾,除了初始的vtable和偏移0x008
处的vtable之外,几乎所有字段都明显是错误的。
发生了什么? 显然,我们得到一个“q
”指针;也就是说,指向除了第一个之外的某个vtable的指针。我们必须调整指针,使其指向对象的开头而不是中间。
我们如何进行这种调整? 有一种系统化的方法和一种快速而简单的方法。
系统化的方法是使用调整器(adjustor thunks)来告诉你需要将指针调整多少,以便从辅助vtable移动到主vtable。(这假设主IUnknown
实现是第一个基类。这不一定是这样,但通常如此。)
0:000> dps 1c9c8e84 l1 1c9c8e84 1c9eb08e ABC![thunk]:CAlphaStream::QueryInterface`adjustor{8}'
啊哈,这些调整器调整了八个字节,所以我们只需要从我们的指针中减去八个字节,就可以得到对象的起始地址。
0:000> dt ABC!CAlphaStream 0x000c7560-8
+0x000 __VFN_table : 0x1c9c8ee8
+0x004 m_cRef : 2
+0x008 lpVtbl : 0x1c9c8e84
+0x00c lpVtbl : 0x1c9c8e70
+0x010 m_pszName : 0x1c9d2d30 "Scramble"
+0x014 m_dwFlags : 0x14
+0x018 m_pBuffer : 0x000c7844
+0x01c m_cbBuffer : 952
+0x020 m_cbPos : 5
啊,这看起来好多了。注意,引用计数是一个更合理的值,两个,名字指针看起来不错,缓冲区大小和位置看起来现实多了。
现在,我不会费心去处理整个调整器thunk的事情。相反,我依赖于“假设它大多是正确的”原则:假设对象没有损坏,只是通过眼睛调整指针,直到字段对齐。
让我们再次看看原始的(错误的)转储:
0:000> dt ABC!CAlphaStream 0x000c7568 +0x000 __VFN_table : 0x1c9c8e84 +0x004 m_cRef : 480022128 +0x008 lpVtbl : 0x1c9d2d30 +0x00c lpVtbl : 0x00000014 +0x010 m_pszName : 0x000c7844 "??????????" +0x014 m_dwFlags : 0x3b8 +0x018 m_pBuffer : 0x00000005 +0x01c m_cbBuffer : 705235565 +0x020 m_cbPos : 2031674
这显然不对,但我们该怎么做才能让事情对齐呢? 嗯,我们知道我们有的vtable必须进入其他两个vtable插槽中的一个,要么是偏移0x008
的插槽,要么是偏移0x00c
的插槽。 如果我们把它移到偏移0x00c
,那么当前在偏移0x00c
处的0x00000014
将向下移动十二个字节,放在偏移0x018
处,正好是m_pBuffer
。但显然0x00000014
不是有效的缓冲区指针,所以0x00c
不可能是正确的调整。 另一方面,如果我们把我们的vtable放在偏移0x008
,那么0x000c7844
将移动到m_pBuffer
的位置,这看起来不太不合理。因此,我猜调整器是八个,得到了我们通过转储vtable来查看调整器时得到的相同的结构转储。
在现实生活中,我倾向于关注vtable、引用计数和任何字符串成员,因为通常很容易看出你是否正确地得到了它们。(vtable位于代码中。引用计数往往是小整数。字符串,嗯,字符串就是字符串。)