您的位置:首页 > 运维架构 > Linux

Linux设备驱动调试技术 3

2013-12-19 20:33 218 查看
4.4.2 系统挂起

尽管内核代码中的大多数错误仅会导致一个oops
消息,但有时它们则会将系统完全挂起。如果系统挂起了,任何消息都无法打印。例如,如果代码进入一个死循环,内核就会停止进行调度,系统不会再响应任何动作,包括
Ctrl-Alt-Del
组合键。处理系统挂起有两个选择――要么是防范于未然;要么就是亡羊补牢,在发生挂起后调试代码。

通过在一些关键点上插入 schedule 调用可以防止死循环。schedule
函数(正如读者猜到的)会调用调度器,并因此允许其他进程“偷取”当然进程的CPU时间。如果该进程因驱动程序的错误而在内核空间陷入死循环,则可以在跟踪到这种情况之后,借助
schedule 调用杀掉这个进程。

当然,应该意识到任何对 schedule 的调用都可能给驱动程序带来代码重入的问题,因为 schedule
允许其他进程开始运行。假设驱动程序进行了合适的锁定,这种重入通常还并不致于带来问题。不过,一定不要在驱动程序持有spinlock
的任何时候调用 schedule。

如果驱动程序确实会挂起系统,而又不知该在什么位置插入 schedule
调用时,最好的方法是加入一些打印信息,并把它们写入控制台(通过修改 console_loglevel 的数值)。

有时系统看起来象挂起了,但其实并没有。例如,如果键盘因某种奇怪的原因被锁住了就会发生这种情况。运行专为探明此种情况而设计的程序,通过查看它的输出情况,可以发现这种假挂起。显示器上的时钟或系统负荷表就是很好的状态监视器;只要它保持更新,就说明
scheduler
正在工作。如果没有使用图形显示,则可以运行一个程序让键盘LED闪烁,或不时地开关软驱马达,或不断触动扬声器(通常蜂鸣声是令人烦恼的,应尽量避免;可改为寻求
ioctl 命令 KDMKTONE ),来检查 scheduler 是否工作正常。O’Reilly
FTP站点上可以找到一个例子(misc-progs/heartbeat.c),它会使键盘LED不断闪烁。

如果键盘不接收输入,最佳的处理方法是从网络登录到系统中,杀掉任何违例的进程,或是重新设置键盘(用 kdb_mode
-a)。然而,如果没有可用的网络用来帮助恢复的话,即使发现了系统挂起是由键盘死锁造成的也没有用了。如果是这样的情况,就应该配置一种可替代的输入设备,以便至少可以正常地重启系统。比起去按所谓的“大红钮”,在你的计算机上,通过替代的输入设备来关机或重启系统要更为容易些,而且它可以免去fsck对磁盘的长时间扫描。

例如,这种替代输入设备可以是鼠标。1.10或更新版本的 gpm
鼠标服务器可以通过命令行选项支持类似的功能,不过仅限于文本模式。如果没有网络连接,并且以图形方式运行,则建议采用某些自定义的解决方案,比如,设置一个与串口线
DCD 针脚相连的开关,并编写一个查询 DCD 信号状态变化的脚本,用于从外界干预键盘已被死锁的系统。

对于上述情形,一个不可缺少的工具是“magic SysRq key”,2.2
和后期版本内核中,在其它体系结构上也可利用得到它。SysRq 魔法键是通过PC键盘上的 ALT 和 SysRq 组合键来激活的,在
SPARC 键盘上则是 ALT 和 Stop
组合键。连同这两个键一起按下的第三个键,会执行许多有用动作中的其中一种,这些动作如下:

r

在无法运行 kbd_mode 的情况中,关闭键盘的 raw 模式。

k

激活“留意安全键”(SAK)功能。SAK 将杀掉当前控制台上运行的所有进程,留下一个干净的终端。

s

对所有磁盘进行紧急同步。

u

尝试以只读模式重新挂装所有磁盘。这个操作通常紧接着 s
动作之后被调用,它可以在系统处于严重故障状态时节省很多检查文件系统的时间。

