一:背景 1. 講故事 前些天有位朋友找到我,說他的軟體在客戶那邊不知道什麼原因崩掉了,從windows事件日誌看崩潰在 clr 里,讓我能否幫忙定位下,dump 也抓到了,既然dump有了,接下來就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼崩潰在 clr 一般來說崩潰在clr ...
一:背景
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 Context Domain Count Apt Exception
0 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 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 [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 0c746012
0: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 esi
0: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 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> !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不住,無語了。