当前位置: 首页 > news >正文

记一次 .NET某工业设计软件 崩溃分析

一:背景

1. 讲故事

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

二:WinDbg 分析

1. 为什么崩溃在 clr

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


0:000> !t
ThreadCount:      18
UnstartedThread:  0
BackgroundThread: 7
PendingThread:    0
DeadThread:       11
Hosted Runtime:   noLock  ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception0    1 52e8 18998d50     24220 Preemptive  639B0D58:00000000 18c361f0 0     STA System.ExecutionEngineException 1f421120...

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


0:000> !analyze -v
CONTEXT:  0115a98c -- (.cxr 0x115a98c)
eax=00000000 ebx=00000000 ecx=00000000 edx=18c364a4 esi=00030000 edi=18998d50
eip=552bfff1 esp=0115ae6c ebp=0115af24 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!VirtualCallStubManager::ResolveWorker+0x33:
552bfff1 8bb968020000    mov     edi,dword ptr [ecx+268h] ds:002b:00000268=????????
Resetting default scopeREAD_ADDRESS:  00000268 STACK_TEXT:  
0115af24 552c0698     0115afdc 1f4222c0 00030000 clr!VirtualCallStubManager::ResolveWorker+0x33
0115affc 552c070b     0115b010 1f4222c0 00030000 clr!VSD_ResolveWorker+0x1d2
0115b024 28a3a949     639b0d38 00000000 00000000 clr!ResolveWorkerAsmStub+0x1b
0115b0a4 28a3a8bd     00000000 00000000 00000000 xxxx!xxx
...

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

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

2. 为什么 this =null

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


PCODE VSD_ResolveWorker(TransitionBlock* pTransitionBlock,TADDR siteAddrForRegisterIndirect,size_t token)
{...VirtualCallStubManager::StubKind stubKind = VirtualCallStubManager::SK_UNKNOWN;VirtualCallStubManager* pMgr = VirtualCallStubManager::FindStubManager(callSiteTarget, &stubKind);...target = pMgr->ResolveWorker(&callSite, protectedObj, representativeToken, stubKind);
}

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


StubKind getStubKind(PCODE stubStartAddress, BOOL usePredictStubKind = TRUE)
{StubKind predictedKind = (usePredictStubKind) ? predictStubKind(stubStartAddress) : SK_UNKNOWN;...if (predictedKind == SK_LOOKUP){if (isLookupStub(stubStartAddress))return SK_LOOKUP;}...return SK_UNKNOWN;
}VirtualCallStubManager::StubKind VirtualCallStubManager::predictStubKind(TADDR stubStartAddress)
{StubKind stubKind = SK_UNKNOWN;WORD firstWord = *((WORD*)stubStartAddress);if (firstWord == 0x05ff){stubKind = SK_DISPATCH;}else if (firstWord == 0x6850){stubKind = SK_LOOKUP;}else if (firstWord == 0x8b50){stubKind = SK_RESOLVE;}return stubKind;
}

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


0:000> dp poi(0115afdc) L1
0c740040  0c7460120:000> u 0c746012
0c746012 50              push    eax
0c746013 6800000300      push    30000h
0c746018 e9d3a6b748      jmp     clr!ResolveWorkerAsmStub (552c06f0)
0c74601d 0000            add     byte ptr [eax],al
0c74601f 0000            add     byte ptr [eax],al
0c746021 005068          add     byte ptr [eax+68h],dl
0c746024 0000            add     byte ptr [eax],al
0c746026 46              inc     esi0:000> dp 0c746012 L1
0c746012  00006850

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

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

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

3. 在托管层寻找突破口

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


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

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


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

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


