如何解决一个内核崩溃问题

通过 kdump 获取内核崩溃现场

内核崩溃的时候如果不能通过管理控制台获取最后的现场信息,可以通过 kdump 工具来转储崩溃现场。

kdump is a feature of the Linux kernel that creates crash dumps in the event of a kernel crash. When triggered, kdump exports a memory image (also known as vmcore) that can be analyzed for the purposes of debugging and determining the cause of a crash.

–- https://en.wikipedia.org/wiki/Kdump_(Linux)

CentOS 系统一般预装了 kdump ,没有的话可以通过以下命令安装。

yum install kexec-tools

修改内核启动参数添加 kdump 配置。

# 修改 /etc/default/grub 中的内核启动参数,添加下面 crashkernel 参数
GRUB_CMDLINE_LINUX="... crashkernel=auto"

重新生成 grub2 配置:

grub2-mkconfig -o /boot/grub2/grub.cfg
reboot

重启确认配置是否成功(成功后内核参数会多出上面新加的 crashkernel 参数)。

# cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.19.113-88.8bs.el7.x86_64 ... crashkernel=auto ...

确认 kdump 服务是否正常

# systemctl status kdump
● kdump.service - Crash recovery kernel arming
Loaded: loaded (/usr/lib/systemd/system/kdump.service; enabled; vendor preset: enabled)
Active: active (exited) since Tue 2024-05-14 10:36:56 CST; 1h 15min ago
...

有些时候 crashkernel=auto 参数会失败,这个时候可以尝试将 auto 改成具体的内存大小。

一个真实的崩溃现场

启用了 kdump 之后,每次内核崩溃,kdump 会在 /var/crash 下创建一个新的目录用来保存崩溃的现场信息,每个现场一般两个文件,一个 core 文件 vmcore ,一个崩溃时的 dmesg 文件。

# ll /var/crash/127.0.0.1-2023-12-20-10\:51\:07/
total 160112
-rw------- 1 root root 81906871 Dec 20 10:51 vmcore
-rw-r--r-- 1 root root   137943 Dec 20 10:51 vmcore-dmesg.txt

首先查看 dmesg 文件,一般崩溃通过 dmesg 就可以定位。

$ cat /var/crash/127.0.0.1-2023-12-20-10\:51\:07/vmcore-dmesg.txt
...
[ 1823.251269] RIP: 0010:iptunnel_xmit+0x17d/0x1c0
[ 1823.253371] Code: ea 4c 89 e7 e8 d4 12 fb ff 48 85 ed 74 25 83 e0 fd 75 31 83 fb 00 7e 2a 48 8b 85 f0 04 00 00 65 48 03 05 ae 63 77 46 48 63 db <48> 83 40 10 01 48 01 58 18 48 83 c4 20 5b 5d 41 5c 41 5d 41 5e 41
...
[ 1823.280234] Call Trace:
[ 1823.281826]  <IRQ>
[ 1823.283814]  send4+0xf2/0x1b0 [XxxWan]
[ 1823.285593]  hfunc_out6+0x2f7/0x510 [XxxWan]
[ 1823.289267]  ? ipt_do_table+0x351/0x680
[ 1823.291741]  nf_hook_slow+0x3d/0xb0
[ 1823.293875]  ip6_xmit+0x332/0x5e0
[ 1823.295120]  ? neigh_key_eq128+0x30/0x30
[ 1823.296623]  ? inet6_csk_route_socket+0x158/0x250
[ 1823.298286]  ? __kmalloc_node_track_caller+0x5d/0x280
[ 1823.299848]  inet6_csk_xmit+0x91/0xe0
[ 1823.301455]  __tcp_transmit_skb+0x536/0x9f0
...
[ 1823.367165]  </IRQ>
...

能从 dmesg 中获取到的有用信息:

  1. RIP 是程序指令指针(Instruction Pointer Relative),指向下一条即将被执行的指令,在上面 dmesg 中,RIP 指向 iptunnel_xmit 函数的 0x17d 偏移处。

  2. Code 行是内核崩溃时正在执行前后的指令 dump,其中 <48> 为当前 RIP 指向的指令处。

  3. 最后 Call Trace 后面是内核崩溃时的函数调用栈,从调用栈可以看出内核是崩溃在了 XxxWan 模块的 send4 函数中,调用栈缺少 send4 函数到 iptunnel_xmit 的调用过程,并不完全。

