首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

战胜 Linux 中的 Bug

战胜 Linux 中的 Bug

当某个进程崩溃时,日志文件(/var/log/messages)中就会给出附加的信息,包括程序终止原因、故障地址,以及包含程序状态字(PSW)、通用寄存器和访问寄存器的简要寄存器转储。
图 1表明程序(名为“simple”)以一个程序中断代码 0x10 终止(操作系统原理表明这是一个段转换错误),而故障地址为 0。毫无疑问,有人使用了空指针。现在我们知道发生了什么,下面需要弄清它发生在何处。      
基本的诊断User Debug 日志条目所提供的信息可用于确定程序的崩溃位置。一些可用的工具可帮助解决您可能会遇到的各种程序终止问题。我们将在本文中逐步介绍那些工具。
首先,让我们检查一下该日志条目中的用户 PSW。该 PSW 包含指令地址、状态码以及关于机器状态的其他信息。眼下,我们仅关心指令地址(第 33 至第 63 位)。为简化起见,让我们假设用户 PSW 是 070dc000 80400618。记住,我们是在考察一个 ESA/390(31 位寻址)PSW。第 32 位不是指令地址的一部分,它是指示 31 位寻址模式的标志,但是在研究 PSW 值时必须处理它。为了获得实际的指令指针,可把 PSW 的第二个字减去 0x80000000。结果是一个指令地址 0x400618。为了定位代码,您需要可执行文件中的一些信息。首先使用 readelf 来打印一些程序头信息。
显示了 readelf -l simple 的结果(记住“simple”是我们的测试程序的名称)。在 Program Headers 部分,第一个 LOAD 行提供了关于程序从哪里加载的信息。在 Flg 列,该段被标记为 R(read)E(executable)。VirtAddr 是程序开始加载的地址。MemSiz 是正在被加载到这个段中的代码长度。把它加到 VirtAddr 上,这个程序的基本地址范围就是 0x400000-0x400990。程序发生崩溃的指令地址为 0x400618,在程序的加载范围之内。现在我们知道了问题直接发生在代码中。      
如果可执行文件包括调试符号,那么确定哪一行代码导致了问题是可以做到的。对该地址和可执行文件使用 addr2line 程序,如下所示:
1
addr2line -e simple 0x400618




将返回:
1
/home/devuser/simple.c:34




要研究该问题,可以检查第 34 行。
对于 中原始的程序崩溃,PSW 为 070dc000 c00ab738。要获得指令地址,可减去 0x80000000。结果为 0x400ab738。这个地址并不准确地落在我们的小程序之内。那么,它是什么呢?是来自共享库的代码。如果对可执行文件运行 ldd 命令(ldd simple),将会返回程序运行所需的共享对象的列表,以及该库在那里可用的地址。      
1
2
3
libc.so.6 => /lib/libc.so.6 (0x40021000)
        <br>
          /lib/ld.so.1 => /lib/ld.so.1 (0x40000000)




该指令地址对应于加载 libc.so.6 的地址。在我们的简单测试案例中,只需要两个共享对象。其他应用程序可能需要更多共享对象,这使得 ldd 的输出更加复杂。我们将以 perl 作为例子。 输入:
1
ldd /usr/bin/perl




将得到:
1
2
3
4
5
6
7
8
9
10
11
12
libnsl.so.1 => /lib/libnsl.so.1
          (0x40021000)
        <br>
          libdl.so.2 => /lib/libdl.so.2 (0x40039000)
        <br>
          libm.so.6 => /lib/libm.so.6 (0x4003d000)
        <br>
          libc.so.6 => /lib/libc.so.6 (0x40064000)
        <br>
          libcrypt.so.1 => /lib/libcrypt.so.1 (0x4018f000)
        <br>
          /lib/ld.so.1 => /lib/ld.so.1 (0x40000000)




所需要的一切都在那里了,但是我发现对于这个进程,下面的内容读起来更快一点:
1
ldd /usr/bin/perl | awk '{print? $4 "" $3 }' | sort




1
2
3
4
5
6
7
8
9
10
11
(0x40000000) /lib/ld.so.1
        <br>
          (0x40021000) /lib/libnsl.so.1
        <br>
          (0x40039000) /lib/libdl.so.2
        <br>
          (0x4003d000) /lib/libm.so.6
        <br>
          (0x40064000) /lib/libc.so.6
        <br>
          (0x4018f000) /lib/libcrypt.so.1




现在我们来确定崩溃发生在 libc 中的何处。假设 libc.so.6 的加载地址是 0x40021000,从指令地址 0x400ab738减去它,结果为 0x8a738。这是进入 libc.so.6 的偏移。使用 nm 命令,从 libc.so.6 转储符号,然后尝试确定该地址位于哪个函数中。对于 libc.so.6,nm 将生成 7,000 多行输出。通过对计算得出的偏移部分执行 grep(正则表达式查找程序)可以削减必须检查的数据量。输入:
1
nm /lib/libc.so.6 | sort | grep 0008a




将返回 66 行,在该输出的中间,我们会发现:
1
2
3
0008a6fc T memcpy
        <br>
          0008a754 t _wordcopy_fwd_aligned




该偏移落在 memcpy 中的某个位置。在此例中,一个空指针被当作目标地址传递给了 memcpy。我们在何处调用的 memcpy 呢?问得好。我们可以通过检查输出在日志文件中的寄存器转储来确定目标区域。寄存器 14 包含执行某个函数调用时的返回地址。根据 ,R14 是 0x8040066e,它在截去高位之后产生一个地址 0x40066e。这个地址落在我们的程序范围之内,因此可以运行addr2line 来确定该地址在何处。输入:      
1
addr2line -e simple 0x40066e




将返回:
1
/home/devuser/simple.c:36




这是我们调用 memcpy 之后的那一行。关于 addr2line 的一点补充:如果可执行文件中没有包括调试符号,您将获得 --:0 作为响应。
返回列表