超凡入圣 发表于 2024-5-31 15:25:26

记一次 .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:   no
                                                                         Lock
       ID OSID ThreadOBJ    State GC Mode   GC Alloc ContextDomain   Count Apt Exception
   0    1 52e8 18998d50   24220 Preemptive639B0D58: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=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00010246
clr!VirtualCallStubManager::ResolveWorker+0x33:
552bfff1 8bb968020000    mov   edi,dword ptr ds:002b:00000268=????????
Resetting default scope

READ_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 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
0c7400400c746012

0:000> u 0c746012
0c746012 50            push    eax
0c746013 6800000300      push    30000h
0c746018 e9d3a6b748      jmp   clr!ResolveWorkerAsmStub (552c06f0)
0c74601d 0000            add   byte ptr ,al
0c74601f 0000            add   byte ptr ,al
0c746021 005068          add   byte ptr ,dl
0c746024 0000            add   byte ptr ,al
0c746026 46            inc   esi

0:000> dp 0c746012 L1
0c74601200006850对比刚才的代码既然都返回来了 SK_LOOKUP 那为什么还是 SK_UNKNOWN 呢? 这个也可以通过在线程栈上找到 &stubKind 变量得到验证。
0:000> uf 552c0698
...
clr!VSD_ResolveWorker+0x1ab:
552c065f 8b85e0ffffff    mov   eax,dword ptr
552c0665 83a5ecffffff00and   dword ptr ,0
552c066c 8d95ecffffff    lea   edx,
552c0672 8b08            mov   ecx,dword ptr
552c0674 e858feffff      call    clr!VirtualCallStubManager::FindStubManager (552c04d1)
552c0679 ffb5ecffffff    push    dword ptr
552c067f 51            push    ecx
552c0680 8bcc            mov   ecx,esp
552c0682 8931            mov   dword ptr ,esi
552c0684 ffb5e8ffffff    push    dword ptr
552c068a 8d8de0ffffff    lea   ecx,
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
0115afe800000000我感觉这逻辑也只有clr团队帮忙解释,我已经搞不清楚了,接下来我们回头看托管方法,看能不能继续下去。
3. 在托管层寻找突破口

高级调试就是这样,一个方向走不通就需要在另一个方向上突破,接下来使用 !clrstack 观察一下。
0:000> !clrstack
OS Thread Id: 0x52e8 (0)
Child SP       IP Call Site
0115af50 775c2aac
0115afac 775c2aac xxx.GetListDrawerType(System.String)
0115b02c 28a3a949 xxx.PluginInvoker.InvokeMothod[](System.String, System.Object[])
0115b0b0 28a3a8bd xxx.xxx.OnFinishSizeCheck(Int64)
...从调用栈来看,貌似是用反射来实现功能增强,不管怎么说先看下xxxCheck 方法干了什么?简化后的代码如下:
public string OnFinishSizeCheck(long uuid)
{
    return PluginInvoker.InvokeMothod<string>("xxxCheck", new object { 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
0c73a4e8400000a      4 ....AppDomainManager0 instance 1f42236c appDomainManager
0c73a2dc4000009       18 ..., xxx]]0   static 1f422214 lazy

0: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 Table
   Entry MethodDe    JIT Name
   ...
0c6c3400 0c73a110    JIT xxx.xxx.InvokeMothod(System.String, System.Object[])

0:000> !dopoi(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不住,无语了。

来源:https://www.cnblogs.com/huangxincheng/p/18224311
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 记一次 .NET某工业设计软件 崩溃分析