经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
记一次 .NET某工业设计软件 崩溃分析
来源:cnblogs  作者:一线码农  时间:2024/5/31 12:43:52  对本文有异议

一:背景

1. 讲故事

前些天有位朋友找到我,说他的软件在客户那边不知道什么原因崩掉了,从windows事件日志看崩溃在 clr 里,让我能否帮忙定位下,dump 也抓到了,既然dump有了,接下来就上 windbg 分析吧。

二:WinDbg 分析

1. 为什么崩溃在 clr

一般来说崩溃在clr里都不是什么好事情,这预示着 clr 在执行自身代码的时候抛了异常,即灾难的 ExecutionEngineException,可以用 !t 验证下。

  1. 0:000> !t
  2. ThreadCount: 18
  3. UnstartedThread: 0
  4. BackgroundThread: 7
  5. PendingThread: 0
  6. DeadThread: 11
  7. Hosted Runtime: no
  8. Lock
  9. ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
  10. 0 1 52e8 18998d50 24220 Preemptive 639B0D58:00000000 18c361f0 0 STA System.ExecutionEngineException 1f421120
  11. ...

既然是灾难性异常,那为什么会出现呢?可以用 !analyze -v 观察下。

  1. 0:000> !analyze -v
  2. CONTEXT: 0115a98c -- (.cxr 0x115a98c)
  3. eax=00000000 ebx=00000000 ecx=00000000 edx=18c364a4 esi=00030000 edi=18998d50
  4. eip=552bfff1 esp=0115ae6c ebp=0115af24 iopl=0 nv up ei pl zr na pe nc
  5. cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
  6. clr!VirtualCallStubManager::ResolveWorker+0x33:
  7. 552bfff1 8bb968020000 mov edi,dword ptr [ecx+268h] ds:002b:00000268=????????
  8. Resetting default scope
  9. READ_ADDRESS: 00000268
  10. STACK_TEXT:
  11. 0115af24 552c0698 0115afdc 1f4222c0 00030000 clr!VirtualCallStubManager::ResolveWorker+0x33
  12. 0115affc 552c070b 0115b010 1f4222c0 00030000 clr!VSD_ResolveWorker+0x1d2
  13. 0115b024 28a3a949 639b0d38 00000000 00000000 clr!ResolveWorkerAsmStub+0x1b
  14. 0115b0a4 28a3a8bd 00000000 00000000 00000000 xxxx!xxx
  15. ...

我去,真无语了,我卦中数据看,这是一个接口Stub调用的崩溃,在这里崩溃真的是少之又少,从汇编代码 edi,dword ptr [ecx+268h] ds:002b:00000268=???????? 上看就是因为 ecx =0 导致的,接下来观察下方法的汇编代码。

从汇编上看这个 ecx 其实就是这个方法的 this 指针,那为什么 this =null 呢?这就很奇葩了。

2. 为什么 this =null

要想找到这个答案,只能看clr源代码,简化后如下:

  1. PCODE VSD_ResolveWorker(TransitionBlock* pTransitionBlock,
  2. TADDR siteAddrForRegisterIndirect,
  3. size_t token
  4. )
  5. {
  6. ...
  7. VirtualCallStubManager::StubKind stubKind = VirtualCallStubManager::SK_UNKNOWN;
  8. VirtualCallStubManager* pMgr = VirtualCallStubManager::FindStubManager(callSiteTarget, &stubKind);
  9. ...
  10. target = pMgr->ResolveWorker(&callSite, protectedObj, representativeToken, stubKind);
  11. }

