通过qemu和crash分析崩溃内核

背景

        在产品研发过程中,有时候会遇到linux内核崩溃,在内核崩溃的时候一般会打印出奔崩位置的上下文、调用栈等。一般简单的问题通过查看内核崩溃时打印出来的日志就能大概判断出是哪里出了问题,但是如果遇到复杂的问题,单纯通过崩溃的日志是难以定位到问题根因的,这时候我们可以把内存ddr里的数据dump出来,然后通过工具crash查看崩溃时内核里的状态和数据,并作进一步的分析,最终找到问题的rootcause。

        在实际硬件产品中,在内核崩溃重启后,一般都是通过bootloader来dump出ddr里的数据,然后再拷贝到开发机上通过crash工具分析。为了方便起见,我们这里通过qemu的arm64模拟器来跑linux内核,然后让内核触发panic,再通过qemu来dump出模拟器内存里的数据,最后再用crash工具分析崩溃内核的dump文件。

        关于使用qemu跑linux内核的详细步骤请参考搭建qemu运行arm64 Linux环境这篇文章。

生成crash工具

crash简介

        crash工具是redhat公司的提供的一个开源的内核分析工具,它是在gdb的基础上实现了解析内核的功能,比如查看当前内核中存在的哪些线程、各个线程的堆栈、内存的使用情况、触发panic时栈中的各个变量的值等等。crash的在github上的主页地址:https://crash-utility.github.io/,如下:

图片

下载crash源码

        crash发布的各个release版本的汇总地址为:https://github.com/crash-utility/crash/releases,在github上的仓库地址为https://github.com/crash-utility/crash,当前最新的release版本是crash-8.0.4,这里我们下载这个最新版本的源码压缩包如下:

图片

编译crash源码

我们先在ubuntu上安装编译crash源码需要用到的工具,命令如下:

sudo apt install zlib1g-dev
sudo apt install libncurses5-dev

解压编译crash源码,然后编译适用于arm64平台的crash工具,命令和日志如下:

max@ubuntu2204:~$ tar xf crash-8.0.4.tar.gz
max@ubuntu2204:~$ cd crash-8.0.4/
max@ubuntu2204:~/crash-8.0.4$ make target=arm64
TARGET: ARM64
 CRASH: 8.0.4
   GDB: 10.2
……
……
ar: creating crashlib.a
  CXXLD  gdb
make[3]: Nothing to be done for 'all-target'.
max@ubuntu2204:~/crash-8.0.4$

查看生成的 crash 工具,如下:

max@ubuntu2204:~/crash-8.0.4$ file crash
crash: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=84677c50d0725597490901c46f45a35c1174ec1b, for GNU/Linux 3.2.0, with debug_info, not stripped

继续编译crash工具的扩展动态库:

max@ubuntu2204:~/crash-8.0.4$ make target=arm64 extensions
gcc -Wall -g -shared -rdynamic -o dminfo.so dminfo.c -fPIC -DARM64  -DGDB_10_2
gcc -Wall -g -shared -rdynamic -o echo.so echo.c -fPIC -DARM64  -DGDB_10_2
eppic.so: failed to pull eppic code from git repo
gcc -Wall -g -I. -shared -rdynamic -o snap.so snap.c -fPIC -DARM64  -DGDB_10_2
max@ubuntu2204:~/crash-8.0.4$

生成的扩展动态库如下:???????

max@ubuntu2204:~/crash-8.0.4$ ls extensions/*so
extensions/dminfo.so  extensions/echo.so  extensions/snap.so

crash源码包中包含的扩展动态库的源码比较少,如果需要使用trace等其他扩展功能,可以从https://crash-utility.github.io/extensions.html下载对应的源码,并放到crash源码中的 extensions 目录下,最后再执行编译make target=arm64 extensions 编译扩展动态库即可。

生成Imagevmlinux文件

        我们在编译内核镜像Image时会同时生成一个对应的vmlinux文件,vmlinux是内核静态链接生成的可执行文件,包含了内核的符号表等调试信息,在用crash工具解析内核的dump文件时,需要用到该dump文件对应的内核的vmlinux,这样crash才能正确解析内核里的数据。

        内核源码的下载的详细步骤请参考 获取Linux内核源码 这篇文章,arm64交叉编译器的安装和内核源码编译的详细过程请参考 搭建qemu运行arm64 Linux环境 这篇文章,这里就不再赘述。我们把交叉编译器添加到PATH环境变量,并下载linux6.1.0的内核源码,命令如下:???????

PATH=/home/max/cross-compiler/gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin:$PATH
git clone https://mirrors.tuna.tsinghua.edu.cn/git/linux-stable.git -b linux-6.1.y
cd linux-stable/

linux6.1.0内核源码的默认配置中使能了"Reduce debugging information" 这个配置,这个配置会导致crash找不到内核的调试信息从而解析失败。我们先通过menuconfig菜单来关闭这个配置,命令如下:

make CROSS_COMPILE=aarch64-linux-gnu- ARCH=arm64 O=build menuconfig

在menuconfig菜单中依次选择如下选项:???????

-> Kernel hacking    -> Compile-time checks and compiler options        -> Reduce debugging information (DEBUG_INFO_REDUCED [=n])

关闭该配置后的界面如下:

图片

然后使能内核的 "RAM block device support" 这个配置,让内核能挂载上文件系统,这里我们调整"Default RAM disk size"的值为65536,界面如下:

图片

保存后退出menuconfig,然后开始编译内核,命令和日志如下:???????

max@ubuntu2204:~/linux-stable$ make CROSS_COMPILE=aarch64-linux-gnu- ARCH=arm64 O=build -j8
make[1]: Entering directory '/home/max/linux-stable/build'
  SYNC    include/config/auto.conf.cmd
  GEN     Makefile
……
……
  LD      vmlinux
  NM      System.map
  SORTTAB vmlinux
  OBJCOPY arch/arm64/boot/Image
  GZIP    arch/arm64/boot/Image.gz
make[1]: Leaving directory '/home/max/linux-stable/build'
max@ubuntu2204:~/linux-stable$

查看生成的内核Image和对应的vmlinux文件,如下:???????

max@ubuntu2204:~/linux-stable$ file build/arch/arm64/boot/Image
build/arch/arm64/boot/Image: Linux kernel ARM64 boot executable Image, little-endian, 4K pages
max@ubuntu2204:~/linux-stable$ 
max@ubuntu2204:~/linux-stable$ file build/vmlinux
build/vmlinux: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), statically linked, BuildID[sha1]=bcefe411be529b3d592a4e2202124e062c042b5b, with debug_info, not stripped

到此适用于arm64平台的linux内核的Image和vmlinux文件就生成好了!

通过qemu运行内核

        用qemu模拟器跑linux内核时需要挂载文件系统,所以我们还需要制作包含busybox程序的initrd文件系统镜像,制作步骤请参考 搭建qemu运行arm64 Linux环境 这篇文章,这里就不再赘述。

        我们选择用ARMv8的64位CPU cortex-a57来运行linux内核,由于后面要用crash工具来分析该内核对应的dump文件,所以这里qemu要给内核传递一个nokaslr 参数,让内核关闭KASLR(内核地址空间布局随机化),这样crash才能解析该内核对应的dump文件。由于命令参数过长,方便起见,我们把运行qemu模拟器的命令写到一个脚本qemu.sh中,脚本如下:

#!/bin/bash
set -x

QEMU=~/qemu-8.2.0
KERNEL=~/linux-stable
INITRD=~/make_initrd
$QEMU/build/aarch64-softmmu/qemu-system-aarch64 
-nographic 
-M virt 
-cpu cortex-a57 
-smp 8 
-m 2G 
-kernel $KERNEL/build/arch/arm64/boot/Image 
-append "nokaslr root=/dev/ram init=/linuxrc console=ttyAMA0 console=ttyS0" 
-initrd $INITRD/initrd.ext4

运行上面的脚本qemu.sh来通过模拟器运行刚才编译的内核Image,命令和日志如下:

max@ubuntu2204:~$ sh qemu.sh 
+ QEMU=/home/max/qemu-8.2.0
+ KERNEL=/home/max/linux-stable
+ INITRD=/home/max/make_initrd
+ /home/max/qemu-8.2.0/build/aarch64-softmmu/qemu-system-aarch64 -nographic -M virt -cpu cortex-a57 -smp 8 -m 2G -kernel /home/max/linux-stable/build/arch/arm64/boot/Image -append nokaslr root=/dev/ram init=/linuxrc console=ttyAMA0 console=ttyS0 -initrd /home/max/make_initrd/initrd.ext4
[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[    0.000000] Linux version 6.1.72 (max@ubuntu2204) (aarch64-linux-gnu-gcc (Linaro GCC 7.5-2019.12) 7.5.0, GNU ld (Linaro_Binutils-2019.12) 2.28.2.20170706) #6 SMP PREEMPT Sat Jan 20 11:32:43 CST 2024
[    0.000000] random: crng init done
[    0.000000] Machine model: linux,dummy-virt
……
……
[    2.274459] Freeing unused kernel memory: 7552K
[    2.276233] Run /linuxrc as init process
[    2.403193] EXT4-fs (ram0): re-mounted. Quota mode: none.
Welcome to ARM64 Linux

Please press Enter to activate this console. 
~ # 
~ #

qemu模拟器跑起来内核后,我们在模拟器里通过sysrq让内核触发panic,命令和日志如下:

~ # 
~ # echo c > /proc/sysrq-trigger 
[   20.107747] sysrq: Trigger a crash
[   20.108570] Kernel panic - not syncing: sysrq triggered crash
[   20.109427] CPU: 2 PID: 140 Comm: sh Not tainted 6.1.72 #6
[   20.110033] Hardware name: linux,dummy-virt (DT)
[   20.110833] Call trace:
[   20.111345]  dump_backtrace.part.6+0xf0/0x100
[   20.111930]  show_stack+0x18/0x28
[   20.112292]  dump_stack_lvl+0x68/0x84
[   20.112607]  dump_stack+0x18/0x38
[   20.112947]  panic+0x19c/0x388
[   20.113336]  sysrq_handle_crash+0x1c/0x20
[   20.114123]  __handle_sysrq+0xbc/0x1b0
[   20.114612]  write_sysrq_trigger+0x70/0xa8
[   20.115142]  proc_reg_write+0xc8/0x100
[   20.115276]  vfs_write+0x138/0x450
[   20.115868]  ksys_write+0x6c/0x100
[   20.116035]  __arm64_sys_write+0x1c/0x28
[   20.116363]  invoke_syscall+0x44/0x100
[   20.116674]  el0_svc_common.constprop.3+0x6c/0xf0
[   20.117049]  do_el0_svc+0x2c/0xc8
[   20.117332]  el0_svc+0x20/0x60
[   20.117547]  el0t_64_sync_handler+0x98/0xc0
[   20.117758]  el0t_64_sync+0x170/0x174
[   20.118457] SMP: stopping secondary CPUs
[   20.119547] Kernel Offset: disabled
[   20.119739] CPU features: 0x88000,2023c080,0000421b
[   20.120348] Memory Limit: none
[   20.120945] ---[ end Kernel panic - not syncing: sysrq triggered crash ]---

生成内核dump文件

        在qemu里的内核panic后,我们让qemu进入monitor控制台,即需要先按下ctrl+a,松开后,再按c,这样就进入monitor控制台命令行了,如下:???????

[   20.120348] Memory Limit: none
[   20.120945] ---[ end Kernel panic - not syncing: sysrq triggered crash ]---
QEMU 8.2.0 monitor - type 'help' for more information
(qemu) 
(qemu)

在monitor 命令行下执行 dump-guest-memory dump.img命令,把模拟器内存里的数据保存到dump.img 文件中,然后输入q退出qemu,这时候在当前目录就可以看到生成的 dump.img 镜像文件,命令和日志如下:???????

(qemu) dump-guest-memory dump.img
(qemu) 
(qemu) q
max@ubuntu2204:~$
max@ubuntu2204:~$ file dump.img 
dump.img: ELF 64-bit LSB core file, ARM aarch64, version 1 (SYSV), SVR4-style

到此崩溃内核的dump文件就制作完成了。

通过crash分析内核dump文件

        在通过crash 解析arm64 平台上的dump文件时,需要给crash指定一些相关的参数,我们通过crash --help命令查看需要指定哪些参数,如下:

max@ubuntu2204:~$ ./crash-8.0.4/crash --help
  ……
  ……
  -m option=value
    --machdep option=value
      Pass an option and value pair to machine-dependent code.  These
      architecture-specific option/pairs should only be required in
      very rare circumstances:
      ……
      …….
      ARM64:
        phys_offset=<physical-address>
        kimage_voffset=<kimage_voffset-value>
        max_physmem_bits=<value>
        vabits_actual=<value>

???????通过crash的help信息可知,我们需要指定参数vabits_actual,即内核配置的虚拟地址的有效bit数,这个配置我们可以内核源码中搜索CONFIG_ARM64_VA_BITS的配置获取,如下:???????

max@ubuntu2204:~$ grep -r CONFIG_ARM64_VA_BITS  linux-stable/arch/arm64/configs/defconfig 
CONFIG_ARM64_VA_BITS_48=y
max@ubuntu2204:~$

同时还需要告诉crash要解析的dump文件对应的vmlinux在哪里,所以最终用crash解析内核dump文件的命令和日志如下:

max@ubuntu2204:~$ ./crash-8.0.4/crash dump.img linux-stable/build/vmlinux --machdep vabits_actual=48
crash 8.0.4
Copyright (C) 2002-2022  Red Hat, Inc.
……
……
      KERNEL: linux-stable/build/vmlinux
    DUMPFILE: dump.img
        CPUS: 8 [OFFLINE: 7]
        DATE: Sat Jan 20 14:02:56 CST 2024
      UPTIME: 00:02:21
LOAD AVERAGE: 2.60, 1.00, 0.37
       TASKS: 97
    NODENAME: (none)
     RELEASE: 6.1.72
     VERSION: #6 SMP PREEMPT Sat Jan 20 11:32:43 CST 2024
     MACHINE: aarch64  (unknown Mhz)
      MEMORY: 2 GB
       PANIC: "Kernel panic - not syncing: sysrq triggered crash"
         PID: 140
     COMMAND: "sh"
        TASK: ffff000006380ec0  [THREAD_INFO: ffff000006380ec0]
         CPU: 2
       STATE: TASK_RUNNING (PANIC)

crash>

当打印出上面的版本信息并出现 “crash >” 的提示符的时候,就表示内核的dump文件解析成功了!此时我们就可以在crash内输入各种命令来查看内核里的数据和状态,比如通过ps命令查看当前内核中有哪些任务,通过log命令查看内核ringbuffer里的日志,如下:

crash> ps
      PID    PPID  CPU       TASK        ST  %MEM      VSZ      RSS  COMM
>       0       0   0  ffff80000a0a3f80  RU   0.0        0        0  [swapper/0]
>       0       0   1  ffff00000294e740  RU   0.0        0        0  [swapper/1]
        0       0   2  ffff000002960000  RU   0.0        0        0  [swapper/2]
>       0       0   3  ffff000002960ec0  RU   0.0        0        0  [swapper/3]
>       0       0   4  ffff000002961d80  RU   0.0        0        0  [swapper/4]
>       0       0   5  ffff000002962c40  RU   0.0        0        0  [swapper/5]
>       0       0   6  ffff000002963b00  RU   0.0        0        0  [swapper/6]
>       0       0   7  ffff0000029649c0  RU   0.0        0        0  [swapper/7]
        1       0   2  ffff0000028b8000  IN   0.0     2276     1224  linuxrc
……
……
crash> 
crash> log
[    0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[    0.000000] Linux version 6.1.72 (max@ubuntu2204) (aarch64-linux-gnu-gcc (Linaro GCC 7.5-2019.12) 7.5.0, GNU ld (Linaro_Binutils-2019.12) 2.28.2.20170706) #6 SMP PREEMPT Sat Jan 20 11:32:43 CST 2024
[    0.000000] random: crng init done
……
……
[   20.119739] CPU features: 0x88000,2023c080,0000421b
[   20.120348] Memory Limit: none
[   20.120945] ---[ end Kernel panic - not syncing: sysrq triggered crash ]---
crash>

到此我们就成功的通过crash工具解析了崩溃的内核!

        crash的使用方法可以通过执行crash –help命令查看,也可以参考crash官方的白皮书:https://crash-utility.github.io/crash_whitepaper.html。后面我们还会有专门的文章来详细介绍crash的使用方法和技巧,并且通过项目实例来讲解怎么通过crash来分析内核的稳定性问题。

请关注微信公众号 “Linux研习社” 获取更多技术内容: