Linux内核工程导论——内核调试

时间:2022-08-28 21:49:24

         内核也是一个程序,一般的,调试程序常用的方法有3种:打印信息、断点执行和插入探测点。

打印信息

printk

         最常用的是printk,可以修改内核代码,在任何想要打印的地方打印信息。

健壮性是printk最容易被接受的一个特质,几乎在任何地方,任何时候内核都可以调用它(中断上下文、进程上下文、持有锁时、多处理器处理时等)。

在系统启动过程中,终端初始化之前,在某些地方是不能调用的。如果真的需要调试系统启动过程最开始的地方,有以下方法可以使用:

l  使用串口调试,将调试信息输出到其他终端设备。

l  使用early_printk(),该函数在系统启动初期就有打印能力。但它只支持部分硬件体系。

 

printk和printf一个主要的区别就是前者可以指定一个LOG等级。内核根据这个等级来判断是否在终端上打印消息。内核把比指定等级高的所有消息显示在终端。可以使用下面的方式指定一个LOG级别:

printk(KERN_CRIT  “Hello, world!\n”);

注意,第一个参数并不一个真正的参数,因为其中没有用于分隔级别(KERN_CRIT)和格式字符的逗号(,)。KERN_CRIT本身只是一个普通的字符串(事实上,它表示的是字符串 "<2>";表 1 列出了完整的日志级别清单)。作为预处理程序的一部分,C 会自动地使用一个名为字符串串联 的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。

内核使用这个指定LOG级别与当前终端LOG等级console_loglevel来决定是不是向终端打印。

注意,如果调用者未将日志级别提供给printk,那么系统就会使用默认值 KERN_WARNING "<4>"(表示只有KERN_WARNING 级别以上的日志消息会被记录)。由于默认值存在变化,所以在使用时最好指定LOG级别。有LOG级别的一个好处就是我们可以选择性的输出LOG。比如平时我们只需要打印KERN_WARNING级别以上的关键性LOG,但是调试的时候,我们可以选择打印KERN_DEBUG等以上的详细LOG。而这些都不需要我们修改代码,只需要通过命令修改默认日志输出级别:

mtj@ubuntu :~$ cat /proc/sys/kernel/printk

4 4 1 7

mtj@ubuntu :~$ cat/proc/sys/kernel/printk_delay

0

mtj@ubuntu :~$ cat/proc/sys/kernel/printk_ratelimit

5

mtj@ubuntu :~$ cat/proc/sys/kernel/printk_ratelimit_burst

10

第一项定义了 printk API 当前使用的日志级别。这些日志级别表示了控制台的日志级别、默认消息日志级别、最小控制台日志级别和默认控制台日志级别。printk_delay 值表示的是 printk 消息之间的延迟毫秒数(用于提高某些场景的可读性)。注意,这里它的值为 0,而它是不可以通过 /proc 设置的。printk_ratelimit 定义了消息之间允许的最小时间间隔(当前定义为每 5 秒内的某个内核消息数)。消息数量是由 printk_ratelimit_burst 定义的(当前定义为 10)。如果您拥有一个非正式内核而又使用有带宽限制的控制台设备(如通过串口),那么这非常有用。注意,在内核中,速度限制是由调用者控制的,而不是在printk 中实现的。如果一个 printk 用户要求进行速度限制,那么该用户就需要调用printk_ratelimit 函数。

·内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中。

  关于LOG_BUF_LEN定义:

CONFIG_LOG_BUF_SHIFT=18

  记录缓冲区操作:

  ① 消息被读出到用户空间时,此消息就会从环形队列中删除。

  ② 当消息缓冲区满时,如果再有printk()调用时,新消息将覆盖队列中的老消息。

  ③ 在读写环形队列时,同步问题很容易得到解决。

  ※ 这个纪录缓冲区之所以称为环形,是因为它的读写都是按照环形队列的方式进行操作的。

oops与ksymoops、kallsyms

oops是程序运行崩溃,应用程序或内核线程的崩溃都会产生oops消息,通常发生oops时,系统不会发生死机,而在终端或日志中打印oops信息。当使用NULL指针或不正确的指针值时,通常会引发一个 oops 消息,这是因为当引用一个非法指针时,页面映射机制无法将虚拟地址映像到物理地址,处理器就会向操作系统发出一个"页面失效"的信号。内核无法"换页"到并不存在的地址上,系统就会产生一个"oops"。

oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其一些难理解的信息。这些消息由失效处理函数(arch *(int *)0 = 0; return 0; }