b

立即重启系统。注意先要同步并重新挂装磁盘。

p

打印当前的寄存器信息。

t

打印当前的任务列表。

m

打印内存信息。

还有其它的一些 SysRq 功能;要获得完整列表,可参阅内核源码 Documentation 目录下的sysrq.txt
文件。注意,SysRq
功能必须明确地在内核配置中被开启,出于安全原因,大多数发行系统并未开启它。不过,对于一个用于驱动程序开发的系统来说,为开启 SysRq
功能而带来的重新编译新内核的麻烦是值得的。SysRq 必须在运行时通过下面的命令启动:

echo 1 > /proc/sys/kernel/sysrq

在复现系统的挂起故障时,另一个要采取的预防措施是,把所有的磁盘都以只读的方式挂装在系统上(或干脆就卸装它们)。如果磁盘是只读的或者并未挂装,就不会发生破坏文件系统或致使文件系统处于不一致状态的危险。另一个可行方法是,使用通过
NFS
(网络文件系统)将其所有文件系统挂装入系统的计算机。这个方法要求内核具有“NFS-Root”的能力,而且在引导时还需传入一些特定参数。如果采用这种方法,即使我们不借助于
SysRq,也能避免任何文件系统的崩溃,因为NFS 服务器管理文件系统的一致性,而它并不受驱动程序的影响。

4.5 调试器和相关工具

最后一种调试模块的方法就是使用调试器来一步步地跟踪代码,查看变量和计算机寄存器的值。这种方法非常耗时,应该尽量避免。不过,某些情况下通过调试器对代码进行细粒度的分析是很有价值的。

在内核中使用交互式调试器是一个很复杂的问题。出于对系统所有进程的整体利益考虑,内核在它自己的地址空间中运行。其结果是,许多用户空间下的调试器所提供的常用功能很难用于内核之中,比如断点和单步调试等。本节着眼于调试内核的几种方法;它们中的每一种都各有利弊。

4.5.1 使用 gdb

gdb在探究系统内部行为时非常有用。在我们这个层次上,熟练使用调试器,需要掌握 gdb
命令、了解目标平台的汇编代码,还要具备对源代码和优化后的汇编码进行匹配的能力。

启动调试器时必须把内核看作是一个应用程序。除了指定未压缩的内核映像文件名外,还应该在命令行中提供“core
文件”的名称。对于正运行的内核,所谓 core 文件就是这个内核在内存中的核心映像,/proc/kcore。典型的 gdb
调用如下所示:

gdb /usr/src/linux/vmlinux /proc/kcore

第一个参数是未经压缩的内核可执行文件的名字,而不是 zImage 或 bzImage 以及其他任何压缩过的内核。

gdb命令行的第二个参数是是 core 文件的名字。与其它 /proc中的文件类似,/proc/kcore也是在被读取时产生的。当在
/proc文件系统中执行 read 系统调用时,它会映射到一个用于数据生成而不是数据读取的函数上;我们已在“使用
/proc文件系统”一节中介绍了这个特性。kcore 用来按照 core
文件的格式表示内核“可执行文件”;由于它要表示对应于所有物理内存的整个内核地址空间,所以是一个非常巨大的文件。在 gdb
的使用中,可以通过标准gdb命令查看内核变量。例如,p jiffies可以打印从系统启动到当前时刻的时钟滴答数。

从gdb 打印数据时,内核仍在运行,不同数据项的值会在不同时刻有所变化;然而,gdb为了优化对 core
文件的访问,会将已经读到的数据缓存起来。如果再次查看jiffies变量,仍会得到和上次一样的值。对通常的 core
文件来说,对变量值进行缓存是正确的,这样可避免额外的磁盘访问。但对“动态的”core 文件来说就不方便了。解决方法是在需要刷新gdb
缓冲区的时候,执行命令core-file /proc/kcore;调试器将使用新的 core
文件并丢弃所有的旧信息。不过,读新数据时并不总是需要执行core-file 命令;gdb 以几KB大小的小数据块形式读取 core
文件,缓存的仅是已经引用的若干小块。