反编译 vmlinux 获取崩溃的代码行

系统日常启动加载的内核镜像(也就是内核的可执行文件)位于 /boot/ 目录下,比如 /boot/vmlinuz-4.19.113-88.8bs.el7.x86_64 ,这个内核镜像是去除了调试信息并压缩了的。各种内核调试工具需要用到是未压缩的原始 vmlinux 文件,这个文件一般在内核的 debuginfo 包中,安装。

对比可以看出未压缩的 vmlinux 镜像要比压缩后的 vmlinuz 镜像大两个数量级。

# ll -h /boot/vmlinuz-4.19.113-88.8bs.el7.x86_64
-rwxr-xr-x 1 root root 8.4M Nov  4  2022 /boot/vmlinuz-4.19.113-88.8bs.el7.x86_64
# ll -h /usr/lib/debug/lib/modules/$(uname -r)/vmlinux
-rw-r--r-- 1 root root 495M Dec 22 14:59 /usr/lib/debug/lib/modules/4.19.113-88.8bs.el7.x86_64/vmlinux

有了 vmlinux,就可以通过 objdump -d 反编译内核,根据前面 dmesg 信息,内核崩溃点在 iptunnel_xmit 函数中,所以先获取 iptunnel_xmit 的起始地址( objdump 不支持直接反编译某一个函数),然后从这个起始地址开始反编译内核并打印出汇编代码对应的 c 代码行号。

$ objdump -d /usr/lib/debug/lib/modules/$(uname -r)/vmlinux | grep iptunnel_xmit
ffffffff81898c00 <iptunnel_xmit>:
...
$ objdump -d --start-address=0xffffffff81898c00 --line-numbers  /usr/lib/debug/lib/modules/$(uname -r)/vmlinux
/usr/lib/debug/lib/modules/4.19.113-88.8bs.el7.x86_64/vmlinux: file format elf64-x86-64
Disassembly of section .text:

...
ffffffff81898c00 <iptunnel_xmit>:
iptunnel_xmit():
...
/usr/src/debug/kernel-alt-4.19.113/linux-4.19.113-88.8bs.el7.x86_64/include/net/ip_tunnels.h:458
ffffffff81898d6b:  48 8b 85 f0 04 00 00    mov    0x4f0(%rbp),%rax
ffffffff81898d72:  65 48 03 05 ae 63 77    add    %gs:0x7e7763ae(%rip),%rax        # f128 <this_cpu_off>
ffffffff81898d79:  7e
/usr/src/debug/kernel-alt-4.19.113/linux-4.19.113-88.8bs.el7.x86_64/include/net/ip_tunnels.h:461
ffffffff81898d7a:  48 63 db                movslq %ebx,%rbx
/usr/src/debug/kernel-alt-4.19.113/linux-4.19.113-88.8bs.el7.x86_64/include/net/ip_tunnels.h:462
ffffffff81898d7d:  48 83 40 10 01          addq   $0x1,0x10(%rax)
👆 RIP
...

iptunnel_xmit 函数 0x17d 偏移处为地址 ffffffff81898d7d

>>> hex(0xffffffff81898c00+0x17d)
0xffffffff81898d7dL

结合反编译内核得到的信息,可以获取到正在执行的代码位于 ip_tunnels.h 文件的 461 行。对应的 c 代码如下:

// ip_tunnels.h
455 static inline void iptunnel_xmit_stats(struct net_device *dev, int pkt_len)
456 {
457   if (pkt_len > 0) {
458    struct pcpu_sw_netstats *tstats = get_cpu_ptr(dev->tstats);
459
460     u64_stats_update_begin(&tstats->syncp);
461     tstats->tx_bytes += pkt_len;
...
475 }

自此分析结束,后面就是结合内核以及 XxxWan 的代码来看为什么这个地方会崩溃。

如果问题更复杂,还可以使用 crash 工具(类似 gdb)来从 core 文件中获取更多现场信息。

crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/.../vmcore

crash 比较耗内存,在本次崩溃的小虚拟机上跑不起来报 crash: cannot allocate any more memory! 错误,以后有机会再说,详细可以参见: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/kernel_administration_guide/kernel_crash_dump_guide#sect-crash-running-the-utility