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

将信号用作 Linux 调试工具

2007-05-20 23:24 316 查看
通过重点分析使用信号处理程序捕获到的数据,您可以加速调试过程中耗时最多的一个步骤:寻找 bug。本文介绍了 Linux® 信号的背景知识,并给出了已在 PPC Linux 测试通过的示例,然后介绍如何设计自己的信号处理程序来输出信息,从而快速定位代码中有问题的部分。
信号 就是软件中断,可以向正在执行的程序(进程)发送有关异步事件发生的信息。大部分硬件 trap(非法指令、对无效地址的访问等等)都可以转换成信号。信号可以由进程本身生成,也可以从一个进程发送到其他进程中。系统中可以产生并发送多种类型的信号,它们对于程序员来说有很多用处。(要在 Linux® 环境中查看完整的信号清单,可使用
kill -l
命令。)尽管本文中介绍的基本原理都是通用的,不过所给出的示例程序是使用 gcc v3.3.3 版及 SUSE Linux Enterprise Server 9(PPC 版)操作系统编译的。将信号用作调试工具在调试程序时,大约有 90% 的时间都要花在寻找问题上。您可以使用信号来缩短寻找问题的时间。信号可以提供很多有关用户空间进程的信息(或者将某些信息提供给用户空间的进程)。您可以将自己的应用程序设计成可以使用信号信息来判断操作过程,从而使应用程序在执行上下文中实现完全控制。信号可以使用
SIG_IGN
忽略,忽略的信号不会发送给进程。清单 1 显示了如何忽略一个
SIGINT
信号。(由于这个进程忽略了
SIGINT
信号,因此您需要使用 Crtl-Z 来终止这个进程,或者使用 Crlt-/ 来退出这个进程。)清单 1. 忽略 SIGINT 信号的示例程序

当一个信号被发送给某个进程时,可能会发生两类操作:默认操作,其中内核会对信号进行处理,并根据信号的不同执行适当的操作。每个信号在内核中都有自己的信号处理程序;信号处理程序的默认行为是终止进程。执行用户定义的操作,此时这个信号由一个用户定义的信号处理程序来处理。下面让我们来重点介绍一下用户空间的信号处理程序。

回页首
用户空间的信号处理程序信号处理程序 (signal handler) 就是在接收到信号时所执行的代码。它是用户空间的程序代码的一部分,需要在用户空间的上下文中执行。信号处理程序中提供了有关在信号发生时要执行的操作的信息。信号处理程序可以编写为忽略这个信号。用户进程不允许为所有信号安装处理程序;例如,不允许为
SIGKILL
SIGSTOP
安装处理程序。如果进程失去了控制,有些地方(至少是内核)需要能够终止这个进程。如果操作系统允许进程为这两个信号注册处理程序,并且这两个处理程序设计为忽略信号,那么除了进行硬件重启之外,就没有任何办法可以终止这个进程了。清单 2 给出了一种注册信号处理程序的方法:清单 2. 注册信号处理程序

sigaction
系统调用需要使用 3 个参数:信号编号指向新
sigaction
结构体的指针指向旧
sigaction
结构体的指针
sigaction
结构体的定义如清单 3 所示:清单 3. sigaction 结构体

其中
sa_flags
设置为
SA_SIGINFO
,信号处理函数应设置为
sa_sigaction
SA_SIGINFO
使用下面 3 个参数来调用信号处理程序:信号编号信号信息硬件上下文的快照
mysig_handler
是在接收到信号时要调用的处理函数。
mysig_act
是一个
sigaction
结构体,其中包含了所有的信息。在 UNIX® 中,每个信号都有自己惟一的信号编号。如前所述,
kill -l
可以列出所有信号及其对应信号编号。第二个参数是信号信息结构体。该结构体名为
siginfo_t
。这个结构体是由内核根据所生成的信号来填充的。结构体可用于获取发送者的 pid、uid、错误地址以及其他信息。其中还提供了一个错误代码和一个
si
代码。包含此结构体定义的头文件是 bits/siginfo.h。第三个参数是
ucontext
结构体。此结构体(也就是 User Context Structure 的简写)有一些指向其他结构体 —— 例如
mcontext_t
sigset_t
等 —— 的指针。
mcontext_t
提供了有关在系统出问题时可以找到的所有寄存器值的数据;这些寄存器值可以作为信号发送给这个进程。内核为系统中所有的进程都维护了一个 context 结构体,以及要在不同进程之间有效进行上下文切换所需要的信息。内核只是在
pt_regs
mcontext_t
结构体中为用户程序提供了有限的信息。这些结构体几乎包含了所有寄存器的数据:通用寄存器 (GPR)、浮点寄存器 (FPR)、VMX 寄存器(如果存在)和专用寄存器 (SPR)。但切记,
pt_regs
是一个面向特定体系结构的结构体。包含这一信息的头文件是 sys/ucontext.h 和 asm/ptrace.h。清单 4. pt_regs 结构体定义 <asm-ppc64/ptrace.h>