对内核进行调试时,gdb 通常能提供的许多功能都不可用。例如,gdb 不能修改内核数据;因为在处理其内存映像之前,gdb
期望把待调试程序运行在自己的控制之下。同样,也不能设置断点或观察点,或者单步跟踪内核函数。

如果用调试选项(-g)编译了内核,产生的 vmlinux 会比没有使用 -g选项的更适合于gdb。不过要注意,用
-g选项编译内核需要大量的磁盘空间(每个目标文件和内核自身都会比通常的大三倍甚至更多)。

在非PC类计算机上,情况则不尽相同。在 Alpha 上,make boot会在生成可启动映像前将调试信息去掉,所以最终会获得
vmlinux 和 vmlinux.gz 两个文件。gdb
可以使用前者,后者用来启动。在SPARC上,默认情况则是不把内核(至少是2.0内核)调试信息去掉。

当用 -g选项编译内核并且和 /proc/kcore一起使用 vmlinux 运行调试器时,gdb
可以返回很多内核内部信息。例如,可以使用下面的命令来转储结构数据,如p *module_list、p
*module_list->next 和 p
*chrdevs[4]->fops 等。为了在使用 p
命令时取得最好效果,有必要保留一份内核映射表和随手可及的源码。

利用 gdb 可在当前内核上执行的另一个有用任务是,通过disassemble命令(可缩写为 disass
)或是“检查指令”(x/i)命令对函数进行反汇编。disassemble 命令的参数可以是函数名或是内存范围;而 x/i
则使用一个内存地址做为参数,也可以是符号名称的形式。例如,可以用 x/20i 反汇编 20
条指令。注意,不能反汇编一个模块的函数,因为调试器作用的是
vmlinux,它并不知道模块的情况。如果试图通过地址反汇编模块代码,gdb 很有可能会返回“Cannot access memory
at xxxx(不能访问 xxxx 处的内存)”这样的信息。基于同样的原因,也不能查看属于模块的数据项。如果已知道变量的地址,可以从
/dev/mem 中读出它们的值,但要弄明白从系统内存中分解出的原始数据的含义,难度是相当大的。

如果需要反汇编模块函数,最好对模块的目标文件用 objdump
工具进行处理。很不幸,该工具只能对磁盘上的文件复本进行处理,而不能对运行中的模块进行处理;因此,由objdump给出的地址都是未经重定位的地址,与模块的运行环境无关。对未经链接的目标文件进行反汇编的另一个不利因素在于,其中的函数调用仍是未作解析的,所以就无法轻松地区分是对
printk 的调用呢,还是对 kmalloc 的调用。

正如上面看到的,当目的在于查看内核的运行情况时,gdb是一个有用的工具,但对于设备驱动程序的调试,它还缺少一些至关重要的功能。

4.5.2 kdb 内核调试器

很多读者可能会奇怪这一点,即为什么不把一些更高级的调试功能直接编译进内核呢。答案很简单,因为 Linus
不信任交互式的调试器。他担心这些调试器会导致一些不良的修改,也就是说,修补的仅是一些表面现象,而没有发现问题的真正原因所在。因此,没有在内核中内建调试器。

然而,其他的内核开发人员偶尔也会用到一些交互式的调试工具。kdb 就是其中一种内建的内核调试器,它在 oss.sgi.com
上以非正式的补丁形式提供。要使用 kdb,必须首先获得这个补丁(取得的版本一定要和内核版本相匹配),然后对当前内核源码进行 patch
操作,再重新编译并安装这个内核。注意,kdb 仅可用于 IA-32(x86) 系统(虽然用于 IA-64
的一个版本在主流内核源码中短暂地出现过,但很快就被删去了)。

一旦运行的是支持 kdb 的内核,有几个方法可以进入 kdb 的调试状态。在控制台上按下 Pause(或
Break)键将启动调试。当内核发生 oops,或到达某个断点时,也会启动 kdb。无论是哪一种情况,都看到下面这样的消息:

Entering kdb (0xc1278000) on processor 1 due to Keyboard Entry
[1]kdb>

