内核调试工具 — kdump & crash

2019-07-13 00:07发布

kdump简介

  kdump是系统崩溃的时候,用来转储运行内存的一个工具。 系统一旦崩溃,内核就没法正常工作了,这个时候将由kdump提供一个用于捕获当前运行信息的内核, 该内核会将此时内存中的所有运行状态和数据信息收集到一个dump core文件中以便之后分析崩溃原因。 一旦内存信息收集完成,可以让系统将自动重启。   kdump是RHEL5之后才支持的,2006被主线接收为内核的一部分。它的原理简单来说是在内存中保留一块 区域,这块区域用来存放capture kernel,当production kernel发生crash的时候,通过kexec把保留区域的 capure kernel给运行起来,再由捕获内核负责把产品内核的完整信息 - 包括CPU寄存器、堆栈数据等转储 到指定位置的文件中。  

kdump原理

  kexec是kdump机制的关键,包含两部分: 内核空间的系统调用kexec_load。负责在生产内核启动时将捕获内核加载到指定地址。 用户空间的工具kexec-tools。将捕获内核的地址传递给生产内核,从而在系统崩溃的时候找到捕获内核的地址并运行。   kdump是一种基于kexec的内核崩溃转储机制。当系统崩溃时,kdump使用kexec启动到第二个内核。第二个内核通常 叫做捕获内核,以很小内存启动以捕获转储镜像。第一个内核保留了内存的一部分给第二个内核启动使用。 由于kdump利用kexec启动捕获内核,绕过了BIOS,所以第一个内核的内存得以保留。这是内存崩溃转储的本质。 捕获内核启动后,会像一般内核一样,去运行为它创建的ramdisk上的init程序。而各种转储机制都可以事先在init中实现。 为了在生产内核崩溃时能顺利启动捕获内核,捕获内核以及它的ramdisk是事先放到生产内核的内存中的。 生产内核的内存是通过/proc/vmcore这个文件交给捕获内核的。为了生成它,用户工具在生产内核中分析出内存的使用和 分布等情况,然后把这些信息综合起来生成一个ELF头文件保存起来。捕获内核被引导时会被同时传递这个ELF文件头的 地址,通过分析它,捕获内核就可以生成出/proc/vmcore。有了/proc/vmcore这个文件,捕获内核的ramdisk中的脚本就 可以通过通常的文件读写和网络来实现各种策略了。  

kdump配置

  RHEL5开始,kexec-tools是默认安装的。 如果需要调试kdump生成的vmcore文件,需要手动安装kernel-debuginfo包。   (1) 预留内存 可以修改内核引导参数,为启动捕获内核预留指定内存。 在/etc/grub.conf (一般为/boot/grub/grub.conf的软链接)中: crashkernel=Y@X,Y是为kdump捕获内核保留的内存,X是保留部分内存的起始位置。 默认为crashkernel=auto,可自行设定如crashkernel=256M。   (2) 配置文件 配置文件为/etc/kdump.conf,以下是几个常用配置:   # path /var/crash 默认的vmcore存放目录为/var/crash/%HOST-%DATE/,包括两个文件:vmcore和vmcore-dmesg.txt   # ssh will copy /proc/vmcore to :/%HOST-%DATE/ via SSH make sure user has necessary write permissions on server. 自动拷贝到远程机器上。   # default Actions to perform in case dumping to intended target fails. 转储失败时执行。   (3) 启动服务 # chkconfig kdump on // 开机启动 # service kdump status // start、stop、restart等   (4) 功能验证 Magic System request key is a magical key combo you can hit which the kernel will respond to regardless of whatever else it is doing, unless it is completely locked up. 使用sysrq需要编译选项CONFIG_MAGIC_SYSRQ的支持。详细信息可看documentation/sysrq.txt。   故意让系统崩溃,来测试kdump是否正常工作。 # echo c > /proc/sysrq-trigger Will perform a system crash by a NULL pointer dereference. A crash dump will be taken if configured.   Magic SysRq还有一些很有趣的值,有的具有很大的破环性,输出在/var/log/messages: f:call oom_kill to kill a memory hog process. 执行oom killer。 l:shows a stack backtrace for all active CPUs. 打印出所有CPU的stack backtrace。 m:dump current memory info. 打印出内存使用信息。 p:dump the current registers and flags. 打印出所在CPU的寄存器信息。   (5) 捕获内核 捕获内核是一个未压缩的ELF映像文件,查看捕获内核是否加载到内存中: # cat /sys/kernel/kexec_crash_loaded 缩小捕获内核占用的内存: # echo N > /sys/kernel/kexec_crash_size  

