在 Unix 上调试 .NET 核心库:lldb 与 SOS 实战指南(runtime 仓库)
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
导读
本文基于 dotnet/runtime 仓库的 docs/workflow/debugging/libraries/unix-instructions.md 编写,面向需要在 Linux/macOS 等 Unix 平台上调试 .NET 核心库(CoreCLR、System.Private.CoreLib 及各托管库)的开发者,系统讲解使用 lldb + SOS 进行源码级调试、以及利用createdump生成并分析崩溃转储(core dump)的完整工作流。读完本文,你将掌握:如何安装配置 SOS 与 lldb、如何用/t:Test目标跑通测试以便附加调试、如何让 lldb 正确解析libcoreclr.so的符号、如何配置环境变量触发崩溃转储,以及如何对核心库测试崩溃产生的 dump 做栈回溯与托管状态分析。
调试方法总览
在 Unix 平台上调试 .NET 核心库,官方支持两条技术路线:
- lldb + SOS:通过 SOS 调试器扩展在 lldb 中查看托管状态(托管调用栈、对象、GC 堆等),适合日常源码级调试与测试失败分析;
- Visual Studio Code:利用 C# 调试适配器与
lldb配合的交互式调试体验(详见 debugging-vscode.md)。
本文以 lldb + SOS 为主线,因为它是分析核心库崩溃、尤其是分析 core dump 时最直接的工具链。
使用 lldb 与 SOS 调试
安装 lldb 与 SOS
在开始调试之前,需要准备两样东西:
- lldb(LLVM 调试器),用于加载可执行文件与 core dump,提供符号解析和原生层栈回溯;
- SOS 调试器扩展,负责在 lldb 内部理解 .NET 运行时与托管代码。
SOS 的安装方式在不同版本/发行版上有差异,官方安装指引见 dotnet/diagnostics 仓库的documentation/sos.md,也可以通过dotnet tool install -g dotnet-sos安装dotnet-sos全局工具,然后运行dotnet-sos install将 SOS 加载进 lldb。dotnet-sos工具同时支持dotnet-sos uninstall、dotnet-sos sethostruntime等子命令,用于管理调试器扩展的加载状态。
先用 msbuild 跑通测试(关键前置步骤)
这是整个调试流程中最容易被忽略、却至关重要的一步:在尝试附加调试之前,先用 msbuild 以/t:Test目标运行至少一次测试。
./build.sh -subset libs /t:Test /p:testscope=<库名> # 或针对具体测试项目 ./dotnet.sh build <测试项目路径> /t:Test这样做的原因是:coreclr 的库测试编译产物(包括libcoreclr.so、对应的.so.dbg符号文件以及各程序集)会被布局到特定的输出目录,而调试器后续要解析的符号路径与这些产物强相关。先跑通一次测试可以保证:
- 所有需要调试的程序集与原生符号文件已生成并处于正确位置;
- 后续用 lldb 加载时,
settings set target.exec-search-paths指向的<runtime-path>目录存在且内容完整。
仓库中相关调试说明可参考 debugging-corelib.md 与 debugging-runtime.md。
用 lldb 调试 core dump
除了附加到正在运行的进程,更常见的核心库调试场景是事后分析崩溃转储。要完成一次成功的 dump 调试,需要准备以下三样东西:
| 必备项 | 说明 |
|---|---|
| 崩溃转储文件(core dump) | 崩溃时生成的转储文件,路径与命名方式由 dump 配置决定 |
createdump工具 | 在 Linux 上,运行时内置的createdump工具可以在托管应用抛未捕获异常或发生 fault 时自动生成 core dump(详见 xplat-minidump-generation.md) |
| lldb + SOS | 已按上文安装配置好的调试工具链 |
让 lldb 正确解析 libcoreclr.so 的符号
关键一步是:加载 core dump 时,必须给 lldb 额外指定target.exec-search-paths,否则 lldb 无法定位libcoreclr.so对应的符号文件,栈回溯会退化为无符号的裸地址。
标准命令格式如下:
lldb-3.9 -O "settings set target.exec-search-paths <runtime-path>" --core <core-file-path> <host-path>三个占位符的含义:
<runtime-path>:包含libcoreclr.so.dbg(以及其余运行时与框架程序集)的目录路径;<core-file-path>:要调试的 core dump 文件路径;<host-path>:dotnet或corerun可执行文件的路径,通常就位于<runtime-path>目录中。
执行成功后,lldb 会以符号已解析的状态进入调试会话,此时bt(backtrace)可以看到带libcoreclr.so符号的栈帧。接着,只要 SOS 已按前述指引加载,就可以开始使用clrstack、pe、dumpheap等一系列 SOS 命令分析托管状态。
完整示例
原文档给出了一个来自 CI(Helix)环境的真实示例,其中 dump 是 Helix 测试机在跑System.Drawing.Common.Tests时崩溃产生的,路径结构完整展示了三个参数的实际形态:
lldb-3.9 -O "settings set target.exec-search-paths /home/parallels/Downloads/System.Drawing.Common.Tests/home/helixbot/dotnetbuild/work/2a74cf82-3018-4e08-9e9a-744bb492869e/Payload/shared/Microsoft.NETCore.App/$(ProductVersion)/" --core /home/parallels/Downloads/System.Drawing.Common.Tests/home/helixbot/dotnetbuild/work/2a74cf82-3018-4e08-9e9a-744bb492869e/Work/f6414a62-9b41-4144-baed-756321e3e075/Unzip/core /home/parallels/Downloads/System.Drawing.Common.Tests/home/helixbot/dotnetbuild/work/2a74cf82-3018-4e08-9e9a-744bb492869e/Payload/shared/Microsoft.NETCore.App/$(ProductVersion)/dotnet可以看到:
--core指向Work/.../Unzip/core,即 Helix 工作项解压目录下的 core 文件;exec-search-paths与host-path都指向Payload/shared/Microsoft.NETCore.App/$(ProductVersion)/,其中$(ProductVersion)是运行时产品版本(如9.0.0),该目录内既有dotnet宿主也有libcoreclr.so.dbg。
拿到这样的环境后,即可在 lldb 内执行clrstack等 SOS 命令查看崩溃时的托管调用栈。
深入底层:createdump 如何工作
了解 dump 的来源有助于判断调试结果的可信度。核心库崩溃转储的生成机制记录在 xplat-minidump-generation.md 中,其核心设计是:
- 当 coreclr 因为未捕获的托管异常或异步信号(
SIGSEGV、SIGILL、SIGFPE等)即将调用PROCAbort()终止进程时,会触发 dump 生成; createdump工具位于libcoreclr.so同目录,通过fork/execve启动为崩溃进程的子进程,被赋予 ptrace 权限;- 子进程用 ptrace 枚举并挂起目标进程的所有线程,收集进程/线程状态与寄存器、auxv 条目、
/proc/$pid/maps中的模块映射(即 DSO 信息,供 gdb/lldb 枚举共享模块和解析符号); - 随后加载 DAC(Data Access Component,用于离线检查运行时托管状态的专用构建),通过
ICLRDataEnumMemoryRegions接口枚举托管状态所需的内存区域,加入线程栈与 IP 周边一页代码,把字节粒度区域按页取整并合并为连续区域; - 最终按 ELF core 格式写出:主 ELF 头、每个内存区域对应一个
PT_LOADnote 条目、NT_FILE条目(由/proc/$pid/maps构建)、线程状态与寄存器,以及各内存区域的实际内容。
在仓库源码中,这一流程体现在 createdumpunix.cpp 的CreateDump()里:先crashInfo->EnumerateAndSuspendThreads()挂起线程,再GatherCrashInfo()收集信息,之后EnumerateMemoryRegionsWithDAC()借助 DAC 枚举托管内存,最后dumpWriter.WriteDump()写出 ELF dump。命令行参数的解析则在 createdumpmain.cpp 的createdump_main()中完成。
环境变量配置
转储的生成由一组DOTNET_前缀的环境变量控制,这些变量会在 PAL 层被读取(见 process.cpp 中PROCAbortInitialize()对DbgEnableMiniDump、DbgMiniDumpName、DbgMiniDumpType等配置项的解析),并作为选项传给createdump:
| 环境变量 | 作用 | 默认值 |
|---|---|---|
DOTNET_DbgEnableMiniDump | 设为1时启用崩溃转储生成 | 不生成 dump |
DOTNET_DbgMiniDumpType | 转储类型,见下表 | 2(MiniDumpWithPrivateReadWriteMemory) |
DOTNET_DbgMiniDumpName | dump 路径与文件名模板,支持格式化占位符 | /tmp/coredump.%p |
DOTNET_DbgCreateDumpToolPath | (仅 NativeAOT)自定义 createdump 工具所在目录 | 无 |
DOTNET_CreateDumpDiagnostics | 设为1启用 createdump 的诊断消息(TRACE) | 无 |
DOTNET_CreateDumpVerboseDiagnostics | 设为1启用 verbose 诊断消息(TRACE_VERBOSE) | 无 |
DOTNET_CreateDumpLogToFile | 诊断消息写入的文件路径 | 无 |
DOTNET_EnableCrashReport | 设为1且开启 MiniDump 时,额外生成 JSON 格式崩溃报告(dump 路径追加.crashreport.json) | 无 |
DOTNET_EnableCrashReportOnly | 同EnableCrashReport,但不生成 core dump,仅输出崩溃报告 | 无 |
注意:在 Docker 容器内生成 core dump 需要为容器授予 ptrace 能力(
--cap-add=SYS_PTRACE或使用--privileged)。
DOTNET_DbgMiniDumpType的取值与 Windows minidump 枚举对应:
| 值 | Minidump 枚举 | 描述 |
|---|---|---|
| 1 | MiniDumpNormal | 仅包含捕获进程所有线程栈回溯所需的信息,GC 堆内存与信息受限 |
| 2 | MiniDumpWithPrivateReadWriteMemory(默认) | 包含 GC 堆以及捕获所有线程栈回溯所需的信息 |
| 3 | MiniDumpFilterTriage | 仅包含捕获所有线程栈回溯所需信息,GC 堆内存受限 |
| 4 | MiniDumpWithFullMemory | 包含进程全部可访问内存,文件可能非常大 |
在源码中,这四类分别映射到 createdumpmain.cpp 的GetMiniDumpType(),其中Heap(默认)还会叠加MiniDumpWithDataSegs、MiniDumpWithHandleData、MiniDumpWithThreadInfo等标志位。
命令行参数
createdump通常由运行时作为崩溃进程的子进程自动启动,不能指定目标 PID(它只转储其父进程)。其命令行选项与上文-f/-h等开关一一对应(解析逻辑见 createdumpmain.cpp):
createdump [options] -f, --name <路径> - dump 路径与文件名,默认 '/tmp/coredump.%p';支持 %p/%e/%h/%t 占位符 -n, --normal - 创建 minidump -h, --withheap - 创建带堆的 minidump(默认) -t, --triage - 创建 triage minidump -u, --full - 创建完整 core dump -d, --diag - 启用诊断消息 -v, --verbose - 启用 verbose 诊断消息 -l, --logtofile - 诊断日志文件路径 --crashreport - 额外写崩溃报告(dump 路径 + .crashreport.json) --crashreportonly - 仅写崩溃报告,不生成 dump --crashthread <id> - 崩溃线程 id --signal <code> - 崩溃信号码 --singlefile - 单文件应用模型 --nativeaot - NativeAOT 应用模型dump 文件名模板支持的格式化占位符(与 Linux core(5) 模式的子集一致):
| 占位符 | 含义 |
|---|---|
%% | 字面%字符 |
%d/%p | 被转储进程的 PID |
%e | 进程可执行文件名 |
%h | gethostname()返回的主机名 |
%t | dump 时间,自 Unix 纪元(1970-01-01 UTC)起的秒数 |
常用调试流程总结
综合原文档与仓库实现,一次典型的 Unix 核心库崩溃调试流程为:
- 复现与收集:在复现崩溃前设置
DOTNET_DbgEnableMiniDump=1(必要时调整DOTNET_DbgMiniDumpType与DOTNET_DbgMiniDumpName),运行测试复现,得到/tmp/coredump.<pid>及配套的.crashreport.json(若开启); - 准备工具链:确认 lldb 与 SOS 已安装,并用 msbuild
/t:Test至少成功运行过一次被测测试,确保符号文件(libcoreclr.so.dbg)与程序集产物齐备; - 加载转储:按上文格式执行
lldb -O "settings set target.exec-search-paths <runtime-path>" --core <core-file> <host-path>; - 分析:先用 lldb 原生命令确认符号已解析(
bt),再用 SOS 命令(clrstack、clrthreads、pe等)分析托管层调用栈与对象状态; - 对照源码:结合 createdump 源码 与 转储生成设计文档 理解 dump 内容的覆盖范围,判断分析结论的完备性。
关于 SOS 命令集的更多用法(如dumpheap、gcroot、soshelp),可参考仓库内 debugging-corelib.md 等调试文档,以及 diagnostics 仓库的 core dump 调试文档。
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考