在 Linux 中,调试系统崩溃的传统方法是分析在发生崩溃时发送到系统控制台的 Oops 消息。一旦您掌握了细节,就可以将消息发送到 ksymoops 实用程序,它将试图将代码转换为指令并将堆栈值映射到内核符号。

※ 如:回溯线索中的地址,会通过ksymoops转化成名称可见的函数名。

还可以直接在内核编译时添加kallsyms,就不需要使用ksymoops工具了。

klogd

klogd 提供了许多信息来帮助分析。为了使 klogd 正确地工作,必须在 /boot 中提供符号表文件 System.map。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。

内核本身的打印信息会通过/proc/kmsg显示,或者调用dmsg命令查看。

BUG_ON

在代码里面老能看到 BUG_ON() ,WARN_ON() 这样的宏,类似我们日常编程里面的断言(assert)。在include/asm-generic/bug.h里定义。

动态调试

动态调试是通过动态的开启和禁止某些内核代码来获取额外的内核信息。

首先内核选项CONFIG_DYNAMIC_DEBUG应该被设置。所有通过pr_debug()/dev_debug()打印的信息都可以动态的显示或不显示。

可以通过简单的查询语句来筛选需要显示的信息。源文件名、函数名、行号(包括指定范围的行号)、模块名、格式化字符串。将要打印信息的格式写入<debugfs>/dynamic_debug/control中。

插入探测点:kprobe

         内核中的对应机制是kprobe。kprobe是由IBM的Dprobe项目发展而来。使用过iptables的都知道,定义的规则实际上是在正常的内核代码执行流程中插入的钩子函数。而钩子函数不但能用来过滤和执行变化,还能用来调试。

         kprobe支持3种调试方式:第一种就叫做kprobe,用来在特定的内核代码位置添加代码(相当于自己手动添加了内核代码再编译执行)。第二种jprobe,可以用于调试内核函数的传入参数。第三种是kretprobe,用于调试内核函数的返回值。

         kprobe是通过回调的形式执行的,因此就有三种可能的回调方式:执行前、执行后和出错时的回调。

         使用kprobe一方面要在内核配置中打开kprobe支持,另一方面要在用户端使用工具,这个工具是systemtab。使用这个工具编写的脚本在执行时其会在后台编译生成内核模块,插入内核与kprobe的内核部分协作完成功能。

         那么kprobe是如何做到的呢?利用异常。当定义了插入点后,kprobe的内核部分就会在内存中将插入点附近的代码保存起来,用触发异常的代码替换,当执行到这里的时候异常被触发,回调函数被执行,执行完毕后恢复被保存的正常代码执行。

断点执行

kgdb

kgdb提供了一种使用 gdb调试 Linux 内核的机制。使用KGDB可以象调试普通的应用程序那样,在内核中进行设置断点、检查变量值、单步跟踪程序运行等操作。使用KGDB调试时需要两台机器,一台作为开发机(Development Machine),另一台作为目标机(Target Machine),两台机器之间通过串口或者以太网口相连。串口连接线是一根RS-232接口的电缆,在其内部两端的第2脚(TXD)与第3脚(RXD)交叉相连,第7脚(接地脚)直接相连。调试过程中,被调试的内核运行在目标机上,gdb调试器运行在开发机上。

kgdb补丁的主要作用是在Linux内核中添加了一个调试Stub。调试Stub是Linux内核中的一小段代码,提供了运行gdb的开发机和所调试内核之间的一个媒介。gdb和调试stub之间通过gdb串行协议进行通讯。gdb串行协议是一种基于消息的ASCII码协议,包含了各种调试命令。当设置断点时,kgdb负责在设置断点的指令前增加一条trap指令,当执行到断点时控制权就转移到调试stub中去。此时,调试stub的任务就是使用远程串行通信协议将当前环境传送给gdb,然后从gdb处接受命令。gdb命令告诉stub下一步该做什么,当stub收到继续执行的命令时,将恢复程序的运行环境,把对CPU的控制权重新交还给内核。

这个流程与kprobe很相似。

Linux内核工程导论——内核调试

kdb内核调试器

Kdb(Kernel Debug)是SGI公司开发的遵循GPL的内建Linux内核调试工具。标准的Linux内核不包括kdb,需要从ftp://oss.sgi.com/www/projects/kdb/download/ix86下载对应标准版本内核的kdb补丁,对标准内核打补丁,然后,编译打过补丁的内核代码。目前kdb支持包括x86(IA32)、IA64和MIPS在内的体系结构。