注意,当 kdb 运行时,内核所做的每一件事情都会停下来。当激活 kdb
调试时,系统不应运行其他的任何东西;尤其是,不要开启网络――当然,除非是在调试网络驱动程序。一般来说,如果要使用 kdb
的话,最好在启动时进入单用户模式。

作为一个例子,考虑下面这个快速的 scull 调试过程。假定驱动程序已被载入,可以象下面这样指示 kdb 在 scull_read
函数中设置一个断点:

[1]kdb> bp scull_read

Instruction(i) BP #0 at 0xc8833514
(scull_read)
is enabled on cpu 1

[1]kdb> go

bp 命令指示 kdb 在内核下一次进入 scull_read 时停止运行。随后我们输入 go 继续执行。在把一些东西放入 scull
的某个设备之后,我们可以在另一个终端的 shell 中运行 cat 命令尝试读取这个设备,这样一来就会产生如下的状态:

Entering kdb (0xc3108000) on processor 0 due to Breakpoint @
0xc8833515

Instruction(i) breakpoint #0 at
0xc8833514

scull_read+0x1:
movl
%esp,�p

[0]kdb>

我们现在正处于 scull_read 的开头位置。为了查明是怎样到达这个位置的,我们可以看看堆栈跟踪记录:

[0]kdb> bt

EBP
EIP
Function(args)

0xc3109c5c 0xc8833515
scull_read+0x1

0xc3109fbc 0xfc458b10 scull_read+0x33c255fc( 0x3,
0x803ad78, 0x1000,

0x1000, 0x804ad78)

0xbffffc88 0xc010bec0
system_call

[0]kdb>

kdb 试图打印出调用跟踪所记录的每个函数的参数列表。然而,它往往会被编译器所使用的优化技巧弄糊涂。所以在这个例子中,虽然
scull_read 实际只有四个参数,kdb 却打印出了五个。

下面我们来看看如何查询数据。mds 命令是用来对数据进行处理的;我们可以用下面的命令查询 scull_devices
指针的值:

[0]kdb> mds scull_devices
1

c8836104: c4c125c0 ....

在这里,我们请求查看的是从 scull_devices
指针位置开始的一个字大小(4个字节)的数据;应答告诉我们设备数据数组的起始地址位于
c4c125c0。要查看设备结构自身的数据值,我们需要用到这个地址:

[0]kdb> mds c4c125c0

c4c125c0: c3785000
....

c4c125c4: 00000000
....

c4c125c8: 00000fa0
....

c4c125cc: 000003e8
....

c4c125d0: 0000009a
....

c4c125d4: 00000000
....

c4c125d8: 00000000
....

c4c125dc: 00000001 ....

上面的8行分别对应于 Scull_Dev 结构中的8个成员。因此,通过显示的这些数据,我们可以知道,第一个设备的内存是从
0xc3785000 开始分配的,链表中没有下一个数据项,量子大小为 4000(十六进制形式为 fa0)字节,量子集大小为
1000(十六进制形式为 3e8),这个设备中有 154 个字节(十六进制形式为 9a)的数据,等等。

kdb 还可以修改数据。假设我们要从设备中削减一些数据:

[0]kdb> mm c4c125d0 0x50

0xc4c125d0 = 0x50

接下来对设备的 cat 操作所返回的数据就会少于上次。

kdb
还有许多其他的功能,包括单步调试(根据指令,而不是C源代码行),在数据访问中设置断点,反汇编代码,跟踪链表,访问寄存器数据等等。加上
kdb 补丁之后,在内核源码树的 Documentation/kdb 目录可以找到完整的手册页。

4.5.3 集成的内核调试器补丁

有很多内核开发人员为一个名为“集成的内核调试器”的非正式补丁作出过贡献,我们可将其简称为 IKD(integrated kernel
debugger)。IKD 提供了很多值得关注的内核调试工具。x86 是这个补丁的主要平台,不过它也可以用于其它的结构体系之上。IKD
补丁可以从 ftp://ftp.kernel.org/pub/linux/kernel/people/andrea/ikd 下载。它是一个必须应用于内核源码的patch 补丁;因为这个 patch
是与版本相关的,所以要确保下载的补丁与正使用的内核版本相一致。

