|
一:背景
1. 讲故事
昨晚给训练营里面的一位朋友分析了一个程序崩溃的故障,因为看小伙子昨天在群里问了一天也没搞定,干脆自己亲自上阵吧,抓取的dump也是我极力推荐的用 procdump 注册 AEDebug 的方式,省去了很多沟通成本。
二:WinDbg分析
1. 为什么会崩溃
windbg有一个非常强大的点就是当你双击打开后,会自动帮你切换到崩溃的线程以及崩溃处的汇编代码,省去了 !analyze -v 命令的龟速输出,参考信息如下:- ................................................................
- ...................................................
- This dump file has an exception of interest stored in it.
- The stored exception information can be accessed via .ecxr.
- (10f4.f58): Access violation - code c0000005 (first/second chance not available)
- For analysis of this file, run !analyze -v
- eax=00000000 ebx=00000000 ecx=00000040 edx=00000000 esi=004c1b98 edi=07a8ed4c
- eip=7008508f esp=07a8ec74 ebp=07a8ec80 iopl=0 nv up ei pl zr na pe nc
- cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
- clr!Thread::GetSafelyRedirectableThreadContext+0x7c:
- 7008508f 8038eb cmp byte ptr [eax],0EBh ds:002b:00000000=??
- ...
复制代码 从卦中可以看到,当前崩溃是因为 eax=0 导致的,那为什么 eax 等于 0 呢?要想寻找这个答案,需要观察崩溃前的线程栈上下文,可以使用命令 .ecxr;k 9 即可。- 0:009> .ecxr;k 9
- eax=00000000 ebx=00000000 ecx=00000040 edx=00000000 esi=004c1b98 edi=07a8ed4c
- eip=7008508f esp=07a8ec74 ebp=07a8ec80 iopl=0 nv up ei pl zr na pe nc
- cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
- clr!Thread::GetSafelyRedirectableThreadContext+0x7c:
- 7008508f 8038eb cmp byte ptr [eax],0EBh ds:002b:00000000=??
- # ChildEBP RetAddr
- 00 07a8ec80 6fe7f6cd clr!Thread::GetSafelyRedirectableThreadContext+0x7c
- 01 07a8f030 6fe7f2f3 clr!Thread::HandledJITCase+0x31
- 02 07a8f0a4 6fee23da clr!Thread::SuspendRuntime+0x260
- 03 07a8f184 6fedf72d clr!WKS::GCHeap::SuspendEE+0x1fe
- 04 07a8f1b0 6fe309ca clr!WKS::GCHeap::GarbageCollectGeneration+0x168
- 05 07a8f1c0 6fe30a2e clr!WKS::GCHeap::GarbageCollectTry+0x56
- 06 07a8f1e4 6fe30a90 clr!WKS::GCHeap::GarbageCollect+0xa5
- 07 07a8f230 6f058b01 clr!GCInterface::Collect+0x5d
- 08 07a8f26c 055fa4b1 mscorlib_ni+0x3b8b01
复制代码 从卦中信息看,尼玛,真无语了 GCInterface::Collect 说明有人用 GC.Collect() 手工触发GC,不知道为什么要这么做来污染GC内部的统计信息,不管怎么说这个肯定不是崩溃的原因。
2. GC正在干什么
我们继续观察线程栈,可以看到它的逻辑大概是这样的,通过 SuspendRuntime 把所有的托管线程进行逻辑上暂停,在暂停其中的一个线程时抛出了异常。
稍微提醒一下,这个 HandledJITCase 方法是用 ip 劫持技术将代码引入到 coreclr 中进行 GC完成等待,这种神操作有些杀毒软件会认为是病毒!!!
有些朋友肯定会说,有没有代码支撑。。。这里我就找一下 coreclr 的源码贴一下吧。- void ThreadSuspend::SuspendRuntime(ThreadSuspend::SUSPEND_REASON reason)
- {
- while ((thread = ThreadStore::GetThreadList(thread)) != NULL)
- {
- ...
- if (workingOnThreadContext.Acquired() && thread->HandledJITCase())
- {
- ...
- }
- ...
- }
- }
复制代码 结合源码分析思路就非常清晰了,这里的 thread->HandledJITCase() 中的 thread 到底是哪一个线程? 可以观察 kb 输出然后用 !t 去做比对。
从卦中看,当前 GC 正在 Suspend 主线程,并且还看到了主线程有一个 System.AccessViolationException 异常,无语了。。。
3. 主线程到底怎么了
主线程进入到视野之后,那就重点关注一下它,可以用 k 看一下输出。- 0:009> ~0s
- eax=00000000 ebx=0029ea50 ecx=0029ea90 edx=00000000 esi=7efdb800 edi=000d0000
- eip=00000000 esp=0029ea4c ebp=75146381 iopl=0 nv up ei pl nz na po nc
- cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00210202
- 00000000 ?? ???
- 0:000> k
- 00 75146381 7efdb800 0x0
- 01 75146381 7517fa04 0x7efdb800
- 02 0029ea80 7736013a user32!__fnHkINLPKBDLLHOOKSTRUCT+0x28
- 03 0029eae4 7514908d ntdll!KiUserCallbackDispatcher+0x2e
- 04 0029eae4 076e3912 user32!CallNextHookEx+0x84
- 05 0029eb28 076e3064 0x76e3912
- 06 0029eb5c 0011d48f xxx!xxx.ScanerHook.KeyboardHookProc+0xe4
- 07 0029eb8c 75146381 0x11d48f
- 08 0029eba8 7517fa04 user32!DispatchHookW+0x38
- 09 0029ebd8 7736013a user32!__fnHkINLPKBDLLHOOKSTRUCT+0x28
- 0a 0029ec3c 751406eb ntdll!KiUserCallbackDispatcher+0x2e
- 0b 0029ec3c 75140751 user32!_PeekMessage+0x88
- 0c 0029ec68 6d8af3bf user32!PeekMessageW+0x108
- ...
复制代码 从卦象看,这卦非常奇怪,有如下两点信息:
- eip=00000000,这个很无语,线程已经疯了
- KeyboardHookProc ,居然有键盘钩子
熟悉 eip 的朋友应该知道,它相当于一辆车的方向盘,一辆高速行驶的车突然没了方向盘,真的太可怕了,最后必然车毁人亡。
4. 是 eip=0 导致的崩溃吗
在汇编中是因为eax=0导致,而这里eip恰好也等于0,仿佛冥冥之中自有牵连,带着强烈的好奇心我们来反汇编下 GetSafelyRedirectableThreadContext 方法逻辑,简化后如下:- 0:000> uf 7008508f
- clr!Thread::GetSafelyRedirectableThreadContext:
- 6fe7f60e 55 push ebp
- 6fe7f60f 8bec mov ebp,esp
- 6fe7f611 53 push ebx
- 6fe7f612 56 push esi
- 6fe7f613 57 push edi
- 6fe7f614 8bf1 mov esi,ecx
- ...
- 7008506d ffe9 jmp rcx
- 7008506f fd std
- 70085070 c1daff rcr edx,0FFh
- 70085073 f6450801 test byte ptr [ebp+8],1
- 70085077 0f84efa5dfff je clr!Thread::GetSafelyRedirectableThreadContext+0xcc (6fe7f66c)
- 7008507d 8b8604010000 mov eax,dword ptr [esi+104h]
- 70085083 3987b8000000 cmp dword ptr [edi+0B8h],eax
- 70085089 0f85dda5dfff jne clr!Thread::GetSafelyRedirectableThreadContext+0xcc (6fe7f66c)
- 7008508f 8038eb cmp byte ptr [eax],0EBh
复制代码 从上面的汇编代码看eax的取值链条是: eax kb 2 # ChildEBP RetAddr Args to Child 00 07a8ec80 6fe7f6cd 00000003 07a8ed4c 07a8ecf0 clr!Thread::GetSafelyRedirectableThreadContext+0x7c01 07a8f030 6fe7f2f3 004c1b98 0b367326 76a016a1 clr!Thread::HandledJITCase+0x310:009> dt _CONTEXT 07a8ed4cntdll!_CONTEXT +0x000 ContextFlags : 0x10007 ... +0x01c FloatSave : _FLOATING_SAVE_AREA +0x08c SegGs : 0x2b +0x090 SegFs : 0x53 +0x094 SegEs : 0x2b +0x098 SegDs : 0x2b +0x09c Edi : 0xd0000 +0x0a0 Esi : 0x7efdb800 +0x0a4 Ebx : 0x29ea50 +0x0a8 Edx : 0 +0x0ac Ecx : 0x29ea90 +0x0b0 Eax : 0 +0x0b4 Ebp : 0x75146381 +0x0b8 Eip : 0 +0x0bc SegCs : 0x23 +0x0c0 EFlags : 0x210202 +0x0c4 Esp : 0x29ea4c ...[/code]从卦中看果然 eip=0,这是一个非常错误的信息,还有一点就是 m_LastRedirectIP 字段一般用来处理一些比较诡异的兼容性问题,所以这里两个字段都是 0 导致崩溃的产生。
有了上面的信息,我们就知道了前因后果,原来是主线程车毁人亡(eip=0),导致GC无法暂停它,在内部抛出了代码异常,你可以说是 CLR 的bug,也可以说是主线程的Bug,所以给到的解决方案就是:
- 屏蔽掉 键盘钩子 的业务逻辑,肯定是它造成的。
- 不去掉的话,要重点观察 键盘盘子 ,是否是代码改动引发的。
三:总结
说实话要想解释这个程序为什么会崩溃,需要分析者对GC的SuspendRuntime运作逻辑有一定的了解,否则真抓瞎了,所以.NET调试训练营中的GC理论知识一定是分析这些 dump 的基石。
来源:https://www.cnblogs.com/huangxincheng/p/18101375
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|