崩溃日志是怎么生成的?

前言

为什么我们程序崩溃后,会有崩溃日志呢?

初学者可能会想,如果进程都直接崩溃了,那不就什么都没了?

但是我们忽略了一点,那就是我们的进程是谁杀死的?

是cpu吗?cpu 似乎不管进程这个概念,只是一味地执行指令。

那么进程这个概念是在操作系统中的概念,那么杀死进程的是我们的操作系统。

我们的操作系统可以告诉我们这个进程为什么被杀死,比如内存不足了,比如死锁?还是其他原因,这个似乎是ok的。

那么来看一下细节吧。

正文

  1. 崩溃瞬间的捕获机制

a. 当进程触发非法操作(如段错误/除零异常)时,CPU会立即产生硬件中断

b. 操作系统预先注册的中断处理程序(如Linux的do_page_fault())会接管控制权

c. 此时崩溃进程被冻结,但操作系统内核仍正常运转

  1. 日志写入的保障体系

a. 内核态日志缓冲区:崩溃信息先存入内核的ring buffer(如Linux的kmsg),这个区域与用户进程内存隔离

b. 同步写入策略:内核直接调用文件系统驱动,绕过用户态缓存(采用O_SYNC方式写入磁盘)

c. 预留磁盘空间:现代系统通常为日志文件保留固定磁盘区块(如journald的持久化存储)

  1. 关键数据采集阶段

a. 寄存器快照:第一时间保存RIP/EIP等寄存器值以定位崩溃点

b. 内存映射备份:记录/proc/pid/maps内容以重建虚拟内存布局

c. 信号上下文:保存引发崩溃的signal number和siginfo_t结构体

  1. 跨进程协作设计

a. 日志守护进程:如syslogd/journald通过netlink socket实时接收内核通知

b. 核心转储机制:通过管道将core dump传递给abrtd等服务(即使原进程已终止)

c. 双重缓冲技术:内存缓冲+磁盘缓冲确保极端情况下数据不丢失

// 内核处理段错误的简化流程
void do_page_fault(...) {
    if (user_mode(regs)) {
        char buf[256];
        snprintf(buf, sizeof(buf), "Process %s[%d] segfault at %lx",
                 current->comm, current->pid, address);
        kmsg_write(buf);  // 直接写入内核日志缓冲区
        
        // 触发核心转储
        if (current->signal->rlim[RLIMIT_CORE].rlim_cur > 0) {
            do_coredump(...);
        }
    }
    die_if_kernel(...);
}
  1. 硬件辅助支持

a. MCA架构:现代CPU的Machine Check Architecture可记录硬件级错误

b. NMI中断:不可屏蔽中断保证在最严重崩溃时仍能执行日志代码

c. TPM芯片:部分服务器通过可信平台模块存储崩溃指纹

把具体流程记一下:

  1. T0: CPU 检测到非法指令(如 mov [0], eax)

  2. T0+1ns: 触发 #PF 异常,硬件自动保存 RIP 到 CR2 寄存器

  3. T0+10μs: 内核的 do_page_fault 开始执行

  4. T0+100μs: 调用 printk 写入内核日志缓冲区

  5. T0+1ms: systemd-journald 从 netlink 读取日志并存入 /var/log/journal

  6. T0+50ms: 核心转储通过管道传递给 abrtd 服务

那么这个abrtd服务是干什么呢?

ABRT 全称脚本 automatic bug reportig tool, 自动bug报告工具。

image

# 检查服务状态
systemctl status abrtd

# 启用服务(开机自启)
sudo systemctl enable --now abrtd

# 手动触发崩溃收集测试(测试用)
kill -ABRT $$

image

  1. 核心功能

a. 自动崩溃检测:监控系统信号(如 SIGSEGV、SIGABRT)和内核通知。
b. 数据收集:保存崩溃时的核心转储(core dump)、堆栈跟踪、寄存器状态、环境变量等。
c. 报告生成:结构化存储崩溃信息,支持本地分析或上报至开发者(如 Red Hat Bugzilla)。
d. 用户通知:通过桌面弹窗或日志告知用户崩溃事件(需图形环境支持)。

处理流程:

a. 崩溃触发:应用程序因非法操作(如空指针访问)崩溃,内核发送信号(如 SIGSEGV)。
b. 事件捕获

abrtd 通过以下方式捕获事件:

监听 /proc/sys/kernel/core_pattern 管道(如 |/usr/libexec/abrt-hook-ccpp %s %c %p %u %g %t e %P %I %h)。