crash简介

  当系统崩溃时,通过kdump可以获得当时的内存转储文件vmcore,但是该如何分析vmcore呢? crash是一个用于分析内核转储文件的工具,一般和kdump搭配使用。 使用crash时,要求调试内核vmlinux在编译时带有-g选项,即带有调试信息。 如果没有指定vmcore,则默认使用实时系统的内存来分析。   值得一提的是,crash也可以用来分析实时的系统内存,是一个很强大的调试工具。 crash使用gdb作为内部引擎,语法类似于gdb,命令的使用说明可以用 help来查看。 使用crash需要安装crash工具包和内核调试信息包: crash kernel-debuginfo-common kernel-debuginfo  

crash使用

  Analyze Linux crash dump data or a live system. crash [OPTION] NAMELIST MEMORY-IMAGE      (dumpfile form) crash [OPTION] [NAMELIST]                                   (live system form)   使用crash来调试vmcore,至少需要两个参数: NAMELIST:未压缩的内核映像文件vmlinux,默认位于/usr/lib/debug/lib/modules/$(uname -r)/vmlinux,由 内核调试信息包提供。 MEMORY-IMAGE:内存转储文件vmcore,默认位于/var/crash/%HOST-%DATE/vmcore,由kdump生成。 例如:# crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux   /var/crash/%HOST-%DATE/vmcore   (1) 错误类型 首先可以在vmcore-dmesg.txt中先查看错误类型,如: 1. divide error: 0000 [#1] SMP,除数为0造成内核崩溃,由1号CPU触发。 2. BUG: unable to handle kernel NULL pointer dereference at 000000000000012c,引用空指针。 这样一来就能知道引发内核崩溃的错误类型。   (2) 错误地点 RIP为造成内核崩溃的指令,Call Trace为函数调用栈,通过RIP和Call Trace可以确定函数的调用路径,以及在 哪个函数中的哪条指令引发了错误。   例如RIP为:[] ? tcp_enter_loss+0x1d3/0x23b []是指令在内存中的虚拟地址。 tcp_enter_loss是函数名(symbol)。 0x1d3是这条指令相对于tcp_enter_loss入口的偏移,0x23b是函数编译成机器码后的长度。 这样一来就能确定在哪个函数中引发了错误,以及错误的大概位置。   Call Trace为函数的调用栈,是从下往上看的。可以用来分析函数的调用关系。   (3) crash基本输出 # crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux   /var/crash/%HOST-%DATE/vmcore KERNEL: /usr/lib/debug/lib/modules/2.6.32-358.el6.x86_64/vmlinux DUMPFILE: vmcore [PARTIAL DUMP] CPUS: 12 DATE: Fri Sep 19 16:47:01 2014 UPTIME: 7 days, 06:37:46 LOAD AVERAGE: 0.19, 0.05, 0.01 TASKS: 282 NODENAME: localhost.localdomain RELEASE: 2.6.32-358.el6.x86_64 VERSION: #1 SMP Tue Oct 29 10:18:21 CST 2013 MACHINE: x86_64 (1999 Mhz) MEMORY: 48 GB PANIC: "Oops: 0002 [#1] SMP " (check log for details) PID: 0 COMMAND: "swapper" TASK: ffffffff81a8d020 (1 of 12) [THREAD_INFO: ffffffff81a00000] CPU: 0 STATE: TASK_RUNNING (PANIC) 这些基本输出信息简单明了,可由sys命令触发。   (4) crash常用命令 bt:打印函数调用栈,displays a task's kernel-stack backtrace,可以指定进程号bt 。 log:打印系统消息缓冲区,displays the kernel log_buf contents,如log | tail -n 30。 ps:显示进程的状态,>表示活跃的进程,如ps | grep RU。 sys:显示系统概况。 kmem -i:显示内存使用信息。 dis :对给定地址进行反汇编。   exception RIP即为造成错误的指令。 关于log命令: 内核首先把消息打印到内核态的ring buffer,用户态的klogd负责读取并转发给syslogd,让它记录到磁盘。 在内核崩溃时,可能无法把消息记录到磁盘,但是ring buffer中一般会有记录。所以log命令有时候能查看 到系统日志中所缺失的信息。   (5) 结构体和变量 查看结构体中所有成员的值,例如: # ps | grep RU > 0 0 0 ffffffff81a8d020 RU 0.0 0 0 [swapper] # struct task_struct ffffffff81a8d020 struct task_struct { state = 0, stack = 0xffffffff81a00000, usage = { counter = 2 }, flags = 2097408, 显示整个结构体的定义: # struct task_struct struct task_struct { volatile long int state; void *stack; atomic_t usage; unsigned int flags; 显示整个结构体的定义,以及每个成员的偏移: # struct -o task_struct struct task_struct { [0] volatile long int state; [8] void *stack; [16] atomic_t usage; [20] unsigned int flags; ... 显示结构体中的成员定义,以及它的偏移: # struct task_struct.pid struct task_struct { [1192] pid_t pid; } 显示结构体中成员的值: # struct task_struct.pid ffffffff81a8d020 pid = 0 查看全局变量的值: # p sysctl_tcp_rmem sysctl_tcp_rmem = $4 = {40960, 873800, 41943040}   查看percpu全局变量(加前缀per_cpu_): # p per_cpu__irq_stat PER-CPU DATA TYPE: irq_cpustat_t per_cpu__irq_stat; // 变量类型的声明 PER-CPU ADDRESSES: [0]: ffff880028216540 // 0号CPU对应变量的地址 [1]: ffff880645416540 ... 查看0号CPU对应变量的值: # struct irq_cpustat_t ffff880028216540 struct irq_cpustat_t { __softirq_pending = 0, __nmi_count = 4780195, irq0_irqs = 148, ...   (6) 反汇编和源码行 反汇编: # dis ffffffffa021ba91 // 反汇编一条指令 # dis -l probe_2093+497 10 // 反汇编从某个地址开始的10条指令