从卦中代码看,问题就是 pMgr=null 导致的,无语了,这个 VirtualCallStubManager::FindStubManager 方法的本意就是根据 callSite的stub的前缀找到对应的 虚调用管理器,它的核心逻辑如下:

  1. StubKind getStubKind(PCODE stubStartAddress, BOOL usePredictStubKind = TRUE)
  2. {
  3. StubKind predictedKind = (usePredictStubKind) ? predictStubKind(stubStartAddress) : SK_UNKNOWN;
  4. ...
  5. if (predictedKind == SK_LOOKUP)
  6. {
  7. if (isLookupStub(stubStartAddress))
  8. return SK_LOOKUP;
  9. }
  10. ...
  11. return SK_UNKNOWN;
  12. }
  13. VirtualCallStubManager::StubKind VirtualCallStubManager::predictStubKind(TADDR stubStartAddress)
  14. {
  15. StubKind stubKind = SK_UNKNOWN;
  16. WORD firstWord = *((WORD*)stubStartAddress);
  17. if (firstWord == 0x05ff)
  18. {
  19. stubKind = SK_DISPATCH;
  20. }
  21. else if (firstWord == 0x6850)
  22. {
  23. stubKind = SK_LOOKUP;
  24. }
  25. else if (firstWord == 0x8b50)
  26. {
  27. stubKind = SK_RESOLVE;
  28. }
  29. return stubKind;
  30. }

接下来需要找到 stubStartAddress 的地址是多少?这个只需要提取 ResolveWorker 方法的第一个参数 callSite 即可。

  1. 0:000> dp poi(0115afdc) L1
  2. 0c740040 0c746012
  3. 0:000> u 0c746012
  4. 0c746012 50 push eax
  5. 0c746013 6800000300 push 30000h
  6. 0c746018 e9d3a6b748 jmp clr!ResolveWorkerAsmStub (552c06f0)
  7. 0c74601d 0000 add byte ptr [eax],al
  8. 0c74601f 0000 add byte ptr [eax],al
  9. 0c746021 005068 add byte ptr [eax+68h],dl
  10. 0c746024 0000 add byte ptr [eax],al
  11. 0c746026 46 inc esi
  12. 0:000> dp 0c746012 L1
  13. 0c746012 00006850

对比刚才的代码既然都返回来了 SK_LOOKUP 那为什么还是 SK_UNKNOWN 呢? 这个也可以通过在线程栈上找到 &stubKind 变量得到验证。

  1. 0:000> uf 552c0698
  2. ...
  3. clr!VSD_ResolveWorker+0x1ab:
  4. 552c065f 8b85e0ffffff mov eax,dword ptr [ebp-20h]
  5. 552c0665 83a5ecffffff00 and dword ptr [ebp-14h],0
  6. 552c066c 8d95ecffffff lea edx,[ebp-14h]
  7. 552c0672 8b08 mov ecx,dword ptr [eax]
  8. 552c0674 e858feffff call clr!VirtualCallStubManager::FindStubManager (552c04d1)
  9. 552c0679 ffb5ecffffff push dword ptr [ebp-14h]
  10. 552c067f 51 push ecx
  11. 552c0680 8bcc mov ecx,esp
  12. 552c0682 8931 mov dword ptr [ecx],esi
  13. 552c0684 ffb5e8ffffff push dword ptr [ebp-18h]
  14. 552c068a 8d8de0ffffff lea ecx,[ebp-20h]
  15. 552c0690 51 push ecx
  16. 552c0691 8bc8 mov ecx,eax
  17. 552c0693 e823f9ffff call clr!VirtualCallStubManager::ResolveWorker (552bffbb)
  18. 552c0698 8bf0 mov esi,eax
  19. ...
  20. 0:000> dp 0115affc-0x14 L1
  21. 0115afe8 00000000

我感觉这逻辑也只有clr团队帮忙解释,我已经搞不清楚了,接下来我们回头看托管方法,看能不能继续下去。

3. 在托管层寻找突破口

高级调试就是这样,一个方向走不通就需要在另一个方向上突破,接下来使用 !clrstack 观察一下。

  1. 0:000> !clrstack
  2. OS Thread Id: 0x52e8 (0)
  3. Child SP IP Call Site
  4. 0115af50 775c2aac [GCFrame: 0115af50]
  5. 0115afac 775c2aac [StubDispatchFrame: 0115afac]xxx.GetListDrawerType(System.String)
  6. 0115b02c 28a3a949 xxx.PluginInvoker.InvokeMothod[[System.__Canon, mscorlib]](System.String, System.Object[])
  7. 0115b0b0 28a3a8bd xxx.xxx.OnFinishSizeCheck(Int64)
  8. ...