Kdb调试器是Linux内核的一部分,提供了检查内存和数据结构的方法。通过附加命令,它可以格式化显示给定地址或ID的基本系统数据结构。kdb当前的命令集可以完全控制内核的操作,包括单步运行一个处理器、在指定的指令执行处理暂停、在访问或修改指定虚拟内存的位置暂停、在输入-输出地址空间对一个寄存器访问处暂停、通过进程ID跟踪任务、指令反汇编等。

其他方法:

kexec

kexec是一套系统调用,允许用户从当前正执行的内核装载另一个内核。用户可用shell命令"yum install kexec-tools"安装kexec工具包,安装后,就可以使用kexec命令。

工具kexec直接启动进入一个新内核,它通过系统调用使用户能够从当前内核装载并启动进入另一个内核。在当前内核中,kexec执行BootLoader的功能。在标准系统启动和kexec启动之间的主要区别是:在kexec启动期间,依赖于硬件构架的固件或BIOS不会被执行来进行硬件初始化。这将大大降低重启动的时间。

为了让内核的kexec功能起作用,内核编译配置是应确认先择了"CONFIG_KEXEC=y",在配置后生成的.config文件中应可看到此条目。

工具kexec的使用分为两步,首先,用kexec将调试的内核装载进内存,接着,用kexec启动装载的内核。

装载内核的语法列出如下:

kexec -lkernel-image --append=command-line-options --initrd=initrd-image

上述命令中,参数kernel-image为装载内核的映射文件,该命令不支持压缩的内核映像文件bzImage,应使用非压缩的内核映射文件vmlinux;参数initrd-image为启动时使用initrd映射文件;参数command-line-options为命令行选项,应来自当前内核的命令行选项,可从文件"/proc/cmdline"中提取,该文件的内容列出如下:

^-^$ cat/proc/cmdline

roroot=/dev/VolGroup00/LogVol00 rhgb quiet

例如:用户想启动的内核映射为/boot/vmlinux,initrd为/boot/initrd,则kexec加载命令列出如下:

Kexec –l/boot/vmlinux –append=/dev/VolGroup00/LogVol00 initrd=/boot/initrd

还可以加上选项-p或--load-panic,表示装载新内核在系统内核崩溃使用。

在内核装载后,用下述命令启动装载的内核,并进行新的内核中运行:

kexec -e

当kexec将当前内核迁移到新内核上运行时,kexec拷贝新内核到预保留内存块,该保留位置如图1所示,原系统内核给kexec装载内核预保留一块内存(在图中的阴影部分),用于装载新内核,其他内存区域在未装载新内核时,由原系统内核使用。

kdump

kdump是基于kexec的崩溃转储机制(kexec-basedCrash Dumping),无论内核内核需要转储时,如:系统崩溃时,kdump使用kexec快速启动进入转储捕捉的内核。在这里,原运行的内核称为系统内核或原内核,新装载运行的内核称为转储捕捉的内核或装载内核或新内核。

在重启动过程中,原内核的内存映像被保存下来,并且转储捕捉的内核(新装载的内核)可以访问转储的映像。用户可以使用命令cp和scp将内存映射拷贝到一个本地硬盘上的转储文件或通过网络拷贝到远程计算机上。

当前仅x86, x86_64, ppc64和ia64构架支持kdump和kexec。

当系统内核启动时,它保留小部分内存给转储(dump)捕捉的内核,确保了来自系统内核正进行的直接内存访问(Direct Memory Access:DMA)不会破坏转储捕捉的内核。命令kexec –p装载新内核到这个保留的内存。

在崩溃前,所有系统内核的核心映像编码为ELF格式,并存储在内核的保留区域。ELF头的开始物理地址通过参数elfcorehdr=boot传递到转储捕捉的内核。

通过使用转储捕捉的内核,用户可以下面两种方式访问内存映像或旧内存:

(1)通过/dev/oldmem设备接口,捕捉工具程序能读取设备文件并以原始流的格式写出内存,它是一个内存原始流的转储。分析和捕捉工具必须足够智能以判断查找正确信息的位置。

(2)通过/proc/vmcore,能以ELF格式文件输出转储信息,用户可以用GDB(GNU Debugger)和崩溃调试工具等分析工具调试转储文件。

(3)建立快速重启动机制和安装工具

1)安装工具kexec-tools

可以下载源代码编译安装工具kexec-tools。由于工具kexec-tools还依赖于一些其他的库,因此,最好的方法是使用命令"yum install kexec-tools"从网上下载安装并自动解决依赖关系。