对于内核中的符号: # sym tcp_v4_do_rcv // 通过symbol,显示虚拟地址和源码位置 # sym ffffffff8149f930 // 通过虚拟地址,显示symbol和源码位置   对于模块中的符号: 需要先加载相应的模块进来,才能显示符号对应的源码: # mod // 查看模块 # mod -s module /path/to/module.ko // 加载模块 # sym symbol // 显示符号对应的模块源码,也可以用virtual address   (7) 修改内存 提供动态的修改运行中内核的功能,以供调试,但是RHEL和CentOS上不允许。 wr:modifies the contents of memory. wr [-u | -k | -p] [-8 | -16 | -32 | -64] [address | symbol] value   使用例子: # p sysctl_tcp_timestamps sysctl_tcp_timestamps = $3 = 1 # wr sysctl_tcp_timestamps 0 wr: cannot write to /dev/crash!
我勒个擦,/dev/crash的文件属性是rw,但是crash_fops中并没有提供写函数,所以还是只读的。 这个功能很有用,但被RHEL和CentOS禁止了,所以如需动态修改运行内核还是用systemtap吧。  

Reference

  [1]. http://www.ibm.com/developerworks/cn/linux/l-cn-kdump1/ [2]. http://www.ibm.com/developerworks/cn/linux/l-cn-kdump2/ [3]. http://www.ibm.com/developerworks/cn/linux/l-cn-kdump3/ [4]. http://www.ibm.com/developerworks/cn/linux/l-cn-kdump4/ [5]. http://www.ibm.com/developerworks/cn/linux/l-cn-dumpanalyse/ [6]. http://people.redhat.com/anderson/crash_whitepaper/