博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Linux设备驱动调试技术 2
阅读量:4052 次
发布时间:2019-05-25

本文共 11429 字,大约阅读时间需要 38 分钟。

4.3  通过监视调试 
有时,通过监视用户空间中应用程序的运行情况,可以捕捉到一些小问题。监视程序同样也有助于确认驱动程序工作是否正常。例如,看到 scull 的 read 实现如何响应不同数据量的 read 请求后,我们就可以判断它是否工作正常。

有许多方法可监视用户空间程序的工作情况。可以用调试器一步步跟踪它的函数,插入打印语句,或者在 strace 状态下运行程序。在检查内核代码时,最后一项技术最值得关注,我们将在此对它进行讨论。

strace 命令是一个功能非常强大的工具,它可以显示程序所调用的所有系统调用。它不仅可以显示调用,而且还能显示调用参数,以及用符号方式表示的返回值。当系统调用失败时,错误的符号值(如 ENOMEM)和对应的字符串(如Out of memory)都能被显示出来。strace 有许多命令行选项;最为有用的是 -t,用来显示调用发生的时间;-T,显示调用所花费的时间; -e,限定被跟踪的调用类型;-o,将输出重定向到一个文件中。默认情况下,strace将跟踪信息打印到 stderr 上。

strace从内核中接收信息。这意味着一个程序无论是否按调试方式编译(用 gcc 的 -g选项)或是被去掉了符号信息都可以被跟踪。与调试器可以连接到一个运行进程并控制它一样,strace 也可以跟踪一个正在运行的进程。

跟踪信息通常用于生成错误报告,然后发给应用开发人员,但是它对内核编程人员来说也同样非常有用。我们已经看到驱动程序是如何通过响应系统调用得到执行的;strace 允许我们检查每次调用中输入和输出数据的一致性。

例如,下面的屏幕信息显示了 strace ls /dev > /dev/scull0 命令的最后几行:


[...] 
open("/dev", O_RDONLY|O_NONBLOCK)     = 4 
fcntl(4, F_SETFD, FD_CLOEXEC)         = 0 
brk(0x8055000)                        = 0x8055000 
lseek(4, 0, SEEK_CUR)                 = 0 
getdents(4, , 3933)   = 1260 
[...] 
getdents(4, , 3933)    = 0 
close(4)                              = 0 
fstat(1, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0 
ioctl(1, TCGETS, 0xbffffa5c)          = -1 ENOTTY (Inappropriate ioctl 
                                                   for device) 
write(1, "MAKEDEV\natibm\naudio\naudio1\na"..., 4096) = 4000 
write(1, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 96) = 96 
write(1, "4\nsde5\nsde6\nsde7\nsde8\nsde9\n"..., 3325) = 3325 
close(1)                              = 0 
_exit(0)                              = ?

 


很明显,ls 完成对目标目录的检索后,在首次对 write 的调用中,它试图写入 4KB 数据。很奇怪(对于 ls 来说),实际只写了4000个字节,接着它重试这一操作。然而,我们知道scull的 write 实现每次最多只写一个量子(scull 中设置的量子大小为4000个字节),所以我们所预期的就是这样的部分写入。经过几个步骤之后,每件工作都顺利通过,程序正常退出。

另一个例子,让我们来对 scull 设备进行读操作(使用 wc 命令):


[...] 
open("/dev/scull0", O_RDONLY)           = 4 
fstat(4, {st_mode=S_IFCHR|0664, st_rdev=makedev(253, 0), ...}) = 0 
read(4, "MAKEDEV\natibm\naudio\naudio1\na"..., 16384) = 4000 
read(4, "d2\nsdd3\nsdd4\nsdd5\nsdd6\nsdd7"..., 16384) = 3421 
read(4, "", 16384)                      = 0 
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(3, 7), ...}) = 0 
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0 
write(1, "   7421 /dev/scull0\n", 20)   = 20 
close(4)                                = 0 
_exit(0)                                = ?

 


正如所料,read 每次只能读取4000个字节,但数据总量与前面例子中写入的数量是相同的。与上面的写跟踪相对比,请读者注意本例中重试工作是如何组织的。为了快速读取数据,wc 已被优化了,因而它绕过了标准库,试图通过一次系统调用读取更多的数据。可以从跟踪的 read 行中看到 wc 每次均试图读取 16KB 数据。

Linux行家可以在 strace 的输出中发现很多有用信息。如果觉得这些符号过于拖累的话,则可以仅限于监视文件方法(open,read 等)是如何工作的。

就个人观点而言,我们发现 strace 对于查找系统调用运行时的细微错误最为有用。通常应用或演示程序中的 perror 调用在用于调试时信息还不够详细,而 strace 能够确切查明系统调用的哪个参数引发了错误,这一点对调试是大有帮助的。

4.4  调试系统故障 
即使采用了所有这些监视和调试技术,有时驱动程序中依然会有错误,这样的驱动程序在执行时就会产生系统故障。在出现这种情况时,获取尽可能多的信息对解决问题是至关重要的。

