.4.1 oops消息
大部分错误都在于 NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息。
由处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第 13 章中的“页表”一节)的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,此时处理器就会向操作系统发出一个“页面失效”的信号。如果地址非法,内核就无法“换页”到并不存在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops”。
值得注意的是,2.0 版本之后引入的第一个增强是,当向用户空间移动数据或者移出时,无效地址错误会被自动处理。Linus 选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时)就可以更有效地得到处理。
oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其它看上去无法理解的信息。这些消息由失效处理函数(arch/*/kernel/traps.c)中的 printk 语句产生,就象前面“printk”一节所介绍的那样分发出来。
让我们看看这样一个消息。当我们在一台运行 2.4 内核的 PC 机上使用一个 NULL 指针时,就会导致下面这些信息显示出来。这里最为相关的信息就是指令指针(EIP),即出错指令的地址。
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU: 0
EIP: 0010:[]
EFLAGS: 00010286
eax: ffffffea ebx: c2281a20 ecx: c48370c0 edx: c2281a40
esi: 4000c000 edi: 4000c000 ebp: c38adf8c esp: c38adf8c
ds: 0018 es: 0018 ss: 0018
Process ls (pid: 23171, stackpage=c38ad000)
Stack: 0000010e c01356e6 c2281a20 4000c000 0000010e c2281a40 c38ac000 \
0000010e
4000c000 bffffc1c 00000000 00000000 c38adfc4 c010b860 00000001 \
4000c000
0000010e 0000010e 4000c000 bffffc1c 00000004 0000002b 0000002b \
00000004
Call Trace: [] []
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
这个消息是通过对 faulty 模块的一个设备进行写操作而产生的,faulty 这个模块专为演示出错而编写。faulty.c 中 write 方法的实现很简单:
ssize_t faulty_write (struct file *filp, const char *buf, size_t count,
loff_t *pos)
{
/* make a simple fault by dereferencing a NULL pointer */
*(int *)0 = 0;
return 0;
}
正如读者所见,我们这使用了一个 NULL 指针。因为 0 决不会是个合法的指针值,所以错误发生,内核进入上面的 oops 消息状态。这个调用进程接着就被杀掉了。在 read 实现中,faulty 模块还有更多有意思的错误状态。
char faulty_buf[1024];
ssize_t faulty_read (struct file *filp, char *buf, size_t count,
loff_t *pos)
{
int ret, ret2;
char stack_buf[4];
printk(KERN_DEBUG "read: buf %p, count %li\n", buf, (long)count);
/* the next line oopses with 2.0, but not with 2.2 and later */
ret = copy_to_user(buf, faulty_buf, count);
if (!ret) return count; /* we survived */
printk(KERN_DEBUG "didn't fail: retry\n");
/* For 2.2 and 2.4, let's try a buffer overflow */
sprintf(stack_buf, "1234567\n");
if (count > 8) count = 8; /* copy 8 bytes to the user */
ret2 = copy_to_user(buf, stack_buf, count);
if (!ret2) return count;
return ret2;
}
这段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度,然后通过对一个局部缓冲区进行写入操作,制造一次缓冲区溢出。第一个操作仅在 2.0 内核会导致 oops 的发生,因为后期版本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成 oops;然而,由于 return 指令把指令指针带到了不知道的地方,所以这种错误很难跟踪,所能获得的仅是如下的信息:
EIP: 0010:[<00000000>]
[...]
Call Trace: []
Code: Bad EIP value.
用户处理 oops 消息的主要问题在于,我们很难从十六进制数值中看出什么内在的意义;为了使这些数据对程序员更有意义,需要把它们解析为符号。有两个工具可用来为开发人员完成这样的解析:klogd 和 ksymoops。前者只要运行就会自行进行符号解码;后者则需要用户有目的地调用。下面的讨论,使用了在我们第一个 oops 例子中通过使用NULL 指针而产生的出错信息。
使用 klogd
klogd 守护进程能在 oops 消息到达记录文件之前对它们解码。很多情况下,klogd 可以为开发者提供所有必要的信息用于捕捉问题的所在,可是有时开发者必须给它一定的帮助。
当 faulty 的一个oops 输出送达系统日志时,转储信息看上去会是下面的情况(注意 EIP 行和 stack 跟踪记录中已经解码的符号):
Unable to handle kernel NULL pointer dereference at virtual address \
00000000
printing eip:
c48370c3
*pde = 00000000
Oops: 0002
CPU: 0
EIP: 0010:[faulty:faulty_write+3/576]
EFLAGS: 00010286
eax: ffffffea ebx: c2c55ae0 ecx: c48370c0 edx: c2c55b00
esi: 0804d038 edi: 0804d038 ebp: c2337f8c esp: c2337f8c
ds: 0018 es: 0018 ss: 0018
Process cat (pid: 23413, stackpage=c2337000)
Stack: 00000001 c01356e6 c2c55ae0 0804d038 00000001 c2c55b00 c2336000 \
00000001
0804d038 bffffbd4 00000000 00000000 bffffbd4 c010b860 00000001 \
0804d038
00000001 00000001 0804d038 bffffbd4 00000004 0000002b 0000002b \
00000004
Call Trace: [sys_write+214/256] [system_call+52/56]
Code: c7 05 00 00 00 00 00 00 00 00 31 c0 89 ec 5d c3 8d b6 00 00
klogd 提供了大多数必要信息用于发现问题。在这个例子中,我们看到指令指针(EIP)正执行于函数 faulty_write 中,因此我们就知道该从哪儿开始检查。字串 3/576 告诉我们处理器正处于函数的第3个字节上,而函数整体长度为 576 个字节。注意这些数值都是十进制的,而非十六进制。
然而,当错误发生在可装载模块中时,为了获取错误相关的有用信息,开发者还必须注意一些情况。klogd 在开始运行时装入所有可用符号,并随后使用这些符号。如果在 klogd 已经对自身初始化之后(一般在系统启动时),装载某个模块,那 klogd 将不会有这个模块的符号信息。强制 klogd取得这些信息的办法是,发送一个 SIGUSR1 信号给 klogd 进程,这种操作在时间顺序上,必须是在模块已经装入(或重新装载)之后,而在进行任何可能引起 oops 的处理之前。
还可以在运行 klogd 时加上 -p 选项,这会使它在任何发现 oops 消息的时刻重新读入符号信息。不过,klogd 的man 手册不推荐这个方法,因为这使 klogd 在出问题之后再向内核查询信息。而发生错误之后,所获得的信息可能是完全错误的了。
为了使 klogd 正确地工作,必须给它提供符号表文件 System.map 的一个当前复本。通常这个文件在 /boot 中;如果从一个非标准的位置编译并安装了一个内核,就需要把 System.map 拷贝到 /boot,或告知 klogd 到什么位置查看。如果符号表与当前内核不匹配,klogd 就会拒绝解析符号。假如一个符号被解析在系统日志中,那么就有理由确信它已被正确解析了。
使用 ksymoops
有些时候,klogd 对于跟踪目的而言仍显不足。开发者经常既需要取得十六进制地址,又要获得对应的符号,而且偏移量也常需要以十六进制的形式打印出来。除了地址解码之外,往往还需要更多的信息。对 klogd 来说,在出错期间被杀掉,也是常用的事情。在这些情况下,可以调用一个更为强大的 oops 分析器,ksymoops 就是这样的一个工具。
在 2.3 开发系列之前,ksymoops 是随内核源码一起发布的,位于 scripts 目录之下。它现在则在自己的FTP 站点上,对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核,或许还可以从 [url]ftp://ftp.ocs.com.au/pub/ksymoops[/url] 站点上获取这个工具的升级版本。
为了取得最佳的工作状态,除错误消息之外,ksymoops 还需要很多信息;可以使用命令行选项告诉它在什么地方能找到这些各个方面的内容。ksymoops 需要下列内容项:
System.map 文件这个映射文件必须与 oops 发生时正在运行的内核相一致。默认为 /usr/src/linux/System.map。
模块列表ksymoops 需要知道 oops 发生时都装入了哪些模块,以便获得它们的符号信息。如果未提供这个列表,ksymoops 会查看 /proc/modules。
在 oops 发生时已定义好的内核符号表默认从 /proc/ksyms 中取得该符号表。
当前正运行的内核映像的复本注意,ksymoops 需要的是一个直接的内核映像,而不是象 vmlinuz、zImage 或 bzImage 这样被大多数系统所使用的压缩版本。默认是不使用内核映像,因为大多数人都不会保存这样的一个内核。如果手边就有这样一个符合要求的内核的话,就应该采用 -v 选项告知 ksymoops 它的位置。
已装载的任何内核模块的目标文件位置ksymoops 将在标准目录路径寻找这些模块,不过在开发中,几乎总要采用 -o 选项告知 ksymoops 这些模块的存放位置。
虽然 ksymoops 会访问 /proc 中的文件来取得它所需的信息,但这样获得的结果是不可靠的。在 oops 发生和 ksymoops 运行的时间间隙中,系统几乎一定会重新启动,这样取自 /proc 的信息就可能与故障发生时的实际状态不符合。只要有可能,最好在引起 oops 发生之前,保存 /proc/modules 和 /proc/ksyms 的复本。
我们强烈建议驱动程序开发人员阅读 ksymoops 的手册页,这是一个很好的资料文档。
这个工具命令行中的最后一个参数是 oops 消息的位置;如果缺少这个参数,ksymoops 会按Unix 的惯例去读取标准输入设备。运气好的话,消息可以从系统日志中重新恢复;在发生很严重的崩溃情况时,我们可能不得不将这些消息从屏幕上抄下来,然后再敲进去(除非用的是串口控制台,这对内核开发人员来说,是非常棒的工具)。
注意,当 oops 消息已经被 klogd 处理过时,ksymoops 将会陷于混乱。如果 klogd 已经运行,而且 oops 发生后系统仍在运行,那么经常可以通过调用 dmesg 命令来获得一个干净的 oops 消息。
如果没有明确地提供全部的上述信息,ksymoops 会发出警告。对于载入模块未作符号定义这类的情况,它同样会发出警告。一个不作任何警告的 ksymoops 是很少见的。
ksymoops 的输出类似如下:
>>EIP; c48370c3 <[faulty]faulty_write+3/20> <=====
Trace; c01356e6
Trace; c010b860
Code; c48370c3 <[faulty]faulty_write+3/20>
00000000 <_EIP>:
Code; c48370c3 <[faulty]faulty_write+3/20> <=====
0: c7 05 00 00 00 movl $0x0,0x0 <=====
Code; c48370c8 <[faulty]faulty_write+8/20>
5: 00 00 00 00 00
Code; c48370cd <[faulty]faulty_write+d/20>
a: 31 c0 xorl %eax,%eax
Code; c48370cf <[faulty]faulty_write+f/20>
c: 89 ec movl %ebp,%esp
Code; c48370d1 <[faulty]faulty_write+11/20>
e: 5d popl %ebp
Code; c48370d2 <[faulty]faulty_write+12/20>
f: c3 ret
Code; c48370d3 <[faulty]faulty_write+13/20>
10: 8d b6 00 00 00 leal 0x0(%esi),%esi
Code; c48370d8 <[faulty]faulty_write+18/20>
15: 00
正如上面所看到的,ksymoops 提供的 EIP 和内核堆栈信息与 klogd 所做的很相似,不过要更为准确,而且是十六进制形式的。可以注意到,faulty_write 函数的长度被正确地报告为 0x20个字节。这是因为 ksymoops 读取了模块的目标文件,并从中获得了全部的有用信息。
而且在这个例子中,还可以得到错误发生处代码的汇编语言形式的转储输出。这些信息常被用于确切地判断发生了些什么事情;这里很明显,错误在于一个向 0 地址写入数据 0 的指令。
ksymoops 的一个有趣特点是,它可以移植到几乎所有 Linux 可以运行的平台上,而且还利用了 bfd (二进制格式描述)库同时支持多种计算机结构。走出 PC 的世界,我们可以看到 SPARC64 平台上显示的 oops 消息是何等的相似(为了便于排版有几行被打断了):
Unable to handle kernel NULL pointer dereference
tsk->mm->context = 0000000000000734
tsk->mm->pgd = fffff80003499000
\/ ____ \/
"@'/ .. \`@"
/_| \_ _/ |_\
\_ _ _/
ls(16740): Oops
TSTATE: 0000004400009601 TPC: 0000000001000128 TNPC: 0000000000457fbc \
Y: 00800000
g0: 000000007002ea88 g1: 0000000000000004 g2: 0000000070029fb0 \
g3: 0000000000000018
g4: fffff80000000000 g5: 0000000000000001 g6: fffff8000119c000 \
g7: 0000000000000001
o0: 0000000000000000 o1: 000000007001a000 o2: 0000000000000178 \
o3: fffff8001224f168
o4: 0000000001000120 o5: 0000000000000000 sp: fffff8000119f621 \
ret_pc: 0000000000457fb4
l0: fffff800122376c0 l1: ffffffffffffffea l2: 000000000002c400 \
l3: 000000000002c400
l4: 0000000000000000 l5: 0000000000000000 l6: 0000000000019c00 \
l7: 0000000070028cbc
i0: fffff8001224f140 i1: 000000007001a000 i2: 0000000000000178 \
i3: 000000000002c400
i4: 000000000002c400 i5: 000000000002c000 i6: fffff8000119f6e1 \
i7: 0000000000410114
Caller[0000000000410114]
Caller[000000007007cba4]
Instruction DUMP: 01000000 90102000 81c3e008 \
30680005 01000000 01000000 01000000 01000000
请注意,指令转储并不是从引起错误的那个指令开始,而是之前的三条指令:这是因为 RISC 平台以并行的方式执行多条指令,这样可能产生延期的异常,因此必须能回溯最后的几条指令。
下面是当从 TSTATE 行开始输入数据时,ksymoops 所打印出的信息:
>>TPC; 0000000001000128 <[faulty].text.start+88/a0> <=====
>>O7; 0000000000457fb4
>>I7; 0000000000410114
Trace; 0000000000410114
Trace; 000000007007cba4
Code; 000000000100011c <[faulty].text.start+7c/a0>
0000000000000000 <_TPC>:
Code; 000000000100011c <[faulty].text.start+7c/a0>
0: 01 00 00 00 nop
Code; 0000000001000120 <[faulty].text.start+80/a0>
4: 90 10 20 00 clr %o0 ! 0 <_TPC>
Code; 0000000001000124 <[faulty].text.start+84/a0>
8: 81 c3 e0 08 retl
Code; 0000000001000128 <[faulty].text.start+88/a0> <=====
c: c0 20 20 00 clr [ %g0 ] <=====
Code; 000000000100012c <[faulty].text.start+8c/a0>
10: 30 68 00 05 b,a %xcc, 24 <_TPC+0x24> \
0000000001000140 <[faulty]faulty_write+0/20>
Code; 0000000001000130 <[faulty].text.start+90/a0>
14: 01 00 00 00 nop
Code; 0000000001000134 <[faulty].text.start+94/a0>
18: 01 00 00 00 nop
Code; 0000000001000138 <[faulty].text.start+98/a0>
1c: 01 00 00 00 nop
Code; 000000000100013c <[faulty].text.start+9c/a0>
20: 01 00 00 00 nop
要打印出上面显示的反汇编代码,我们就必须告知 ksymoops 目标文件的格式和结构(之所以需要这些信息,是因为 SPARC64 用户空间的本地结构是32位的)。本例中,使用选项 -t elf64-sparc -a sparc:v9 可进行这样的设置。
读者可能会抱怨对调用的跟踪并没带回什么值得注意的信息;然而,SPARC 处理器并不会把所有的调用跟踪记录保存到堆栈中:07 和 I7 寄存器保存了最后调用的两个函数的指令指针,这就是它们出现在调用跟踪记录边上的原因。在这个例子中,我们可以看到,故障指令位于一个由 sys_write 调用的函数中。
要注意的是,无论平台/结构是怎样的一种配合情况,用来显示反汇编代码的格式与 objdump 程序所使用的格式是一样的。objdump 是个很强大的工具;如果想查看发生故障的完整函数,可以调用命令: objdump -d faulty.o(再次重申,对于 SPARC64 平台,需要使用特殊选项:--target elf64-sparc-architecture sparc:v9)。
关于 objdump 和它的命令行选项的更多信息,可以参阅这个命令的手册页帮助。
学习对 oops 消息进行解码,需要一定的实践经验,并且了解所使用的目标处理器,以及汇编语言的表达习惯等。这样的准备是值得的,因为花费在学习上的时间很快会得到回报。即使之前读者已经具备了非 Unix 操作系统中PC 汇编语言的专门知识,仍有必要花些时间对此进行学习,因为Unix 的语法与 Intel 的语法并不一样。(在 as 命令 infor 页的“i386-specific”一章中,对这种差异进行了很好的描述。)
阅读(937) | 评论(0) | 转发(0) |