IKD
补丁的功能之一是内核堆栈调试。如果开启这个功能,内核就会在每个函数调用时检查内核堆栈的空闲空间的大小,如果过小的话就会强制产生一个
oops。如果内核中的某些事情引起堆栈崩溃,这个工具就能用来帮助查找问题。这其实也就是一种“堆栈计量表”的功能,可以在任何特定的时刻查看堆栈的填充程度。

IKD 补丁还包含了一些用于发现内核死锁的工具。如果某个内核过程持续时间过久而没有得到调度的话,“软件死锁”探测器就会强制产生一个
oops。这是简单地通过对函数调用进行计数来实现的,如果计数值超过了一个预定义的阈值,探测器就会动作,并中止一些工作。IKD
的另一个功能是可以连续地把程序计数器打印到虚拟控制台上,这可以作为跟踪死锁的最后手段。“信号量死锁”探测器则是在某个进程的 down
调用持续时间过久时强制产生 oops。

IKD
中的其它调试功能包括内核的跟踪功能,它可以记录内核代码的执行路径。还有一些内存调试工具,包括一个内存泄漏探测器和一些称为“poisoner”的工具,它们在跟踪内存崩溃问题时非常有用。

最后,IKD 也包含前一节讨论过的 kdb 调试器。不过,IKD 补丁中的 kdb 版本有些老。如果需要 kdb 的话,我们推荐直接从
oss.sgi.com 获取当前的版本。

4.5.4 kgdb 补丁

kgdb 是一个在Linux 内核上提供完整的 gdb 调试器功能的补丁,不过仅限于 x86
系统。它通过串口连线以钩子的形式挂入目标调试系统进行工作,而在远端运行 gdb。使用 kgdb
时需要两个系统――一个用于运行调试器,另一个用于运行待调试的内核。和 kdb 一样,kgdb 目前可从 oss.sgi.com
获得。

设置 kgdb包括安装内核补丁并引导打过补丁之后的内核两个步骤。两个系统之间需要通过串口电缆(或空调制解调器电缆)进行连接,在 gdb
这一侧,需要安装一些支持文件。kgdb 补丁把详细的用法说明放在了文件
Documentation/i386/gdb-serial.txt
中;我们在这里就不再赘述。建议读者阅读关于“调试模块”的说明:接近末尾的地方,有一些出于这个目的而编写的很好的 gdb 宏。

4.5.5 内核崩溃转储分析器

崩溃转储分析器使系统能把发生 oops
时的系统状态记录下来,以便在随后空闲的时候查看这些信息。如果是对于一个异地用户的驱动程序进行支持,这些工具就会特别有用。用户可能不太愿意把
oops
复制下来,因此安装崩溃转储系统可以使技术支持人员不必依赖于用户的工作,也能获得用于跟踪用户问题的必要信息。也正是出于这样的原因,可供利用的崩溃转储分析器都是由那些对用户系统进行商业支持的公司开发的,这也就不足为奇了。

目前有两个崩溃转储分析器的补丁可以用于
Linux。在编写本节的时候,这两个工具都比较新,而且都处在不断的变化之中。与其提供可能已经过时的详细信息,我们倒不如只是给出一个概观,并指点读者在哪里可以找到更多的信息。

第一个分析器是 LKCD(Linux Kernel Crash Dumps,“Linux内核崩溃转储”)。这个工具仍可以从
oss.sgi.com 上获得。当内核发生 oops 时,LKCD
会把当前系统状态(主要指内存)写入事先指定好的转储设备中。这个转储设备必须是一个系统交换区。下次重启中(在存储交换功能开启之前)系统会运行一个称为
LCRASH 的工具,来生成崩溃的概要记录,并可选择地把转储的复本保存在一个普通文件中。LCRASH
可以交互方式地运行,提供了很多调试器风格的命令,用以查询系统状态。

LKCD 目前只支持 Intel 32位体系结构,并只能用在 SCSI 磁盘的交换分区上。