注意,“故障”不意味着“panic”。Linux 代码非常健壮(用术语讲即为鲁棒,robust),可以很好地响应大部分错误:故障通常会导致当前进程崩溃,而系统仍会继续运行。如果在进程上下文之外发生故障,或是系统的重要组成被损害时,系统才有可能 panic。但如果问题出在驱动程序中时,通常只会导致正在使用驱动程序的那个进程突然终止。唯一不可恢复的损失就是进程被终止时,为进程上下文分配的一些内存可能会丢失;例如,由驱动程序通过 kmalloc 分配的动态链表可能丢失。然而,由于内核在进程中止时会对已打开的设备调用 close 操作,驱动程序仍可以释放由 open 方法分配的资源。

我们已经说过,当内核行为异常时,会在控制台上打印出提示信息。下一节将解释如何解码并使用这些消息。尽管它们对于初学者来说相当晦涩,不过处理器在出错时转储出的这些数据包含了许多值得关注的信息,通常足以查明程序错误,而无需额外的测试。

4.4.1  oops消息 
大部分错误都在于 NULL指针的使用或其他不正确的指针值的使用上。这些错误通常会导致一个 oops 消息。

由处理器使用的地址都是虚拟地址,而且通过一个复杂的称为页表(见第 13 章中的“页表”一节)的结构映射为物理地址。当引用一个非法指针时,页面映射机制就不能将地址映射到物理地址,此时处理器就会向操作系统发出一个“页面失效”的信号。如果地址非法,内核就无法“换页”到并不存在的地址上;如果此时处理器处于超级用户模式,系统就会产生一个“oops”。

值得注意的是,2.0 版本之后引入的第一个增强是,当向用户空间移动数据或者移出时,无效地址错误会被自动处理。Linus 选择了让硬件来捕捉错误的内存引用,所以正常情况(地址都正确时)就可以更有效地得到处理。

oops 显示发生错误时处理器的状态,包括 CPU 寄存器的内容、页描述符表的位置,以及其它看上去无法理解的信息。这些消息由失效处理函数(arch 
  *(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); 
   
  ret = copy_to_user(buf, faulty_buf, count); 
  if (!ret) return count;

  printk(KERN_DEBUG "didn't fail: retry\n"); 
   
  sprintf(stack_buf, "1234567\n"); 
  if (count > 8) count = 8;  
  ret2 = copy_to_user(buf, stack_buf, count); 
  if (!ret2) return count; 
  return ret2; 
}

 


这段程序首先从一个全局缓冲区读取数据,但并不检查数据的长度,然后通过对一个局部缓冲区进行写入操作,制造一次缓冲区溢出。第一个操作仅在 2.0 内核会导致 oops 的发生,因为后期版本能自动地处理用户拷贝函数。缓冲区溢出则会在所有版本的内核中造成 oops;然而,由于 return 指令把指令指针带到了不知道的地方,所以这种错误很难跟踪,所能获得的仅是如下的信息:


EIP:    0010:[<00000000>] 
[...] 
Call Trace: [<c010b860>] 
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:   
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 站点上,对它的维护是与内核相独立的。即使读者所用的仍是较早期的内核,或许还可以从 ftp://ftp.ocs.com.au/pub/ksymoops 站点上获取这个工具的升级版本。

为了取得最佳的工作状态,除错误消息之外,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 <sys_write+d6/100> 
Trace; c010b860 <system_call+34/38> 
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   �x,�x 
Code;  c48370cf <[faulty]faulty_write+f/20> 
 c:   89 ec             movl   �p,%esp 
Code;  c48370d1 <[faulty]faulty_write+11/20> 
 e:   5d                popl   �p 
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 <c0202000> \ 
30680005 01000000 01000000 01000000 01000000

 


请注意,指令转储并不是从引起错误的那个指令开始,而是之前的三条指令:这是因为 RISC 平台以并行的方式执行多条指令,这样可能产生延期的异常,因此必须能回溯最后的几条指令。

下面是当从 TSTATE 行开始输入数据时,ksymoops 所打印出的信息:


>>TPC; 0000000001000128 <[faulty].text.start+88/a0>   <===== 
>>O7;  0000000000457fb4 <sys_write+114/160> 
>>I7;  0000000000410114 <linux_sparc_syscall+34/40> 
Trace; 0000000000410114 <linux_sparc_syscall+34/40> 
Trace; 000000007007cba4 <END_OF_CODE+6f07c40d/????> 
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”一章中,对这种差异进行了很好的描述。)

转载地址:http://aosci.baihongyu.com/

你可能感兴趣的文章
Selenium-ActionChains Api接口详解
查看>>
Selenium-Switch与SelectApi接口详解
查看>>
Selenium-Css Selector使用方法
查看>>
Linux常用统计命令之wc
查看>>
测试必会之 Linux 三剑客之 sed
查看>>
Socket请求XML客户端程序
查看>>
Java中数字转大写货币(支持到千亿)
查看>>
Java.nio
查看>>
函数模版类模版和偏特化泛化的总结
查看>>
VMware Workstation Pro虚拟机不可用解决方法
查看>>
最简单的使用redis自带程序实现c程序远程访问redis服务
查看>>
redis学习总结-- 内部数据 字符串 链表 字典 跳跃表
查看>>
iOS 对象序列化与反序列化
查看>>
iOS 序列化与反序列化(runtime) 01
查看>>
iOS AFN 3.0版本前后区别 01
查看>>
iOS ASI和AFN有什么区别
查看>>
iOS QQ侧滑菜单(高仿)
查看>>
iOS 扫一扫功能开发
查看>>
iOS app之间的跳转以及传参数
查看>>
iOS __block和__weak的区别
查看>>