在调试信号时,需要查看的一些重要寄存器包括 GPR、指令指针 (NIP)、机器状态寄存器 (MSR)、Trap、数据地址寄存器 (DAR) 等等。不过并非所有的寄存器都是与所有的信号有关的。在
SIGILL
的情况中,DAR 可能不会提供任何有用的数据,因为这个寄存器在
SIGSEGV
的情况中就被用来存放故障地址。现在您已经了解了有关信号的背景知识,接下来让我们看一下如何使用信号。下面这个示例程序使用了
SIGTERM
信号。清单 5. 处理 SIGTERM 的程序

上面这个示例程序为
SIGTERM
注册一个信号处理程序,在处理程序的代码中,它打印了发送者进程的 pid 和 uid,并直接忽略这个信号,然后继续执行。下面是这个程序的输出结果:清单 6. 清单 5 程序的输出结果

这一信号处理数据在某些情况中非常重要。使用这些数据,进程如果在运行过程中接收到一个
SIGTERM
信号,就可以在执行完关键代码(如果已经启动)之后自行终止。这可以通过在信号处理程序代码中设置一个全局标志并在完成关键部分的代码之后检查这个标志来实现。您也可以将发送者的 pid 保存下来,并将其打印到一个输出文件中,从而了解是哪些进程发送的信号。下面让我们来看一个更重要的例子。考虑一下
SIGILL
信号。
SIGILL
是为那些执行非法指令的情况而产生的。它是在特定条件下产生的。例如非法的操作码、非法操作数、特权操作码等等。清单 7 所示程序就试图执行一个特权操作:清单 7. 处理 SIGILL 的程序

有些指令不允许在用户空间中执行,例如试图访问 MSR 和 SRR0/SRR1(保存恢复寄存器)的指令。要执行这些指令,您必须切换到内核上下文。清单 7 中的程序会试图执行一条将一个值从 MSR 移动到 GPR 的指令。读取 MSR 就是特权操作,因此就会产生一个
SIGILL
信号。输出结果如清单 8 所示:清单 8. 清单 7 的输出结果

正如我们期望的一样,这个程序会接收到一个
SIGILL
(信号编号为 4)信号,其
si
代码为 5,这是在用户空间的程序执行特权操作时产生的。正如清单 8 所示,这个程序输出了 6 条连续的指令,包括出错的那条指令。要查看代码中是哪条指令出错了,可以使用
objdump
命令输出可执行文件的代码,该命令会列出编译器所生成的指令。(从
objdump
的帮助页中可获得关于此工具的更多信息。)清单 9. objdump 命令

/tmp/mdmp 文件中保存了可执行文件 mysigill 执行 objdump 之后的结果。首先,查找出错的操作码/指令。在本例中,出错的操作码是 7c6000a6。清单 10. 对象 dump 文件

如果这个程序中一条操作码出现了多次,请尝试在 dump 文件中寻找处理程序代码所打印的序列。这让您可以将程序中导致执行或生成这条指令的函数隔离开来。当使用
-g
选项来编译源代码时,dump 文件通常会包含有一行行的源代码以及对应的实现指令。下面让我们来看一种程序员经常会遇到的、由信号引起的错误的调试方法。
SIGSEGV
信号是在特定的条件下生成的,例如当进程试图在一个尚未分配的内存区域中加载或保存数据时、或程序试图对只读内存进行写操作时都会产生这个信号。清单 11 所示程序是一个段错误的典型例子。清单 11. 处理 SIGSEGV 的程序

这个程序试图在一个尚未分配的内存中保存数据:它执行一个字符串复制操作,将
arr
中的数据复制到
p
变量中。这样做的结果是产生一个
SEGSEGV
信号,如清单 12 所示:清单 12. 清单 11 的输出结果

这个示例程序还输出了当时通用寄存器的值。调试这个问题的一种方法是对这个可执行程序执行 objdump 命令,并将其结果保存到一个文件中;然后查找出错的指令(在本例中,出错的操作码是 98080000)。清单 13. 对象 dump 文件

由于这个程序是使用
-g
选项编译的,因此对象 dump 文件中就包含了源代码。此处出错的指令是
stb
。这个进程试图将一个字节从寄存器
r0
保存到一个寄存器
r8
所指向的内存地址中,但是寄存器
r8
的值为 0x0 —— 这可以从处理程序代码所输出的
gpr
的值中看出来,这就是产生信号的根源。 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息