翻译《The Old New Thing》- Identifying an object whose underlying DLL has been unloaded

50 篇文章 1 订阅
7 篇文章 0 订阅

Identifying an object whose underlying DLL has been unloaded - The Old New Thing (microsoft.com)icon-default.png?t=N7T8https://devblogs.microsoft.com/oldnewthing/20070425-00/?p=27123

Raymond Chen 2007年04月25日


识别底层 DLL 已卸载的对象

简要

        本文通过实例教学如何诊断程序崩溃问题,特别是当涉及到动态链接库(DLL)被卸载时。作者利用调试器识别出虚方法调用、vtable位置、以及DLL卸载的迹象。通过将模块作为转储文件加载,计算vtable地址的偏移,并最终确认了崩溃的原因是由于DLL被错误地卸载,而程序仍尝试访问其资源。

正文

        好吧,我在标题里已经透露了答案,但请继续阅读。

        你的程序运行着,然后突然像这样崩溃了:

eax=06bad8e8 ebx=00000000 ecx=1e1cfdf0 edx=00000000 esi=06b9a680 edi=01812950
eip=1180ab57 esp=001178b4 ebp=001178c0 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
ABC!FunctionX+0x1f:
1180ab57 ff5108          call    dword ptr [ecx+8]    ds:0023:1e1cfdf8=????????
0:000>>

你可能会迅速察觉到几个关键点::

  • 这是一个通过寄存器间接调用的虚方法调用。 (我们对此有极高的把握。)
  • 虚函数表(vtable)存储在ecx寄存器中。 (这是间接调用的基础寄存器,我们对此也非常有信心。)
  • 这个对象的底层DLL已经被卸载了。 (vtable所在的内存区域不再有效,而且地址与曾经是有效代码的区域相符,我们对此有较高的把握。)
  • 这很可能是一个IUnknown::Release的调用。 (因为在x86架构中,ReleaseIUnknown接口的第三个函数,位于vtable的第8个字节位置,我们对此有很高的信心。) 

        当然,所有的这些“即时判断”都只是基于经验的猜测,但生活中充满了这样的猜测。(比如每天早晨,我都会猜测我的盘子还在橱柜里。)

        接下来,我们假设对象位于一个已经被卸载的DLL中,并寻找证据来支持这一点。

0:000> lm
start    end        module name
...
Unloaded modules:
10340000 10348000   DEF.DLL
1e1c0000 1e781000   GHI.DLL
25a90000 25a96000   JKL.DLL
0:000>

        我们注意到,假设的vtable地址正好处于GHI.DLL之前的加载地址空间内。

        为了查看那个地址之前加载了什么,我们采用了Doron的一个技巧:将模块作为一个转储文件加载。这样,我们可以在其中进行探索,就像它被加载了一样。

C:\Program Files\ABC> ntsd -z GHI.DLL
Microsoft (R) Windows Debugger
Copyright (c) Microsoft Corporation. All rights reserved.
Loading Dump File [C:\Program Files\ABC\GHI.DLL]
...
ModLoad: 15800000 15dc1000   C:\Program Files\ABC\GHI.DLL
eax=00000000 ebx=00000000 ecx=00000000 edx=00000000 esi=00000000 edi=00000000
eip=15807366 esp=00000000 ebp=00000000 iopl=0         nv up di pl nz na pe nc
cs=0000  ss=0000  ds=0000  es=0000  fs=0000  gs=0000             efl=00000000
GHI!_DllMainCRTStartup:
15807366 8bff             mov     edi,edi
0:000>

        加载模块的通知告诉我们DLL被加载到了哪个地址;在我们的例子中,它被加载到了0x15800000。

        这个地址和程序崩溃时的地址不同,因此我们需要做一些计算来调整这个差异。

        回顾原始的寄存器转储,我们假设的vtable在ecx=1e1cfdf0,相对于加载地址1e1c0000。由于我们作为转储文件加载的DLL被加载在0x1580000,我们需要将地址调整为相对于新位置。

// working with the second copy of ntsd
0:000> ln 0x1580fdf0
(1580fdf0)   GHI!CAlphaStream::`vftable'

        通过一些简单的计算,我们得到了0x1580fdf0这个地址。首先,我们从原始的vtable地址中减去DLL的加载地址:

0x1e1cfdf0
-0x1e1c0000
0x0000fdf0

        这是崩溃时vtable相对于DLL加载地址的偏移量。然后,我们将这个偏移量加到作为转储文件加载的DLL的加载地址上:

0x15800000
+0x0000fdf0
0x1580fdf0

        这是 DLL-loaded-as-a-dump-file(DLL 加载即转储文件)中 vtable 的地址,相对于 DLL-loaded-as-a-dump-file(DLL 加载即转储文件)中 DLL 的加载地址。正如你所看到的,计算其实并不难,因为很多东西都可以抵消。这种情况经常发生。

        当我们要求调试器告诉我们哪个符号离该地址最近时,我们中了大奖:它正是 CAlphaStream 对象的 vtable。这证实了我们最初的推测。我们甚至可以通过查看vtable的内容来验证IUnknown::Release的调用:

0:000> dds 1580fdf0
1580fdf0  159234b3 GHI!CAlphaStream::QueryInterface
1580fdf4  15810539 GHI!CBetaState::AddRef
1580fdf8  15923cfc GHI!CAlphaStream::Release
1580fdfc  15923d30 GHI!CAlphaStream::Read
...

        没错,这就是一个 CAlphaStream vtable。

        由于我对 GHI.DLL 文件不太熟悉,所以让我们问问调试器源代码在哪里,以便仔细查看:

0:000> .lines
Line number information will be loaded
0:000> dds 1580fdf0
1580fdf0  159234b3 GHI!CAlphaStream::QueryInterface
                   [c:\dev\fabricam\synergy\proactive\winwin.cpp @ 2624]
1580fdf4  15810539 GHI!CBetaState::AddRef
                   [c:\dev\fabricam\leverage\paradigm\initiative.cpp @ 427]
1580fdf8  15923cfc GHI!CAlphaStream::Release
                   [c:\dev\fabricam\synergy\proactive\winwin.cpp @ 2638]
1580fdfc  15923d30 GHI!CAlphaStream::Read
                   [c:\dev\fabricam\synergy\proactive\winwin.cpp @ 2649]

        既然我们知道了 CAlphaStream 的源代码在哪里,我们就可以跳过去快速浏览一下,确认一下,哦,看,这个对象在构造时并没有增加 DLL 对象计数(或在销毁时减少 DLL 对象计数)。因此,当 COM 调用 DllCanUnloadNow 时,GHI.DLL 会说:"当然,请便!"即使 ABC 仍有对该 DLL 的引用,该 DLL 还是被卸载了,然后当 ABC 去释放该引用时,我们就会崩溃,因为 GHI 已经消失了。

        写完这篇文章后,发现Tony Schreiner也经历了类似的挑战,他处理的是一个第三方的Internet Explorer工具栏,而且他没有插件的源代码,这给他带来了额外的挑战!

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0x0007

可不可奖励我吃只毛嘴鸡 馋😋

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值