从调用栈来看,貌似是用反射来实现功能增强,不管怎么说先看下xxxCheck 方法干了什么?简化后的代码如下:

  1. public string OnFinishSizeCheck(long uuid)
  2. {
  3. return PluginInvoker.InvokeMothod<string>("xxxCheck", new object[1] { uuid });
  4. }
  5. public static T InvokeMothod<T>(string methodName, params object[] args)
  6. {
  7. IPluginInvoker pluginInvoker = GetPluginInvoker();
  8. return (T)pluginInvoker.InvokeMothod(methodName, args);
  9. }

从代码上可以看到原来是使用 (T)pluginInvoker.InvokeMothod(methodName, args); 实现的接口调用,在coreclr层面也能观察得到,找到对象 1f4222c0 之后按图索骥即可。

  1. 0:000> !do 1f4222c0
  2. Name: xxx.xxx.BusinessAppDomainInvoker
  3. MethodTable: 0c73a144
  4. EEClass: 0c6d6f0c
  5. Size: 12(0xc) bytes
  6. File: E:\xxx\xxx.dll
  7. Fields:
  8. MT Field Offset Type VT Attr Value Name
  9. 0c73a4e8 400000a 4 ....AppDomainManager 0 instance 1f42236c appDomainManager
  10. 0c73a2dc 4000009 18 ..., xxx]] 0 static 1f422214 lazy
  11. 0:000> !dumpmt -md 0c73a144
  12. EEClass: 0c6d6f0c
  13. Module: 0c7383dc
  14. Name: xxx.xxx.BusinessAppDomainInvoker
  15. mdToken: 02000006
  16. File: E:\xxx\xxx.dll
  17. BaseSize: 0xc
  18. ComponentSize: 0x0
  19. Slots in VTable: 10
  20. Number of IFaces in IFaceMap: 1
  21. --------------------------------------
  22. MethodDesc Table
  23. Entry MethodDe JIT Name
  24. ...
  25. 0c6c3400 0c73a110 JIT xxx.xxx.InvokeMothod(System.String, System.Object[])
  26. 0:000> !do poi(0c73a144+0x24)
  27. Name: xxx.IPluginInvoker
  28. MethodTable: 0c739f30
  29. EEClass: 0c6d6d34
  30. Size: 0(0x0) bytes
  31. File: E:\xxx\xxx.dll
  32. Fields:
  33. None
  34. ThinLock owner 1 (18998d50), Recursive 0

对比那个 token=30000h 发现什么地方都没有问题,奇葩的就是一个简单接口调用就出现了问题,仔细观察代码之后发现了两个和别人不一样的地方。

4. 与众不同的地方在哪里

第一个是他的程序是多 AppDomain 的,可以用 !dumpdomain 观察。

  1. 0:000> !dumpdomain
  2. --------------------------------------
  3. System Domain: 55a6caa0
  4. ...
  5. --------------------------------------
  6. Shared Domain: 55a6c750
  7. LowFrequencyHeap: 55a6cdc4
  8. Stage: OPEN
  9. --------------------------------------
  10. Domain 1: 18b04690
  11. LowFrequencyHeap: 18b04afc
  12. Name: DefaultDomain
  13. --------------------------------------
  14. Domain 2: 18c361f0
  15. LowFrequencyHeap: 18c3665c
  16. ...

第二个是我发现托管调用栈上还有很多 托管C++,这种混合编程真的是无语了。

到这里我想到了三个办法:

1)如果可以先把接口方法预热,clr会直接把方法入口塞到汇编里,就不会再走clr底层逻辑了。

2)能否将 托管C++ 和 C# 隔离,不要混合编程。

3)重点观察下多Domain下这个托管调用是不是有什么问题。

三:总结

这种 多domain + 托管C++混合C# 编程,真出问题了基本上就是无解,一般人hold不住,无语了。

图片名称

原文链接:https://www.cnblogs.com/huangxincheng/p/18224311

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号