解析内核日志(dmesg)或进程退出状态。

c. 数据收集

在 /var/spool/abrt/ 下创建目录,保存以下文件:

/var/spool/abrt/ccpp-2024-06-30-15:00:00-12345/
├── coredump      # 核心转储(二进制内存镜像)
├── backtrace     # 堆栈回溯(gdb生成)
├── cmdline       # 进程启动命令
├── environ       # 环境变量
└── package       # 关联的软件包(如nginx-1.18.0-3.el8)
  1. 后续处理

本地分析(通过 abrt-cli 或 gnome-abrt)。

自动上报至配置的远程服务器(需用户授权)。

那么有时候为什么我们c#的时候通过journalctl能够查到具体的错误呢?

理论上不应该啊,比如触发了cpu的问题,然后cpu告诉操作系统,操作系统怎么能够捕获到c#的详细错误呢?

操作系统能告知的只有错误码,比如说被除数是0,又或者内存不足oom了。

这些似乎是操作系统能够告知的, 其他的平台信息是操作系统如何知道的,那么问题就回到了操作系统是怎么和.net 平台就行交互的。

(1).NET 运行时与操作系统的交互
未处理异常:当 C# 程序抛出未捕获的异常时,.NET 运行时(如 dotnet 或 mono)会:

  1. 调用 libc 的 abort() 或触发 SIGABRT/SIGSEGV 信号
  2. 通过 stderr 输出异常堆栈(如果未重定向)

信号传递:操作系统内核捕获信号后,将进程崩溃事件记录到系统日志(通过 printk 或 syslog)

(2)日志传递路径

flowchart LR
    C#程序 -->|抛出异常| .NET运行时 -->|调用abort()| libc -->|触发SIGABRT| 内核 -->|记录到kmsg| journald --> journalctl

请看我们这里的journald是捕获内核信息的,是一些操作系统的内核错误信息的,但是我们好像在journald 能查看到stderr,也就是错误标准输出。

这是为啥呢? 有没有可能是改变了stderr的输出位置呢?

(1)C# 程序输出异常到 stderr
当发生未处理异常时,.NET 运行时会默认将异常信息写入标准错误流(stderr):

// CLR 内部行为(伪代码)
Console.Error.WriteLine($"Unhandled Exception: {exception.ToString()}");
Environment.Exit(1); // 或调用 abort() 触发 SIGABRT

(2)Docker/Systemd 捕获 stderr
若程序在终端中运行:stderr 直接输出到终端,但不会被 journald 捕获。

若程序由 systemd 或 Docker 管理:

systemd 服务单元:自动将 stdout/stderr 重定向到 journald。

Docker 默认配置:日志驱动(如 json-file)会捕获 stderr,但需配置 journald 驱动才能转发到 journald。

(3)journald 接收日志
日志来源:

应用程序:通过 sd_journal_print() 或 stderr 重定向。

系统服务:如 Docker 的 journald 驱动。

结构化存储:

# 查看日志的原始字段
journalctl -o json-pretty CONTAINER_NAME=myapp

输出示例:

{
  "__MONOTONIC_TIMESTAMP": "1234567890",
  "_TRANSPORT": "stderr",
  "MESSAGE": "Unhandled Exception: System.NullReferenceException...",
  "CONTAINER_ID": "abcd1234",
  "_PID": "5678"
}

(4)journalctl 过滤显示

通过字段匹配查询特定异常:

journalctl _TRANSPORT=stderr | grep "Unhandled Exception"
  1. 不同场景下的日志传递

场景 1:原生 systemd 服务

# /etc/systemd/system/myapp.service
[Service]
ExecStart=/usr/bin/dotnet /app/MyApp.dll
StandardError=journal  # 显式重定向 stderr 到 journald

此时所有 stderr 输出(包括未处理异常)会被 journald 捕获。

场景 2:Docker 容器

# 使用 journald 日志驱动运行容器
docker run --log-driver=journald myapp

场景 3:直接终端运行

dotnet MyApp.dll  # 直接输出到终端,journald 不捕获

需手动重定向:

dotnet MyApp.dll 2>&1 | systemd-cat -t myapp

暂时先这样吧,简单介绍一下崩溃日志是怎么产生的,又是为什么我们能够看到的。

posted @ 2025-07-02 17:17  敖毛毛  阅读(102)  评论(0)    收藏  举报