.netcore调试

核心转储概念

什么是核心转储?核心转储就是进程内存的快照

下面是 Linux 系统的行为与核心转储文件生成相关的一些关键方面:

默认情况下,当进程意外终止时,将生成核心转储文件。

核心转储文件名为“core”,在当前工作目录或目录中 /var/lib/systemd/coredump 创建。

尽管默认行为是操作系统生成核心转储文件,但可以覆盖 /proc/sys/kernel/core_pattern 此设置,将核心转储文件输出直接传送到另一个应用程序。

那么生成了核心转储服务,有没有管理的,那肯定是有的,比如apport捕获核心转储和调试器:

有多种工具可用于捕获核心转储文件,例如 gcore、gdb 和用于分析核心转储文件的多个工具,例如 objdump、 kdump、 gdb 和 lldb。

但是,在使用这些工具尝试执行 .NET 调试时,你会遇到一些重大困难:

  • 与在 Windows 上为 WinDbg 调试器设置符号的过程相比,配置可能很困难。
  • 核心转储文件很大,因为这些工具不知道在 .NET Core 进程中使用了哪个内存区域,并且无法将内存信息剪裁为仅需要什么。
  • 转储文件不可移植。 必须分析生成这些文件的 Linux 计算机上的转储文件。 如果要分析不同 Linux 计算机上的转储文件,则需要执行额外的步骤来为调试会话配置主机。

微软官方推荐:

Lldb 是用于分析 .NET Core 转储的建议工具。 .NET SDK 包括用于正确配置 lldb 的有用工具。 但是,必须安装至少版本 3.9 才能对 .NET Core 执行此类调试分析。

若要安装 lldb 3.9 或更高版本,请使用包管理器 (,例如: sudo apt install lldb) 。

而且微软有自己的分析工具:

  • createdump
  • dotnet-dump
  • dotnet-gcdump
  • dotnet-symbol
  • Procdump

我们来以官网的例子来作为演示学习:示例调试项目

项目地址:https://github.com/ahmetmithat/buggyamb

我这里已经用Nginx代理BuggyAmb应用服务,已经启动起来了;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Unit]
Description=BuggyAmb

[Service]
WorkingDirectory=/home/bomir/dotnetDemo/buggyamb_v1.1
ExecStart=/usr/bin/dotnet /home/bomir/dotnetDemo/buggyamb_v1.1/BuggyAmb.dll
Restart=aways
RestartSec=10
SyslogIdentifier=BuggyAmb
User=root
Environment=ASPNETCORE_ENVIRONMENT=Development
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
Environment=ASPNETCORE_URLS=http://0.0.0.0:6000
[Install]
WantedBy=multi-user.target

image-20230520203301158

image-20230520203733953

这个页面里面的异常分别是:

慢、处理异常、不处理异常、崩溃、未找到页面、批处理;

崩溃情况

这种一般是比较好排查的,我们来看一下Crash的场景,首先需要模拟下点击一下Crash2的,让程序崩溃;

首先查下日志看能给我们什么信息:

1
2
3
4
5
6
7
/*
-r:按反向顺序打印日志,以便首先列出最新日志。
--identifier:请记住 SyslogIdentifier=buggyamb-identifier 测试应用程序的服务文件中的行。 (可以使用此方法强制日志仅显示适用于有问题的应用程序的条目。)
--since:显示在指定的上一时期记录的信息。 示例: --since "10 minute ago" 或 --since "2 hour ago".
*/

journalctl -r --identifier=BuggyAmb --since "10 minute ago"

image-20230521180509144

日志信息告诉我们在Crash2.cshtml.cs的15行,我们这里都是用的官方的示例,来结合代码看是怎么崩溃的。

image-20230521180819402

image-20230521181508488

这个错误是C#中常见的空引用异常,表示你尝试访问一个空对象的属性或方法, 本例中是quote为null。

核心转储

centos 默认不开启的:

可以看下这个怎么开启的:cenot开启核心转储

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash

#me: coredumpshell.sh
### Description: enable coredump and format the name of core file on centos system

# enable coredump whith unlimited file-size for all users
echo -e "\n# enable coredump whith unlimited file-size for all users\n* soft core unlimited" >> /etc/security/limits.conf

# set the path of core file with permission 777 
cd /var/buggyamb && mkdir corefile && chmod 777 corefile

# format the name of core file.   
# %% – 符号%
# %p – 进程号
# %u – 进程用户id
# %g – 进程用户组id
# %s – 生成core文件时收到的信号
# %t – 生成core文件的时间戳(seconds since 0:00h, 1 Jan 1970)
# %h – 主机名
# %e – 程序文件名
echo -e "/var/buggyamb/corefile/core-%e-%s-%u-%g-%p-%t" > /proc/sys/kernel/core_pattern

# for centos7 system(update 2017.2.3 21:44)
echo -e "/var/buggyamb/corefile/core-%e-%s-%u-%g-%p-%t" > /etc/sysctl.conf

# suffix of the core file name
echo -e "1" > /proc/sys/kernel/core_uses_pid

把这个脚本跑一遍就可以开启了。

使用lldb

我这里使用的Ubuntu 18.04, 这里需要安装要lldb 3.9 以上的, 如果是其他linux发行版可以参考下面文档进行环境安装。

安装lldb

1
2
3
sudo apt-get update
// 这里踩坑,需要之前是华为源,现在切到阿里源才能正常安装包,否则有包的依赖处理不正确
sudo apt-get install lldb-3.9 llvm-3.9 python-lldb-3.9

image-20230521190209778

image-20230521193541420

安装完成后,必须配置 lldb,以便在打开核心转储文件时自动加载 SOS。

配置lldb

  1. 安装 dotnet 符号工具:

    dotnet tool install -g dotnet-symbol

  2. 下载目标转储文件的符号:

    dotnet-symbol <path_of_dump_file>

  3. 安装 SOS:

    1. 安装 dotnet-sos 全局工具:

      dotnet tool install -g dotnet-sos

    2. 安装 SOS:

      dotnet-sos install

下面具体展开每步的配置(因为我开始学习读别人的博客都是上面的,抄来抄去都是一样,但结果到现在是没有什么用的,实际操作很多坑而且过时,深切感受到读第一手的文档的重要性,辅以其他人的博客):

  • 安装dotnet符号工具

一般来说,我们应该将dotnet 符号工具与 dotnet-dump 和 dotnet-gcdump 工具一起安装。 如果尚未安装这些工具,请先安装它们,然后再继续。 为此,请运行 dotnet tool install -g dotnet-symbol 命令以安装 dotnet 符号。 如果尚未执行此操作,请安装 dotnet-dump 和 dotnet-gcdump。

image-20230521190831669

空空如也,看起来之前都没安装;

image-20230521193357229

现在用这个命令安装,这里会报版本兼容的错误;

当前默认是适配.net6版本的:

尝试看下面这个适配netcore3.1的这个版本:

因此,命令应该修正为指定版本:dotnet tool install --global dotnet-symbol --version 1.0.412101

同理dotnet-dump也是一样:

dotnet tool install -g dotnet-dump --version 5.0.160202

dotnet tool install -g dotnet-gcdump --version 5.0.160202

  • 下载转储文件的符号

符号文件可以充当源代码和二进制文件之间的映射。 这些映射由调试器在读取调用堆栈时用于解析函数或方法名称、源行信息或本地变量名称。

image-20230522214049976

如果未添加 --host-only 开关,则在下载符号文件时可能会收到多个“找不到 HTTP 404”错误消息。 可以安全地忽略这些消息。 该 --host-only 参数将仅下载主机程序。 这是 lldb 开始调试 ASP.NET Core应用程序所需的全部。

注意:~/dumps/dotnet/CoreDump指的是核心转储文件的路径,可以认为是一个占位符,需要替换为实际的 Core Dump 文件路径才能正常执行。

  • 安装SOS

SOS 是一个调试器扩展,使开发人员能够检查 .NET 应用程序的托管状态,包括 ASP.NET Core和其他。基于 NET 的应用程序,如 .NET WPF 和 .NET Windows 窗体。 SOS 是一个跨平台扩展,可由 WinDbg 或 Windows 上的 cdb 调试器加载,并且可由 Linux 和 macOS 上的 lldb 加载。

若要安装 SOS,必须首先安装以下 dotnet-sos 工具:

1
dotnet tool install -g dotnet-sos --version 5.0.160202

然后,安装 SOS:

1
dotnet-sos install

otnet tool install -g dotnet-gcdump –version 5.0.160202`

image-20230522214950607

现在我们终于可以使用lldb来打开转储文件了;

  • 在lldb中打开核心存储

若要打开核心转储,必须使用 lldb 和以下语法:

1
lldb --core <dump path> <host-program>

<host-program> 启动 .NET Core 应用程序的本机程序。 这通常是 dotnet,除非应用程序是自包含的。 如果这是一个独立应用程序,则此变量是应用程序的名称,没有 .dll 扩展。

假设使用相同的文件夹名称进行跟踪,则在上一部分中生成的内存转储文件的路径应为 ~/dumps/dotnet/CoreDump。 因此,你将运行 lldb --core ~/dumps/dotnet/CoreDump 以打开文件。

image-20230522220623334

  • 设置符号

回想一下,你使用 dotnet-symbol 目录中的 ~/dumps/symbols 命令下载了符号文件。 应在调试器中运行的第一个命令是将符号服务器路径设置为为符号下载设置的目录:

1
setsymbolserver -directory ~/dumps/symbols

接下来,加载符号:

image-20230522220738939

  • 运行lldb和SOS命令

有几个 lldb 命令。 可以使用 help 命令列出这些内容。 在列表中,可以看到 SOS 命令也列在 用户定义的命令下。 SOS 是 lldb 的插件。 可以使用同 help 一命令检索插件帮助信息。

首先,使用 (“后退跟踪”) 命令查看线程的本机调用 bt 堆栈。 此命令显示活动线程的调用堆栈。

image-20230522221202792

接下来,使用 SOS clrstack 命令检查托管调用堆栈:

clrstack-command

不应检索任何信息。 堆栈演练将失败,因为报告的堆栈不完整。 这与之前讨论的内容相关:自动生成的核心转储文件无法收集所有托管状态。

另请记住,发生了异常,这会导致进程崩溃。 运行 SOS pe 命令查看异常信息。

在这种情况下,异常消息是 资源暂时不可用。 但是,不会解析异常类型和函数名称。 相反,它们的值表示为 未知

还会显示异常的地址。 可以尝试将此地址作为参数传递到命令中 pe ,以查看是否提供了更多详细信息。 然后,运行 pe 00007F8244048538

unknow-exception-type

即使要显示堆栈中引用的对象,也会看到相同的 未知值。

dso-command

可以通过选择堆栈上某个对象的地址并使用 dumpobj <address> 命令查看对象来尝试检索更多信息。

dumpobj-command

但是你或许看到此命令的效果也是有限,因为它只返回更多未知消息。 是时候停止处理自动生成的转储文件了。 运行命令 exit 以结束 lldb 会话。

总之,系统生成的转储文件提供了一些信息,但仍缺少一些重要信息。

所以我们需要使用工具去捕获核心故障转储;

使用createdump工具捕获核心故障转储文件

Createdump 会与每个 .NET Core 运行时一起自动安装。

可以设置具有环境变量的配置选项。 这些将作为参数传递给创建的ump 命令。 下面是支持的环境变量:

  • COMPlus_DbgEnableMiniDump:如果设置为 1,则在终止时启用自动核心转储生成。 默认值为 0
  • COMPlus_DbgMiniDumpType:这是将要创建的微型转储文件的类型。 此值的默认值为 2 (或枚举类型 MiniDumpWithPrivateReadWriteMemory) 。 这意味着生成的转储文件将包括 GC 堆以及捕获进程中所有现有线程的堆栈跟踪所需的信息。
  • COMPlus_DbgMiniDumpName:如果设置,请用作模板来创建转储文件路径和文件名。 可以使用参数将 PID 放入名称中 %d 。 默认模板为 /tmp/coredump.%d. 通过使用此环境变量,可以配置输出目录。
  • COMPlus_CreateDumpDiagnostics:如果设置为 1,则启用创建的ump 工具诊断消息 (TRACE 宏) 。 如果 createdump 不能按预期工作并且不生成内存转储文件,则此设置可能很有用。

此处 COMPlus_DbgEnableMiniDump的重要变量 。 必须将此环境变量设置为 1。 有几种方法可以设置此环境:

  • 在应用程序的配置文件中设置它。
  • 使用该 export COMPlus_DbgEnableMiniDump=1 命令进行设置。 此设置在操作系统重启后不会保留。 因此,如果要在重启后保持启用设置,则必须将其设置为持久设置。
  • 在 ASP.NET Core服务单元文件中设置它。

在 ASP.NET Core服务单元文件中设置此变量是最简单的方法。 缺点是应重启服务。

我们打开BuggyAmb的配置文件,并添加并添加 COMPlus_DbgEnableMiniDump=1 环境变量。

image-20230522224538661

必须重新加载服务配置,因为配置已更改。 然后,重启 BuggyAmb 服务。

image-20230522224704254

进行这些更改后,重现崩溃问题。 如果 createdump 有效,则转储文件应作为 coredump 写入 /tmp/ 目录下

按照相同的步骤重现问题:

  1. 选择 “崩溃 3”。 页面加载正确,但返回一条错误消息,指示进程应该已崩溃。

  2. 选择 “慢”。 这将生成“HTTP 502”响应代码, (错误的网关错误) 而不是产品表。

    image-20230522225349308

  3. 出现问题后,不会呈现任何页面,并且你将收到相同的错误消息 10-15 秒。

  4. 10-15 秒后,应用程序开始正常工作。

现在应在 /tmp 目录中包含一个核心转储文件, 并且我们将转储文件移动到 ~/dumps/ 文件夹,以配合示例分析。 若要打开转储文件,请运行 lldb --core ~/dumps/coredump.<5172>。 在此命令中,将 5172 占位符替换为进程的 PID。。

image-20230523223811619

在这之前, 我已经打开转储文件并使用 lldb,则已设置符号并安装了 SOS。 可以打开相同的 .NET Core 版本转储文件,而无需再次下载符号。 但是,如果打开另一个尚未下载符号的 .NET Core 版本转储文件,则必须先下载该版本的符号,然后才能开始分析。

运行 SOS clrstack 命令以显示托管调用堆栈。 请记住,在使用系统生成的核心转储文件运行同一命令时,看到错误。 这一次,应会看到正确的托管调用堆栈。

image-20230523224240423

这是一个良好的开端。但是,显示的调用堆栈属于调试进程的主线程, 这并不是引发异常的线程;

接着我们需要检查所有线程,以尝试确定可在何处引发异常。

首先,使用 thread list 命令检查本机线程。列表中第一个线程附近的星号 (*) (thread #1) 指示它是活动线程。

image-20230523224838703

本机线程检查不会显示太多内容。 由于这是 .NET Core 应用程序,因此请运行 SOS clrthreads 命令来检查 CLR 线程列表。 此命令列出在应用程序中运行的托管线程。

image-20230523225258620

上面截图未显示所有托管线程。 但是,应关注的详细信息会列在截图中。 Thread #18 系统日志中已出现异常。

检查该线程的调用堆栈。 为此,必须首先选择有问题的线程。 在要运行的内存转储分析中,线程号很可能会有所不同。 若要选择另一个线程作为活动线程,请使用该 thread select 命令并传递 lldb dbg 线程 ID。 例如,运行 thread select 18 以切换到线程 18。 然后,运行的每个连续命令都将位于该线程的上下文中。 若要查看本机调用堆栈,请运行 bt (后跟踪) 命令。

image-20230523230004378

可以通过运行 clrstack 命令来检查托管调用堆栈。 然而,这不会透露太多。 运行命令 pe 以获取异常详细信息。

image-20230523230202504

image-20230523230233788

1
2
3
4
// 摘取信息
Exception type:   System.Net.WebException
Message:          The remote server returned an error: (404) Not Found.
00007FC3A0B9F0F0 00007FC3A843C85D BuggyAmb.dll!BuggyAmb.Pages.Problem.Crash3Model+<LogTheRequest>d__1.MoveNext()+0x22d

此信息指示:A System.Net.HttpWebRequest 在方法的 Crash3 页中 LogTheRequest() 触发。 这是帮助查找问题的重要信息。 但是,如果要查找 HTTP 请求的 URL,该怎么办? 若要继续,请尝试检查堆栈上引用的对象,以查看是否可以从此列表中收集更多信息。 若要显示在当前堆栈边界内找到的所有托管对象,请运行 dso

image-20230523230507744

这不起作用。 不应看到任何 System.Net.HttpWebRequest 实例。 有异常的实例,并且你已对其进行了检查。 因此,此命令未生成与原因相关的新信息。

所有托管对象都存储在托管堆中,我们可以通过运行 dumpheap来查看托管堆。 不要在没有任何参数的情况下运行 dumpheap ,因为然后命令会列出托管堆中的所有对象 (一个大列表) 。 相反,可以使用该命令获取堆的 dumpheap -stat 统计信息。

可以通过以下格式运行命令,再使用一种策略缩小统计信息范围:

dumpheap -stat -type System.Net.HttpWebRequest

以下显示托管对象的统计信息,其中包含其名称中的字符串 System.Net.HttpWebRequest

image-20230523231124042

在本例中,托管堆上只有一个 System.Net.HttpWebRequest 对象。 在上一个列表中,条目旁边 HttpWebRequest 看到的地址不是该对象在内存中的地址。 相反,它是与类型 System.Net.HttpWebRequest对象的“方法表”相对应的地址。 若要获取对象的实际列表,可以使用以下方式将该方法表 (MT) 地址传递给 dumpheap 命令:

1
dumpheap -mt <address>

例如,运行 dumpheap -mt 00007fc3a842d638 以查找对象的地址。

image-20230523231519122

现在,你能够识别有问题的对象的地址。 在此示例中,它是 00007fc2543b4468. 可以通过将该地址传递给命令来 dumpobj 调查对象的属性。 这将列出该对象的属性。 在此示例中,运行 dumpobj 00007fc2543b4468 以检查对象的属性。

image-20230523231907622

如果你正在调查一个 System.Net.HttpWebRequest 对象,其属性之一是 _requestUri。 这是类型的对象 System.Uri 。 你想要确定 URI。 因此,将属性的 _requestUri 地址传递给 dumpobj 命令

复制对象的 System.Uri 地址,然后再次使用 dumpobj 来调查它。 运行 dumpobj 00007fc2543b3768。 生成的内存转储文件中对象的地址肯定会有所不同。 该列表将显示 _string 其属性 System.Uri

image-20230523232207469

复制地址 _string,然后再次对其运行 dumpobj 命令。 运行 dumpobj 00007fc2543b36e0。 结果列在以下屏幕截图中。

image-20230523232712539

最后,可以找到 HttpWebRequest 的 URL: http://192.168.44.138/Problem/Api/NotExistingLoggingApi。 顾名思义,这可能不是应用程序中的现有页面。

最后,关于崩溃发生方式的结论如下所示:

  • HttpWebRequest 是在 Crash3 网页中对方法中的LogTheRequest()非现有 URL 进行的。
  • 在实际应用程序中,解决此问题的解决方案是处理 HttpWebRequest 错误。 但是,在这种情况下,解决方案要简单得多:不要向不存在的 HttpWebRequest 页面发出请求。