Kdump 文档 - 基于 kexec 的崩溃转储解决方案

本文档包括概述、设置、安装和分析信息。

概述

Kdump 使用 kexec 在需要获取系统内核内存转储时(例如,当系统发生恐慌时)快速启动到转储捕获内核。系统内核的内存映像在重新启动后保留,并且转储捕获内核可以访问它。

您可以使用 cp、scp 或 makedumpfile 等常用命令将内存映像复制到本地磁盘上的转储文件,或通过网络复制到远程系统。

目前在 x86、x86_64、ppc64、s390x、arm 和 arm64 架构上支持 Kdump 和 kexec。

当系统内核启动时,它会为转储捕获内核保留一小部分内存。这确保了来自系统内核的持续直接内存访问 (DMA) 不会损坏转储捕获内核。kexec -p 命令将转储捕获内核加载到此保留内存中。

在 x86 机器上,无论内核加载在哪里,都需要物理内存的前 640 KB 用于启动。为了简化处理,整个低 1M 都被保留,以避免任何后续的内核或设备驱动程序将数据写入此区域。这样,低 1M 可以被 kdump 内核重用为系统 RAM,而无需额外的处理。

在 PPC64 机器上,无论内核加载在哪里,都需要物理内存的前 32KB 用于启动,并且为了支持 64K 页面大小,kexec 会备份前 64KB 的内存。

对于 s390x,当触发 kdump 时,crashkernel 区域将与区域 [0, crashkernel 区域大小] 交换,然后 kdump 内核在 [0, crashkernel 区域大小] 中运行。因此,s390x 不需要可重定位内核。

有关系统内核核心映像的所有必要信息都以 ELF 格式编码,并在崩溃前存储在保留的内存区域中。ELF 标头起始的物理地址通过 elfcorehdr= 引导参数传递给转储捕获内核。可选地,当使用 elfcorehdr=[size[KMG]@]offset[KMG] 语法时,也可以传递 ELF 标头的大小。

使用转储捕获内核,您可以通过 /proc/vmcore 访问内存映像。这将转储导出为 ELF 格式的文件,您可以使用 cp 或 scp 等文件复制命令将其写出。您还可以使用 makedumpfile 实用程序来分析和写出带有选项的过滤内容,例如,使用“-d 31”将只写出内核数据。此外,您可以使用 GNU 调试器 (GDB) 和 Crash 工具等分析工具来调试转储文件。此方法确保转储页面正确排序。

设置和安装

安装 kexec-tools

  1. 以 root 用户身份登录。

  2. 从以下 URL 下载 kexec-tools 用户空间软件包

https://linuxkernel.org.cn/pub/linux/utils/kernel/kexec/kexec-tools.tar.gz

这是指向最新版本的符号链接。

最新的 kexec-tools git 树可在以下网址获得

还有一个 gitweb 界面可用,网址为 https://linuxkernel.org.cn/git/?p=utils/kernel/kexec/kexec-tools.git

有关 kexec-tools 的更多信息,请访问 http://horms.net/projects/kexec/

  1. 使用 tar 命令解压 tarball,如下所示

    tar xvpzf kexec-tools.tar.gz
    
  2. 更改到 kexec-tools 目录,如下所示

    cd kexec-tools-VERSION
    
  3. 配置软件包,如下所示

    ./configure
    
  4. 编译软件包,如下所示

    make
    
  5. 安装软件包,如下所示

    make install
    

构建系统和转储捕获内核

使用 Kdump 有两种可能的方法。

  1. 构建一个单独的自定义转储捕获内核来捕获内核核心转储。

  2. 或者使用系统内核二进制文件本身作为转储捕获内核,并且无需构建单独的转储捕获内核。这仅适用于支持可重定位内核的架构。截至今天,i386、x86_64、ppc64、arm 和 arm64 架构支持可重定位内核。

从不需要构建第二个内核来捕获转储的角度来看,构建可重定位内核是有利的。但与此同时,您可能希望构建适合您需求的自定义转储捕获内核。

以下是用于启用 kdump 支持的系统和转储捕获内核所需的配置设置。

系统内核配置选项

  1. 在“处理器类型和功能”中启用“kexec 系统调用”或“基于文件的 kexec 系统调用”。

    CONFIG_KEXEC=y or CONFIG_KEXEC_FILE=y
    

    它们都会选择 KEXEC_CORE

    CONFIG_KEXEC_CORE=y
    
  2. 在“文件系统”->“伪文件系统”中启用“sysfs 文件系统支持”。这通常默认启用

    CONFIG_SYSFS=y
    

    请注意,如果“通用设置”中未启用“配置标准内核功能(专家用户)”,则“sysfs 文件系统支持”可能不会出现在“伪文件系统”菜单中。在这种情况下,请检查 .config 文件本身以确保 sysfs 已打开,如下所示

    grep 'CONFIG_SYSFS' .config
    
  3. 在“内核黑客”中启用“使用调试信息编译内核”。

    CONFIG_DEBUG_INFO=Y
    

    这会导致内核使用调试符号构建。转储分析工具需要带有调试符号的 vmlinux 才能读取和分析转储文件。

转储捕获内核配置选项(与架构无关)

  1. 在“处理器类型和功能”下启用“内核崩溃转储”支持

    CONFIG_CRASH_DUMP=y
    
    这将选择 VMCORE_INFO 和 CRASH_RESERVE:

    CONFIG_VMCORE_INFO=y CONFIG_CRASH_RESERVE=y

  2. 在“文件系统”->“伪文件系统”下启用“/proc/vmcore 支持”

    CONFIG_PROC_VMCORE=y
    

    (选择 CONFIG_CRASH_DUMP 时,默认情况下会设置 CONFIG_PROC_VMCORE。)

转储捕获内核配置选项(与架构相关,i386 和 x86_64)

  1. 在 i386 上,在“处理器类型和功能”下启用高内存支持

    CONFIG_HIGHMEM64G=y
    

    CONFIG_HIGHMEM4G
    
  2. 使用 CONFIG_SMP=y,通常在加载转储捕获内核时需要在内核命令行上指定 nr_cpus=1,因为一个 CPU 就足以让 kdump 内核在大多数系统上转储 vmcore。

    但是,您也可以指定 nr_cpus=X 以在 kdump 内核中启用多个处理器。

    使用 CONFIG_SMP=n,以上内容不相关。

  3. 建议默认构建可重定位内核。如果尚未启用,请在“处理器类型和功能”下启用“构建可重定位内核”支持

    CONFIG_RELOCATABLE=y
    
  4. 使用“内核加载的物理地址”(在“处理器类型和功能”下)的合适值。仅当启用“内核崩溃转储”时,才会出现此选项。合适的值取决于内核是否可重定位。

    如果您使用的是可重定位内核,请使用 CONFIG_PHYSICAL_START=0x100000。这将为物理地址 1MB 编译内核,但考虑到内核是可重定位的,它可以从任何物理地址运行,因此 kexec 引导加载程序会将其加载到为转储捕获内核保留的内存区域中。

    否则,它应该是使用启动参数 “crashkernel=Y@X” 为第二个内核预留的内存区域的起始位置。这里 X 是为转储捕获内核预留的内存区域的起始位置。通常 X 是 16MB (0x1000000)。因此,您可以设置 CONFIG_PHYSICAL_START=0x1000000

  5. 编译并安装内核及其模块。不要将此内核添加到启动加载程序配置文件中。

转储捕获内核配置选项(架构相关,ppc64)

  1. 在“内核”选项下启用“构建 kdump 崩溃内核”支持

    CONFIG_CRASH_DUMP=y
    
  2. 启用“构建可重定位内核”支持

    CONFIG_RELOCATABLE=y
    

编译并安装内核及其模块。

转储捕获内核配置选项(架构相关,arm)

  • 要使用可重定位内核,请在“启动”选项下启用“AUTO_ZRELADDR”支持

    AUTO_ZRELADDR=y
    

转储捕获内核配置选项(架构相关,arm64)

  • 请注意,即使配置了转储捕获内核的 kvm,在非 VHE 系统上也不会启用它。这是因为 CPU 不会在发生 panic 时重置为 EL2。

crashkernel 语法

  1. crashkernel=大小@偏移量

    这里,“大小” 指定为转储捕获内核预留多少内存,“偏移量” 指定此预留内存的起始位置。例如,“crashkernel=64M@16M” 告诉系统内核为转储捕获内核预留从物理地址 0x01000000 (16MB) 开始的 64 MB 内存。

    crashkernel 区域可以由系统内核在运行时自动放置。这可以通过将基地址指定为 0 或完全省略它来实现

    crashkernel=256M@0
    

    crashkernel=256M
    

    如果指定了起始地址,请注意内核的起始地址将与一个值(取决于架构)对齐,因此如果起始地址不对齐,则对齐点以下的任何空间都将被浪费。

  2. range1:size1[,range2:size2,...][@offset]

    虽然 “crashkernel=size[@offset]” 语法足以满足大多数配置,但有时让预留内存依赖于系统 RAM 的值会很方便 -- 这主要是为分发版预先设置内核命令行,以避免在从机器中移除一些内存后出现无法启动的系统。

    语法是

    crashkernel=<range1>:<size1>[,<range2>:<size2>,...][@offset]
    range=start-[end]
    

    例如

    crashkernel=512M-2G:64M,2G-:128M
    

    这将意味着

    1. 如果 RAM 小于 512M,则不预留任何内容(这是 “rescue” 情况)

    2. 如果 RAM 大小在 512M 和 2G 之间(不包括 2G),则预留 64M

    3. 如果 RAM 大小大于 2G,则预留 128M

  3. crashkernel=size,high 和 crashkernel=size,low

    如果首选 4G 以上的内存,可以使用 crashkernel=size,high 来实现。使用它,允许从顶部分配物理内存,因此如果系统安装了超过 4G 的 RAM,则可能高于 4G。否则,如果可用,内存区域将在 4G 以下分配。

    当传递 crashkernel=X,high 时,内核可以在 4G 以上分配物理内存区域,在这种情况下需要 4G 以下的低内存。有三种方法可以获取低内存

    1. 如果没有指定 crashkernel=Y,low,内核将自动在 4G 以下分配至少 256M 的内存。

    2. 让用户指定低内存大小。

    3. 指定值 0 将禁用低内存分配

      crashkernel=0,low
      

启动到系统内核

  1. 根据需要更新启动加载程序(例如 grub、yaboot 或 lilo)配置文件。

  2. 使用启动参数 “crashkernel=Y@X” 启动系统内核。

    在 x86 和 x86_64 上,使用 “crashkernel=Y[@X]”。大多数情况下,起始地址 “X” 不是必需的,内核将搜索合适的区域。除非需要显式的起始地址。

    在 ppc64 上,使用 “crashkernel=128M@32M”。

    在 s390x 上,通常使用 “crashkernel=xxM”。xx 的值取决于 kdump 系统的内存消耗。一般来说,这不取决于生产系统的内存大小。

    在 arm 上,不再需要使用 “crashkernel=Y@X”;如果未给定 X,内核将自动在 RAM 的前 512MB 内找到崩溃内核映像。

    在 arm64 上,使用 “crashkernel=Y[@X]”。请注意,内核的起始地址 X(如果显式指定)必须与 2MiB (0x200000) 对齐。

加载转储捕获内核

启动到系统内核后,需要加载转储捕获内核。

根据架构和映像类型(可重定位或不可重定位),可以选择加载转储捕获内核的未压缩 vmlinux 或压缩 bzImage/vmlinuz。以下是摘要。

对于 i386 和 x86_64

  • 如果内核是可重定位的,则使用 bzImage/vmlinuz。

  • 如果内核不可重定位,则使用 vmlinux。

对于 ppc64

  • 使用 vmlinux

对于 s390x

  • 使用 image 或 bzImage

对于 arm

  • 使用 zImage

对于 arm64

  • 使用 vmlinux 或 Image

如果您使用的是未压缩的 vmlinux 映像,则使用以下命令加载转储捕获内核

kexec -p <dump-capture-kernel-vmlinux-image> \
--initrd=<initrd-for-dump-capture-kernel> --args-linux \
--append="root=<root-dev> <arch-specific-options>"

如果您使用的是压缩的 bzImage/vmlinuz,则使用以下命令加载转储捕获内核

kexec -p <dump-capture-kernel-bzImage> \
--initrd=<initrd-for-dump-capture-kernel> \
--append="root=<root-dev> <arch-specific-options>"

如果您使用的是压缩的 zImage,则使用以下命令加载转储捕获内核

kexec --type zImage -p <dump-capture-kernel-bzImage> \
--initrd=<initrd-for-dump-capture-kernel> \
--dtb=<dtb-for-dump-capture-kernel> \
--append="root=<root-dev> <arch-specific-options>"

如果您使用的是未压缩的 Image,则使用以下命令加载转储捕获内核

kexec -p <dump-capture-kernel-Image> \
--initrd=<initrd-for-dump-capture-kernel> \
--append="root=<root-dev> <arch-specific-options>"

以下是加载转储捕获内核时要使用的特定于架构的命令行选项。

对于 i386 和 x86_64

“1 irqpoll nr_cpus=1 reset_devices”

对于 ppc64

“1 maxcpus=1 noirqdistrib reset_devices”

对于 s390x

“1 nr_cpus=1 cgroup_disable=memory”

对于 arm

“1 maxcpus=1 reset_devices”

对于 arm64

“1 nr_cpus=1 reset_devices”

有关加载转储捕获内核的注意事项

  • 默认情况下,ELF 标头以 ELF64 格式存储,以支持内存超过 4GB 的系统。在 i386 上,kexec 会自动检查物理 RAM 大小是否超过 4 GB 限制,如果未超过,则使用 ELF32。因此,在非 PAE 系统上,始终使用 ELF32。

    可以使用 --elf32-core-headers 选项来强制生成 ELF32 标头。这是必要的,因为 GDB 目前无法在 32 位系统上打开具有 ELF64 标头的 vmcore 文件。

  • “irqpoll” 启动参数减少了由于转储捕获内核中的共享中断而导致的驱动程序初始化失败。

  • 您必须以与 mount 命令输出中的根设备名称相对应的格式指定 <root-dev>。

  • 启动参数 “1” 将转储捕获内核引导到单用户模式,而无需网络。如果要使用网络,请使用 “3”。

  • 我们通常不必启动 SMP 内核来捕获转储。因此,通常最好构建 UP 转储捕获内核或在加载转储捕获内核时指定 maxcpus=1 选项。请注意,虽然 maxcpus 始终有效,但如果当前 ARCH(例如 x86)支持,您最好将其替换为 nr_cpus 以节省内存。

  • 如果您打算将其与多线程程序一起使用,例如 makedumpfile 的并行转储功能,则应在转储捕获内核中启用多 CPU 支持。否则,多线程程序可能会出现严重的性能下降。要启用多 CPU 支持,您应该启动一个 SMP 转储捕获内核,并在加载时指定 maxcpus/nr_cpus 选项。

  • 对于 s390x,有两种 kdump 模式:如果使用 elfcorehdr= 内核参数指定了 ELF 标头,则 kdump 内核会像在所有其他架构上一样使用它。如果未指定 elfcorehdr= 内核参数,则 s390x kdump 内核会动态创建标头。第二种模式的优点是对于 CPU 和内存热插拔,不必使用 kexec_load() 重新加载 kdump。

  • 对于具有许多连接设备的 s390x 系统,应该为 kdump 内核使用 “cio_ignore” 内核参数,以防止为与 kdump 无关的设备分配内核内存。这同样适用于使用 SCSI/FCP 设备的系统。在这种情况下,在使 FCP 设备联机之前,应将 “allow_lun_scan” zfcp 模块参数设置为零。

内核 Panic

如前所述成功加载转储捕获内核后,如果触发系统崩溃,系统将重新启动到转储捕获内核。触发点位于 panic()、die()、die_nmi() 和 sysrq 处理程序 (ALT-SysRq-c) 中。

以下情况将执行崩溃触发点

如果检测到硬锁定并且配置了 “NMI 监视程序”,系统将启动到转储捕获内核 ( die_nmi() )。

如果调用 die(),并且恰好是 pid 为 0 或 1 的线程,或者在中断上下文中调用 die(),或者调用 die() 并且设置了 panic_on_oops,系统将启动到转储捕获内核。

在 powerpc 系统上,当生成软复位时,所有 cpu 都会调用 die(),系统将启动到转储捕获内核。

出于测试目的,您可以使用 “ALT-SysRq-c”、“echo c > /proc/sysrq-trigger” 或编写模块来强制 panic 来触发崩溃。

写出转储文件

在启动转储捕获内核后,使用以下命令写出转储文件

cp /proc/vmcore <dump-file>

或者使用 scp 在网络上的主机之间写出转储文件,例如

scp /proc/vmcore remote_username@remote_ip:<dump-file>

您还可以使用 makedumpfile 实用程序写出具有指定选项的转储文件以过滤掉不需要的内容,例如

makedumpfile -l --message-level 1 -d 31 /proc/vmcore <dump-file>

分析

在分析转储映像之前,您应该重新启动到一个稳定的内核。

您可以使用 GDB 对 /proc/vmcore 中复制出来的转储文件进行有限的分析。使用使用 -g 构建的调试 vmlinux 并运行以下命令

gdb vmlinux <dump-file>

处理器 0 上任务的堆栈跟踪、寄存器显示和内存显示工作正常。

注意:GDB 无法分析 x86 的 ELF64 格式生成的 core 文件。在内存最大为 4GB 的系统上,您可以使用转储内核上的 --elf32-core-headers 内核选项生成 ELF32 格式的标头。

您还可以使用 Crash 实用程序来分析 Kdump 格式的转储文件。Crash 可在以下 URL 中找到

Crash 文档可以在以下网址找到

https://crash-utility.github.io/

在 WARN() 上触发 Kdump

内核参数 panic_on_warn 会在所有 WARN() 路径中调用 panic()。这将导致在 panic() 调用时发生 kdump。如果用户想要在运行时指定此项,可以将 /proc/sys/kernel/panic_on_warn 设置为 1 以实现相同的行为。

在 add_taint() 上触发 Kdump

内核参数 panic_on_taint 允许在 add_taint() 函数内部有条件地调用 panic(),前提是此位掩码中设置的值与 add_taint() 设置的位标志相匹配。 这将导致在 add_taint() -> panic() 调用时发生 kdump。

联系方式

GDB 宏

#
# This file contains a few gdb macros (user defined commands) to extract
# useful information from kernel crashdump (kdump) like stack traces of
# all the processes or a particular process and trapinfo.
#
# These macros can be used by copying this file in .gdbinit (put in home
# directory or current directory) or by invoking gdb command with
# --command=<command-file-name> option
#
# Credits:
# Alexander Nyberg <[email protected]>
# V Srivatsa <[email protected]>
# Maneesh Soni <[email protected]>
#

define bttnobp
        set $tasks_off=((size_t)&((struct task_struct *)0)->tasks)
        set $pid_off=((size_t)&((struct task_struct *)0)->thread_group.next)
        set $init_t=&init_task
        set $next_t=(((char *)($init_t->tasks).next) - $tasks_off)
        set var $stacksize = sizeof(union thread_union)
        while ($next_t != $init_t)
                set $next_t=(struct task_struct *)$next_t
                printf "\npid %d; comm %s:\n", $next_t.pid, $next_t.comm
                printf "===================\n"
                set var $stackp = $next_t.thread.sp
                set var $stack_top = ($stackp & ~($stacksize - 1)) + $stacksize

                while ($stackp < $stack_top)
                        if (*($stackp) > _stext && *($stackp) < _sinittext)
                                info symbol *($stackp)
                        end
                        set $stackp += 4
                end
                set $next_th=(((char *)$next_t->thread_group.next) - $pid_off)
                while ($next_th != $next_t)
                        set $next_th=(struct task_struct *)$next_th
                        printf "\npid %d; comm %s:\n", $next_t.pid, $next_t.comm
                        printf "===================\n"
                        set var $stackp = $next_t.thread.sp
                        set var $stack_top = ($stackp & ~($stacksize - 1)) + stacksize

                        while ($stackp < $stack_top)
                                if (*($stackp) > _stext && *($stackp) < _sinittext)
                                        info symbol *($stackp)
                                end
                                set $stackp += 4
                        end
                        set $next_th=(((char *)$next_th->thread_group.next) - $pid_off)
                end
                set $next_t=(char *)($next_t->tasks.next) - $tasks_off
        end
end
document bttnobp
        dump all thread stack traces on a kernel compiled with !CONFIG_FRAME_POINTER
end

define btthreadstack
        set var $pid_task = $arg0

        printf "\npid %d; comm %s:\n", $pid_task.pid, $pid_task.comm
        printf "task struct: "
        print $pid_task
        printf "===================\n"
        set var $stackp = $pid_task.thread.sp
        set var $stacksize = sizeof(union thread_union)
        set var $stack_top = ($stackp & ~($stacksize - 1)) + $stacksize
        set var $stack_bot = ($stackp & ~($stacksize - 1))

        set $stackp = *((unsigned long *) $stackp)
        while (($stackp < $stack_top) && ($stackp > $stack_bot))
                set var $addr = *(((unsigned long *) $stackp) + 1)
                info symbol $addr
                set $stackp = *((unsigned long *) $stackp)
        end
end
document btthreadstack
         dump a thread stack using the given task structure pointer
end


define btt
        set $tasks_off=((size_t)&((struct task_struct *)0)->tasks)
        set $pid_off=((size_t)&((struct task_struct *)0)->thread_group.next)
        set $init_t=&init_task
        set $next_t=(((char *)($init_t->tasks).next) - $tasks_off)
        while ($next_t != $init_t)
                set $next_t=(struct task_struct *)$next_t
                btthreadstack $next_t

                set $next_th=(((char *)$next_t->thread_group.next) - $pid_off)
                while ($next_th != $next_t)
                        set $next_th=(struct task_struct *)$next_th
                        btthreadstack $next_th
                        set $next_th=(((char *)$next_th->thread_group.next) - $pid_off)
                end
                set $next_t=(char *)($next_t->tasks.next) - $tasks_off
        end
end
document btt
        dump all thread stack traces on a kernel compiled with CONFIG_FRAME_POINTER
end

define btpid
        set var $pid = $arg0
        set $tasks_off=((size_t)&((struct task_struct *)0)->tasks)
        set $pid_off=((size_t)&((struct task_struct *)0)->thread_group.next)
        set $init_t=&init_task
        set $next_t=(((char *)($init_t->tasks).next) - $tasks_off)
        set var $pid_task = 0

        while ($next_t != $init_t)
                set $next_t=(struct task_struct *)$next_t

                if ($next_t.pid == $pid)
                        set $pid_task = $next_t
                end

                set $next_th=(((char *)$next_t->thread_group.next) - $pid_off)
                while ($next_th != $next_t)
                        set $next_th=(struct task_struct *)$next_th
                        if ($next_th.pid == $pid)
                                set $pid_task = $next_th
                        end
                        set $next_th=(((char *)$next_th->thread_group.next) - $pid_off)
                end
                set $next_t=(char *)($next_t->tasks.next) - $tasks_off
        end

        btthreadstack $pid_task
end
document btpid
        backtrace of pid
end


define trapinfo
        set var $pid = $arg0
        set $tasks_off=((size_t)&((struct task_struct *)0)->tasks)
        set $pid_off=((size_t)&((struct task_struct *)0)->thread_group.next)
        set $init_t=&init_task
        set $next_t=(((char *)($init_t->tasks).next) - $tasks_off)
        set var $pid_task = 0

        while ($next_t != $init_t)
                set $next_t=(struct task_struct *)$next_t

                if ($next_t.pid == $pid)
                        set $pid_task = $next_t
                end

                set $next_th=(((char *)$next_t->thread_group.next) - $pid_off)
                while ($next_th != $next_t)
                        set $next_th=(struct task_struct *)$next_th
                        if ($next_th.pid == $pid)
                                set $pid_task = $next_th
                        end
                        set $next_th=(((char *)$next_th->thread_group.next) - $pid_off)
                end
                set $next_t=(char *)($next_t->tasks.next) - $tasks_off
        end

        printf "Trapno %ld, cr2 0x%lx, error_code %ld\n", $pid_task.thread.trap_no, \
                                $pid_task.thread.cr2, $pid_task.thread.error_code

end
document trapinfo
        Run info threads and lookup pid of thread #1
        'trapinfo <pid>' will tell you by which trap & possibly
        address the kernel panicked.
end

define dump_record
        set var $desc = $arg0
        set var $info = $arg1
        if ($argc > 2)
                set var $prev_flags = $arg2
        else
                set var $prev_flags = 0
        end

        set var $prefix = 1
        set var $newline = 1

        set var $begin = $desc->text_blk_lpos.begin % (1U << prb->text_data_ring.size_bits)
        set var $next = $desc->text_blk_lpos.next % (1U << prb->text_data_ring.size_bits)

        # handle data-less record
        if ($begin & 1)
                set var $text_len = 0
                set var $log = ""
        else
                # handle wrapping data block
                if ($begin > $next)
                        set var $begin = 0
                end

                # skip over descriptor id
                set var $begin = $begin + sizeof(long)

                # handle truncated message
                if ($next - $begin < $info->text_len)
                        set var $text_len = $next - $begin
                else
                        set var $text_len = $info->text_len
                end

                set var $log = &prb->text_data_ring.data[$begin]
        end

        # prev & LOG_CONT && !(info->flags & LOG_PREIX)
        if (($prev_flags & 8) && !($info->flags & 4))
                set var $prefix = 0
        end

        # info->flags & LOG_CONT
        if ($info->flags & 8)
                # (prev & LOG_CONT && !(prev & LOG_NEWLINE))
                if (($prev_flags & 8) && !($prev_flags & 2))
                        set var $prefix = 0
                end
                # (!(info->flags & LOG_NEWLINE))
                if (!($info->flags & 2))
                        set var $newline = 0
                end
        end

        if ($prefix)
                printf "[%5lu.%06lu] ", $info->ts_nsec / 1000000000, $info->ts_nsec % 1000000000
        end
        if ($text_len)
                eval "printf \"%%%d.%ds\", $log", $text_len, $text_len
        end
        if ($newline)
                printf "\n"
        end

        # handle dictionary data

        set var $dict = &$info->dev_info.subsystem[0]
        set var $dict_len = sizeof($info->dev_info.subsystem)
        if ($dict[0] != '\0')
                printf " SUBSYSTEM="
                set var $idx = 0
                while ($idx < $dict_len)
                        set var $c = $dict[$idx]
                        if ($c == '\0')
                                loop_break
                        else
                                if ($c < ' ' || $c >= 127 || $c == '\\')
                                        printf "\\x%02x", $c
                                else
                                        printf "%c", $c
                                end
                        end
                        set var $idx = $idx + 1
                end
                printf "\n"
        end

        set var $dict = &$info->dev_info.device[0]
        set var $dict_len = sizeof($info->dev_info.device)
        if ($dict[0] != '\0')
                printf " DEVICE="
                set var $idx = 0
                while ($idx < $dict_len)
                        set var $c = $dict[$idx]
                        if ($c == '\0')
                                loop_break
                        else
                                if ($c < ' ' || $c >= 127 || $c == '\\')
                                        printf "\\x%02x", $c
                                else
                                        printf "%c", $c
                                end
                        end
                        set var $idx = $idx + 1
                end
                printf "\n"
        end
end
document dump_record
        Dump a single record. The first parameter is the descriptor,
        the second parameter is the info, the third parameter is
        optional and specifies the previous record's flags, used for
        properly formatting continued lines.
end

define dmesg
        # definitions from kernel/printk/printk_ringbuffer.h
        set var $desc_committed = 1
        set var $desc_finalized = 2
        set var $desc_sv_bits = sizeof(long) * 8
        set var $desc_flags_shift = $desc_sv_bits - 2
        set var $desc_flags_mask = 3 << $desc_flags_shift
        set var $id_mask = ~$desc_flags_mask

        set var $desc_count = 1U << prb->desc_ring.count_bits
        set var $prev_flags = 0

        set var $id = prb->desc_ring.tail_id.counter
        set var $end_id = prb->desc_ring.head_id.counter

        while (1)
                set var $desc = &prb->desc_ring.descs[$id % $desc_count]
                set var $info = &prb->desc_ring.infos[$id % $desc_count]

                # skip non-committed record
                set var $state = 3 & ($desc->state_var.counter >> $desc_flags_shift)
                if ($state == $desc_committed || $state == $desc_finalized)
                        dump_record $desc $info $prev_flags
                        set var $prev_flags = $info->flags
                end

                if ($id == $end_id)
                        loop_break
                end
                set var $id = ($id + 1) & $id_mask
        end
end
document dmesg
        print the kernel ring buffer
end