另一个崩溃转储设施可以从 www.missioncriticallinux.com 获得。这个崩溃转储子系统直接在目录
/var/dumps
中创建崩溃转储文件,而且并不使用交换区。这样就使某些事情变得更为容易,但也意味着在知道问题已经出现在哪里的时候,文件系统已被系统修改。生成的崩溃转储的格式是标准的
core 文件格式,所以可以利用 gdb 这类工具进行事后的分析。这个工具包也提供了另外的分析器,可以从崩溃转储文件中解析出比 gdb
更丰富的信息。

4.5.6 用户模式的 Linux 虚拟机

用户模式 Linux 是一个很有意思的概念。它作为一个独立的可移植的 Linux 内核而构建,包含在子目录 arch/um
中。然而,它并不是运行在某种新的硬件上,而是运行在基于 Linux 系统调用接口所实现的虚拟机之上。因此,用户模式 Linux 可以使
Linux 内核成为一个运行在 Linux 系统之上单独的、用户模式的进程。

把一个内核的复本当作用户模式下的进程来运行可以带来很多好处。因为它运行在一个受约束的虚拟处理器之上,所以有错误的内核不会破坏“真正的”系统。对软/硬件的不同配置可以在相同的框架中轻易地进行尝试。并且,对于内核开发人员来说最值得注目的特点在于,可以很容易地利用
gdb 或其它调试器对用户模式 Linux 进行处理。归根结底,它只是一个进程。很明显,用户模式 Linux
有潜力加快内核的开发过程。

迄今为止,用户模式 Linux 虚拟机还未在主流内核中发布;要下载它,必须访问它的 web
站点(http://user-mode-linux.sourceforge.net)。需要提醒的是,它仅可以集成到 2.4.0
之后的早期 2.4 内核版本中;当然等到本书出版的时候,版本支持方面可能会做得更好。

目前,用户模式 Linux
虚拟机也存在一些重大的限制,不过大部分可能很快就会得到解决。虚拟处理器当前只能工作于单处理器模式;虽然虚拟机可以毫无问题地运行在
SMP 系统上,但它仍是把主机模拟成单 CPU
模式。不过,对于驱动编写者来说,最大的麻烦在于,用户模式内核不能访问主机系统上的硬件设备。因此,尽管用户模式
Linux虚拟机对于本书中的大多数样例驱动程序的调试非常有用,却无法用于调试那些处理实际硬件的驱动程序。最后一点,用户模式
Linux虚拟机仅能运行在 IA-32 体系结构之上。

因为对所有这些问题的修补工作正在进行之中,所以在不远的将来,对于 Linux 设备驱动程序的开发人员,用户模式
Linux虚拟机可能会成为一个不可或缺的工具。

4.5.7 Linux 跟踪工具包

Linux
跟踪工具包(LTT)是一个内核补丁,包含了一组可以用于内核事件跟踪的相关工具集。跟踪内容包括时间信息,而且还能合理地建立在一段指定时间内所发生事件的完整图形化描述。因此,LTT不仅能用于调试,还能用来捕捉性能方面的问题。

在 Web 站点 www.opersys.com/LTT 上,可以找到 LTT 以及大量的资料。

4.5.8 Dynamic Probes

Dynamic Probes (或 DProbes )是 IBM 为基于 IA-32 结构的Linux 发布的一种调试工具(遵循
GPL
协议)。它可以在系统的几乎任何一个地方放置一个“探针”,既可以是用户空间也可以是内核空间。这个探针由一些当控制到达指定地点即开始执行的代码(用一种特别设计的,面向堆栈的语言编写)组成。这种代码能把信息传送回用户空间,修改寄存器,或者完成许多其它的工作。DProbes
很有用的特点是,一旦内核编译进了这个功能,探针就可以插到一个运行系统的任一个位置,而无需重建内核或重新启动。DProbes 也可以协同
LTT 工具在任意位置插入新的跟踪事件。

DProbes 工具可以从 IBM 的开放源码站点,即 http://oss.software.ibm.com 上下载.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: