一:背景
1. 讲故事
微信上有位朋友找到我,说他们部署在linux上的 .net 程序会隔几天崩溃一次,一直找不到原因,让我帮忙看下怎么回事,让朋友用 procdump 抓了一个dump下来,然后就是正式的分析啦。
二:崩溃分析
1. 为什么会崩溃
拿到dump之后,双击dump打开,会看到程序崩溃的原因,参考如下:
C#
(1.d): Signal SIGSEGV (Segmentation fault) code SEGV_MAPERR (Address not mapped to object) at 0x108
libc_so!wait4+0x57:
00007f44`37aa5c17 483d00f0ffff cmp rax,0FFFFFFFFFFFFF000h
从卦中可以看到如下几点信息:
- 1.d 表示 d 号线程出现了崩溃。
- SIGSEGV 表示经典的 段错误,用 Windows 的话术就是访问违例。
- SEGV_MAPERR 表示mapper错误,即当前地址无法映射到有效内存。
- 0x108 当前访问的地址。
既然都说到 d 号线程了,接下来就是切过去看看,参考输出如下:
C#
0:007> ~~[d]s
libc_so!wait4+0x57:
00007f44`37aa5c17 483d00f0ffff cmp rax,0FFFFFFFFFFFFF000h
0:007> k
# Child-SP RetAddr Call Site
00 00007f44`367d1cd0 00007f44`37851c05 libc_so!wait4+0x57
01 00007f44`367d1d00 00007f44`37852b40 libcoreclr!PROCCreateCrashDump+0x275 [/__w/1/s/src/coreclr/pal/src/thread/process.cpp @ 2307]
02 00007f44`367d1d60 00007f44`3782518e libcoreclr!PROCCreateCrashDumpIfEnabled+0x770 [/__w/1/s/src/coreclr/pal/src/thread/process.cpp @ 2524]
03 00007f44`367d1df0 00007f44`37824765 libcoreclr!invoke_previous_action+0x10e [/__w/1/s/src/coreclr/pal/src/exception/signal.cpp @ 397]
04 00007f44`367d1e30 00007f44`37a0e050 libcoreclr!sigsegv_handler+0x1d5 [/__w/1/s/src/coreclr/pal/src/exception/signal.cpp @ 631]
05 00007f44`367d2ac0 00007f44`37754e2a libc_so!_sigaction+0x40
06 00007f44`368d2830 00007f44`375109c3 libcoreclr!CustomAssemblyBinder::PrepareForLoadContextRelease+0xa [/__w/1/s/src/coreclr/binder/customassemblybinder.cpp @ 222]
07 00007f44`368d2850 00007f44`1adbe9c5 libcoreclr!AssemblyNative_PrepareForAssemblyLoadContextRelease+0x93 [/__w/1/s/src/coreclr/inc/clrtypes.h @ 1263]
08 00007f44`368d28e0 00007f44`224242cd System_Private_CoreLib!System.Runtime.Loader.AssemblyLoadContext.InitiateUnload+0xe5 [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @ 151]
09 00007f44`368d29b0 00007f44`376de496 System_Private_CoreLib!System.Runtime.Loader.AssemblyLoadContext.Finalize+0x2d [/_/src/libraries/System.Private.CoreLib/src/System/Runtime/Loader/AssemblyLoadContext.cs @ 124]
0a 00007f44`368d29d0 00007f44`3749b8a3 libcoreclr!FastCallFinalizeWorker+0x6 [/__w/1/s/src/coreclr/vm/amd64/calldescrworkeramd64.S @ 31]
0b (Inline Function) --------`-------- libcoreclr!FastCallFinalize+0x4a [/__w/1/s/src/coreclr/vm/methodtable.cpp @ 4771]
0c 00007f44`368d29e0 00007f44`375540f5 libcoreclr!MethodTable::CallFinalizer+0x253 [/__w/1/s/src/coreclr/vm/spinlock.h @ 4889]
0d (Inline Function) --------`-------- libcoreclr!CallFinalizer+0x58 [/__w/1/s/src/coreclr/vm/finalizerthread.cpp @ 75]
0e 00007f44`368d2a40 00007f44`37554345 libcoreclr!FinalizerThread::FinalizeAllObjects+0xc5 [/__w/1/s/src/coreclr/inc/volatile.h @ 104]
0f 00007f44`368d2a80 00007f44`374e5b75 libcoreclr!FinalizerThread::FinalizerThreadWorker+0x95 [/__w/1/s/src/coreclr/inc/volatile.h @ 354]
10 (Inline Function) --------`-------- libcoreclr!ManagedThreadBase_DispatchInner+0x2 [/__w/1/s/src/coreclr/vm/threads.cpp @ 7222]
11 (Inline Function) --------`-------- libcoreclr!ManagedThreadBase_DispatchMiddle+0x3d [/__w/1/s/src/coreclr/vm/util.hpp @ 7266]
12 (Inline Function) --------`-------- libcoreclr!<unnamed-class>::operator()+0x3d [/__w/1/s/src/coreclr/vm/util.hpp @ 7424]
13 (Inline Function) --------`-------- libcoreclr!<unnamed-class>::operator()+0xa9 [/__w/1/s/src/coreclr/vm/util.hpp @ 7426]
14 00007f44`368d2cd0 00007f44`374e619d libcoreclr!ManagedThreadBase_DispatchOuter+0x135 [/__w/1/s/src/coreclr/vm/util.hpp @ 7450]
15 (Inline Function) --------`-------- libcoreclr!ManagedThreadBase_NoADTransition+0x18 [/__w/1/s/src/coreclr/vm/threads.cpp @ 7494]
16 00007f44`368d2de0 00007f44`375545e8 libcoreclr!ManagedThreadBase::FinalizerBase+0x2d [/__w/1/s/src/coreclr/vm/threads.cpp @ 7514]
17 00007f44`368d2e10 00007f44`3785476e libcoreclr!FinalizerThread::FinalizerThreadStart+0x58 [/__w/1/s/src/coreclr/vm/finalizerthread.cpp @ 403]
18 00007f44`368d2e30 00007f44`37a5b1f5 libcoreclr!CorUnix::CPalThread::ThreadEntry+0x1fe [/__w/1/s/src/coreclr/pal/inc/pal.h @ 1763]
19 00007f44`368d2ee0 00007f44`37adab00 libc_so!pthread_condattr_setpshared+0x515
1a 00007f44`368d2f80 ffffffff`ffffffff libc_so!_clone+0x40
1b 00007f44`368d2f88 00000000`00000000 0xffffffff`ffffffff
从卦象看是终结器线程正在调用 AssemblyLoadContext 的析构函数,在coreclr层的 PrepareForLoadContextRelease 函数中抛出了访问违例,这段代码很明显犯了编程的一个大忌,即不手工调用Dispose,而是依赖终结器线程的兜底,导致灾难的发生,
不过按理说这些代码都是固若金汤,抛异常也是有点奇葩。。。
2. 为什么会抛出异常
要想找到这个答案,可以借助可视化的VS面板,将 dump 拖到 VS 中,在线程面板中找到 终结器线程
,然后观察 InitiateUnload 方法的代码逻辑,可以清楚的看到然来是 _nativeAssemblyLoadContext 字段为 null 导致的,截图如下:

观察源代码发现 _nativeAssemblyLoadContext 是 coreclr 对外提供操作的句柄,它的赋值是在 AssemblyLoadContext 初始化构造时,截图如下:

说实话看到这个源头就蒙圈了,_nativeAssemblyLoadContext
居然还有null的情况,这也就说明 InitializeAssemblyLoadContext 函数有为null的情况,签名如下:
C#
[DllImport("QCall", CharSet = CharSet.Unicode)]
private static extern IntPtr InitializeAssemblyLoadContext(IntPtr ptrAssemblyLoadContext, bool fRepresentsTPALoadContext, bool isCollectible);
3. 接下来怎么办
针对 _nativeAssemblyLoadContext=null
这种奇葩情况,我个人提供两种方案。
1) 使用 using 替代 兜底线程
如果调用线程执行了错误的 _nativeAssemblyLoadContext
,那最多就是抛个异常,不会导致程序崩溃,相反如果让终结器线程崩溃了,那就是大大的一个灾难。无法挽回。
2) 使用 harmony 跟踪
如果你是一个极客,一定要抓到 _nativeAssemblyLoadContext=null
时的调用栈,可以使用 harmony 进行实时跟踪,即对 AssemblyLoadContext
构造函数进行注入,在后缀补丁中获取 _nativeAssemblyLoadContext 值即可,这里借助上一篇的 CustomAssemblyLoadContext 代码例子,参考代码如下:
C#
[HarmonyPatch(typeof(AssemblyLoadContext), MethodType.Constructor, new Type[] { typeof(string), typeof(bool) })]
public class AssemblyLoadContextHook
{
// 后缀补丁 - 在原始方法执行后运行
public static void Postfix(AssemblyLoadContext __instance, IntPtr ____nativeAssemblyLoadContext)
{
Console.WriteLine("----------------------------");
long addr = (____nativeAssemblyLoadContext == IntPtr.Zero) ? 0 : ____nativeAssemblyLoadContext.ToInt64();
Console.WriteLine($"____nativeAssemblyLoadContext: 0x{addr:X}");
Console.WriteLine(JsonConvert.SerializeObject(__instance));
Console.WriteLine("----------------------------");
Console.WriteLine(Environment.StackTrace);
}
}

从卦中可以清晰的看到 new AssemblyLoadContext
之后的类型信息,并记录了当前的调用栈,一旦有 null 出现的时候,是不是一下子就缩小了包围圈哈。。。
三:总结
这次生产事故也强烈的警示了大家,能用 using 就不要让 终结器线程 兜底,后者一旦崩溃就会酿成灾难性后果。