0:000> !do 1f4222c0
Name:        xxx.xxx.BusinessAppDomainInvoker
MethodTable: 0c73a144
EEClass:     0c6d6f0c
Size:        12(0xc) bytes
File:        E:\xxx\xxx.dll
Fields:MT    Field   Offset                 Type VT     Attr    Value Name
0c73a4e8  400000a        4 ....AppDomainManager  0 instance 1f42236c appDomainManager
0c73a2dc  4000009       18 ..., xxx]]  0   static 1f422214 lazy0:000> !dumpmt -md 0c73a144
EEClass:         0c6d6f0c
Module:          0c7383dc
Name:            xxx.xxx.BusinessAppDomainInvoker
mdToken:         02000006
File:            E:\xxx\xxx.dll
BaseSize:        0xc
ComponentSize:   0x0
Slots in VTable: 10
Number of IFaces in IFaceMap: 1
--------------------------------------
MethodDesc TableEntry MethodDe    JIT Name...
0c6c3400 0c73a110    JIT xxx.xxx.InvokeMothod(System.String, System.Object[])0:000> !do  poi(0c73a144+0x24)
Name:        xxx.IPluginInvoker
MethodTable: 0c739f30
EEClass:     0c6d6d34
Size:        0(0x0) bytes
File:        E:\xxx\xxx.dll
Fields:
None
ThinLock owner 1 (18998d50), Recursive 0

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

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

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


0:000> !dumpdomain
--------------------------------------
System Domain:      55a6caa0
...
--------------------------------------
Shared Domain:      55a6c750
LowFrequencyHeap:   55a6cdc4
Stage:              OPEN
--------------------------------------
Domain 1:           18b04690
LowFrequencyHeap:   18b04afc
Name:               DefaultDomain
--------------------------------------
Domain 2:           18c361f0
LowFrequencyHeap:   18c3665c
...

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

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

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

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

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

三:总结

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

相关文章:

  • 并发任务的进化之旅
  • 排序方法——堆排序
  • Qt实现窗口失去焦点抖动功能
  • 在 Kubesphere 中开启新一代云原生数仓 Databend
  • 基于51单片机的超声波测距—数码管显示
  • sqliteSQL基础
  • 理解lambda表达式
  • 在本地电脑中如何用命令操作远程服务器上的数据库
  • [力扣题解] 28. 找出字符串中第一个匹配项的下标
  • 【算法】模拟算法——Z字形变换(medium)
  • Python魔法之旅-魔法方法(08)
  • BearPi-HM Nano开发笔记
  • LiveWeb前端:深度解析与挑战应对
  • net语言编程:深入探索其奥秘与挑战
  • 说说影响网络的因素
  • (ckeditor+ckfinder用法)Jquery,js获取ckeditor值
  • JS 面试题总结
  • JS创建对象模式及其对象原型链探究(一):Object模式
  • leetcode46 Permutation 排列组合
  • Python socket服务器端、客户端传送信息
  • vue.js框架原理浅析
  • windows下mongoDB的环境配置
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 百度小程序遇到的问题
  • 机器学习学习笔记一
  • 如何解决微信端直接跳WAP端
  • 小李飞刀:SQL题目刷起来!
  • 协程
  • 追踪解析 FutureTask 源码
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • ​MPV,汽车产品里一个特殊品类的进化过程
  • ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTr
  • (4) PIVOT 和 UPIVOT 的使用
  • (4)通过调用hadoop的java api实现本地文件上传到hadoop文件系统上
  • (使用vite搭建vue3项目(vite + vue3 + vue router + pinia + element plus))
  • (译) 理解 Elixir 中的宏 Macro, 第四部分:深入化
  • (转)socket Aio demo
  • (转载)Google Chrome调试JS
  • (最完美)小米手机6X的Usb调试模式在哪里打开的流程
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .NET Core 2.1路线图
  • .Net Remoting常用部署结构
  • .net 程序发生了一个不可捕获的异常
  • .NET/C# 异常处理:写一个空的 try 块代码,而把重要代码写到 finally 中(Constrained Execution Regions)
  • .net之微信企业号开发(一) 所使用的环境与工具以及准备工作
  • @Autowired多个相同类型bean装配问题
  • @property括号内属性讲解
  • @vue-office/excel 解决移动端预览excel文件触发软键盘
  • [ IOS ] iOS-控制器View的创建和生命周期
  • [1] 平面(Plane)图形的生成算法
  • [2015][note]基于薄向列液晶层的可调谐THz fishnet超材料快速开关——
  • [Android] Binder 里的 Service 和 Interface 分别是什么
  • [C++] Boost智能指针——boost::scoped_ptr(使用及原理分析)
  • [Jquery] 实现鼠标移到某个对象,在旁边显示层。
  • [LeetCode] 148. Sort List 链表排序