2)编译系统和转储捕捉的内核

可编译独立的转储捕捉内核用于捕捉内核的转储,还可以使用原系统内核作为转储捕捉内核,在这种情况下,不需要再编译独立的转储捕捉内核,但仅支持重定位内核的构架才可以用作转储捕捉的内核,如:构架i386和ia64支持重定位内核。

SysRq魔术组合键打印内核信息

SysRq"魔术组合键"是一组按键,由键盘上的"Alt+SysRq+[CommandKey]"三个键组成,其中CommandKey为可选的按键。SysRq魔术组合键根据组合键的不同,可提供控制内核或打印内核信息的功能。SysRq魔术组合键的功能说明如表1所示。

1 SysRq组合键的功能说明

键名

功能说明

b

在没有同步或卸载硬盘的情况下立即启动。

c

为了获取崩溃转储执行kexe重启动。

d

显示被持的所有锁。

e

发送信号SIGTERM给所有进程,除了init外。

f

将调用oom_kill杀死内存热进程。

g

在平台ppcsh上被kgdb使用。

h

显示帮助信息。

i

发送信号SIGKILL给所有的进程,除了init外。

k

安全访问密钥(Secure Access KeySAK)杀死在当前虚拟终端上的所有程序。

m

转储当前的内存信息到控制台。

n

用于设置实时任务为可调整nice的。

o

将关闭系统(如果配置为支持)。

p

打印当前寄存器和标识到控制台。

q

将转储所有正运行定时器的列表。

r

关闭键盘Raw模式并设置为XLATE模式。

s

尝试同步所有挂接的文件系统。

t

将转储当前的任务列表和它们的信息到控制台。

u

尝试以仅读的方式重挂接所有已挂接的文件系统。

v

转储Voyager SMP处理器信息到控制台。

w

转储的所有非可中断(已阻塞)状态的任务。

x

在平台ppc/powerpc上被xmonX监视器)接口使用。

0~9

设备控制台日志级别,控制将打印到控制台的内核信息。例如:0仅打印紧急信息,如:PANICOOPS信息。

默认SysRq组合键是关闭的。可用下面的命令打开此功能:

# echo 1> /proc/sys/kernel/sysrq



关闭此功能的命令列出如下:

# echo 0> /proc/sys/kernel/sysrq

命令strace

命令strace 显示程序调用的所有系统调用。使用 strace 工具,用户可以清楚地看到这些调用过程及其使用的参数,了解它们与操作系统之间的底层交互。当系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。

strace 的另一个用处是解决和动态库相关的问题。当对一个可执行文件运行ldd时,它会告诉你程序使用的动态库和找到动态库的位置。

锁验证器

锁调试内核锁验证器(Kernel lockvalidator)可以在死锁发生前检测到死锁,即使是很少发生的死锁。它将每个自旋锁与一个键值相关,相似的锁仅处理一次。加锁时,查看所有已获取的锁,并确信在其他上下文中没有已获取的锁,在新获取锁之后被获取。解锁时,确信正被解开的锁在已获取锁的顶部。

当加锁动态发生时,锁验证器映射所有加锁规则,该检测由内核的spinlocks、rwlocks、mutexes和rwsems等锁机制触发。不管何时锁合法性检测器子系统检测到一个新加锁场景,它检查新规则是否违反正存在的规则集,如果新规则与正存在的规则集一致,则加入新规则,内核正常运行。如果新规则可能创建一个死锁场景,那么这种创建死锁的条件会被打印出来。

当判断加锁的有效性时,所有可能的"死锁场景"会被考虑到:假定任意数量的CPU、任意的中断上下文和任务上下文群、运行所有正存在的加锁场景的任意组合。在一个典型系统中,这意味着有成千上万个独立的场景。这就是为什么称它为"加锁正确性"验证器,对于所有被观察的规则来说,锁验证器用数学的确定性证明死锁不可能发生,假定锁验证器实现本身正确,并且它内部的数据结构不会被其他内核子系统弄坏。

硬件模拟

         vmware创建虚拟机,可以模拟两台电脑的互连,由于vmware软件的高质量,不必担心模拟的不如真实的。

         skyeye也可以模拟硬件,它是一个纯粹的硬件模拟平台,对于开发arm的嵌入式系统上的linux十分有用。

使用UML调试Linux内核是在本机的linux上调试linux的好方法,User-modeLinux(UML)可以在用户端作为进程运行linux内核,如此可以轻松的使用gdb等用户端调试程序。