古修蟑 发表于 2025-9-23 20:11:48

记一次 .NET 某CRM物流行业管理系统 崩溃分析

一:背景

1. 讲故事

微信上有位朋友找到我,说他们部署在linux上的 .net 程序会隔几天崩溃一次,一直找不到原因,让我帮忙看下怎么回事,让朋友用 procdump 抓了一个dump下来,然后就是正式的分析啦。
二:崩溃分析

1. 为什么会崩溃

拿到dump之后,双击dump打开,会看到程序崩溃的原因,参考如下:
(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 号线程了,接下来就是切过去看看,参考输出如下:
0:007> ~~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
02 00007f44`367d1d60 00007f44`3782518e   libcoreclr!PROCCreateCrashDumpIfEnabled+0x770
03 00007f44`367d1df0 00007f44`37824765   libcoreclr!invoke_previous_action+0x10e
04 00007f44`367d1e30 00007f44`37a0e050   libcoreclr!sigsegv_handler+0x1d5
05 00007f44`367d2ac0 00007f44`37754e2a   libc_so!_sigaction+0x40
06 00007f44`368d2830 00007f44`375109c3   libcoreclr!CustomAssemblyBinder::PrepareForLoadContextRelease+0xa
07 00007f44`368d2850 00007f44`1adbe9c5   libcoreclr!AssemblyNative_PrepareForAssemblyLoadContextRelease+0x93
08 00007f44`368d28e0 00007f44`224242cd   System_Private_CoreLib!System.Runtime.Loader.AssemblyLoadContext.InitiateUnload+0xe5
09 00007f44`368d29b0 00007f44`376de496   System_Private_CoreLib!System.Runtime.Loader.AssemblyLoadContext.Finalize+0x2d
0a 00007f44`368d29d0 00007f44`3749b8a3   libcoreclr!FastCallFinalizeWorker+0x6
0b (Inline Function) --------`--------   libcoreclr!FastCallFinalize+0x4a
0c 00007f44`368d29e0 00007f44`375540f5   libcoreclr!MethodTable::CallFinalizer+0x253
0d (Inline Function) --------`--------   libcoreclr!CallFinalizer+0x58
0e 00007f44`368d2a40 00007f44`37554345   libcoreclr!FinalizerThread::FinalizeAllObjects+0xc5
0f 00007f44`368d2a80 00007f44`374e5b75   libcoreclr!FinalizerThread::FinalizerThreadWorker+0x95
10 (Inline Function) --------`--------   libcoreclr!ManagedThreadBase_DispatchInner+0x2
11 (Inline Function) --------`--------   libcoreclr!ManagedThreadBase_DispatchMiddle+0x3d
12 (Inline Function) --------`--------   libcoreclr!<unnamed-class>::operator()+0x3d
13 (Inline Function) --------`--------   libcoreclr!<unnamed-class>::operator()+0xa9
14 00007f44`368d2cd0 00007f44`374e619d   libcoreclr!ManagedThreadBase_DispatchOuter+0x135
15 (Inline Function) --------`--------   libcoreclr!ManagedThreadBase_NoADTransition+0x18
16 00007f44`368d2de0 00007f44`375545e8   libcoreclr!ManagedThreadBase::FinalizerBase+0x2d
17 00007f44`368d2e10 00007f44`3785476e   libcoreclr!FinalizerThread::FinalizerThreadStart+0x58
18 00007f44`368d2e30 00007f44`37a5b1f5   libcoreclr!CorUnix::CPalThread::ThreadEntry+0x1fe
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的情况,签名如下:
       
        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 代码例子,参考代码如下:
    { 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 就不要让 终结器线程 兜底,后者一旦崩溃就会酿成灾难性后果。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 记一次 .NET 某CRM物流行业管理系统 崩溃分析