將信號用作 Linux 調試工具

通 過重點分析使用信號處理程序捕獲到的數據,您可以加速調試過程中耗時最多的一個步驟:尋找 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 信號的示例程序


#include <stdio.h>
#include <signal.h>

main()
{
signal(SIGINT,SIG_IGN);
while(1)
printf("You can't kill me with SIGINT anymore, dude/n");
return 0;
}


當一個信號被髮送給某個進程時,可能會發生兩類操作:

默認操作,其中內核會對信號進行處理,並根據信號的不同執行適當的操作。每個信號在內核中都有自己的信號處理程序;信號處理程序的默認行爲是終止進程。 執行用戶定義的操作,此時這個信號由一個用戶定義的信號處理程序來處理。

下面讓我們來重點介紹一下用戶空間的信號處理程序。



回頁首


用戶空間的信號處理程序

信號處理程序 (signal handler) 就是在接收到信號時所執行的代碼。它是用戶空間的程序代碼的一部分,需要在用戶空間的上下文中執行。信號處理程序中提供了有關在信號發生時要執行的操作的信息。信號處理程序可以編寫爲忽略這個信號。

用戶進程不允許爲所有信號安裝處理程序;例如,不允許爲 SIGKILLSIGSTOP 安裝處理程序。如果進程失去了控制,有些地方(至少是內核)需要能夠終止這個進程。如果操作系統允許進程爲這兩個信號註冊處理程序,並且這兩個處理程序設計爲忽略信號,那麼除了進行硬件重啓之外,就沒有任何辦法可以終止這個進程了。

清單 2 給出了一種註冊信號處理程序的方法:


清單 2. 註冊信號處理程序


struct sigaction mysig_act;
mysig_act.sa_flags = SA_SIGINFO;
mysig_act.sa_sigaction = (void *)mysig_handler;
if(sigaction (<signal number>,&mysig_act,(struct sigaction *)NULL)) {
printf("Sigaction returned error = %d/n", errno);
exit(0);
}

sigaction 系統調用需要使用 3 個參數:

信號編號 指向新 sigaction 結構體的指針 指向舊 sigaction 結構體的指針

sigaction 結構體的定義如清單 3 所示:


清單 3. sigaction 結構體


struct sigaction {
void (*sa_handler)(int); /* func pointer */
void (*sa_sigaction)(int, siginfo_t *, void *); /*func pointer */
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}

其中 sa_flags 設置爲 SA_SIGINFO ,信號處理函數應設置爲 sa_sigactionSA_SIGINFO 使用下面 3 個參數來調用信號處理程序:

信號編號 信號信息 硬件上下文的快照

mysig_handler 是在接收到信號時要調用的處理函數。mysig_act 是一個 sigaction 結構體,其中包含了所有的信息。

在 UNIX® 中,每個信號都有自己惟一的信號編號。如前所述,kill -l 可以列出所有信號及其對應信號編號。

第二個參數是信號信息結構體 。該結構體名爲 siginfo_t 。這個結構體是由內核根據所生成的信號來填充的。結構體可用於獲取發送者的 pid、uid、錯誤地址以及其他信息。其中還提供了一個錯誤代碼和一個 si 代碼。包含此結構體定義的頭文件是 bits/siginfo.h。

第三個參數是 ucontext 結構體。此結構體(也就是 User Context Structure 的簡寫)有一些指向其他結構體 —— 例如 mcontext_tsigset_t 等 —— 的指針。mcontext_t 提供了有關在系統出問題時可以找到的所有寄存器值的數據;這些寄存器值可以作爲信號發送給這個進程。內核爲系統中所有的進程都維護了一個 context 結構體,以及要在不同進程之間有效進行上下文切換所需要的信息。

內核只是在 pt_regsmcontext_t 結構體中爲用戶程序提供了有限的信息。這些結構體幾乎包含了所有寄存器的數據:通用寄存器 (GPR)、浮點寄存器 (FPR)、VMX 寄存器(如果存在)和專用寄存器 (SPR)。

但切記,pt_regs 是一個面向特定體系結構的結構體。包含這一信息的頭文件是 sys/ucontext.h 和 asm/ptrace.h。


清單 4. pt_regs 結構體定義 <asm-ppc64/ptrace.h>


#define PPC_REG unsigned long
struct pt_regs {
PPC_REG gpr[32];
PPC_REG nip;
PPC_REG msr;
PPC_REG orig_gpr3; /* Used for restarting system calls */
PPC_REG ctr;
PPC_REG link;
PPC_REG xer;
PPC_REG ccr;
PPC_REG softe; /* Soft enabled/disabled */
PPC_REG trap; /* Reason for being here */
PPC_REG dar; /* Fault registers */
PPC_REG dsisr;
PPC_REG result; /* Result of a system call */
};

在調試信號時,需要查看的一些重要寄存器包括 GPR、指令指針 (NIP)、機器狀態寄存器 (MSR)、Trap、數據地址寄存器 (DAR) 等等。不過並非所有的寄存器都是與所有的信號有關的。在 SIGILL 的情況中,DAR 可能不會提供任何有用的數據,因爲這個寄存器在 SIGSEGV 的情況中就被用來存放故障地址。

現在您已經瞭解了有關信號的背景知識,接下來讓我們看一下如何使用信號。下面這個示例程序使用了 SIGTERM 信號。


清單 5. 處理 SIGTERM 的程序


#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <ucontext.h>

static void myhandler (unsigned int sn , siginfo_t si , struct ucontext *sc)
{
unsigned int mnip;
int i;
printf(" signal number = %d, signal errno = %d, signal code = %d/n",
si.si_signo,si.si_errno,si.si_code);
printf(" senders' pid = %x, sender's uid = %d, /n",si.si_pid,si.si_uid);
}

main()
{
struct sigaction s;
s.sa_flags = SA_SIGINFO;
s.sa_sigaction = (void *)myhandler;
if(sigaction (SIGTERM,&s,(struct sigaction *)NULL)) {
printf("Sigaction returned error = %d/n", errno);
exit(0);
}
while(1);
return 0;
}

上面這個示例程序爲 SIGTERM 註冊一個信號處理程序,在處理程序的代碼中,它打印了發送者進程的 pid 和 uid,並直接忽略這個信號,然後繼續執行。下面是這個程序的輸出結果:


清單 6. 清單 5 程序的輸出結果


> ./fin &
[2] 7375
> ps -ef | grep 7375
maddy 7375 7063 90 16:51 pts/0 00:00:24 ./fin
maddy 7377 7063 0 16:52 pts/0 00:00:00 grep 7375
> kill 7375
signal number = 15, signal errno = 0, signal code = 0
senders' pid = 7063, sender's uid = 1001,
> kill -9 7375
> ps -ef | grep 7375
maddy 7379 7063 0 16:52 pts/0 00:00:00 grep 7375
[2]+ Killed ./fin

這一信號處理數據在某些情況中非常重要。使用這些數據,進程如果在運行過程中接收到一個 SIGTERM 信號,就可以在執行完關鍵代碼(如果已經啓動)之後自行終止。這可以通過在信號處理程序代碼中設置一個全局標誌並在完成關鍵部分的代碼之後檢查這個標誌來實現。您也可以將發送者的 pid 保存下來,並將其打印到一個輸出文件中,從而瞭解是哪些進程發送的信號。

下面讓我們來看一個更重要的例子。考慮一下 SIGILL 信號。SIGILL 是爲那些執行非法指令的情況而產生的。它是在特定條件下產生的。例如非法的操作碼、非法操作數、特權操作碼等等。

清單 7 所示程序就試圖執行一個特權操作:


清單 7. 處理 SIGILL 的程序


#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <ucontext.h>

static void myhandler (unsigned int sn , siginfo_t si ,/
struct ucontext *sc)
{
unsigned int mnip;
int i,j;

printf(" Signal number = %d, Signal errno = %d/n"
,si.si_signo,si.si_errno);
switch(si.si_code)
{
case 1: printf(" SI code = %d (Illegal opcode)/n",si.si_code);
break;
case 2: printf(" SI code = %d (Illegal operand)/n",si.si_code);
break;
case 3: printf(" SI code = %d (Illegal addressing mode)/n",
si.si_code);
break;
case 4: printf(" SI code = %d (Illegal trap)/n",si.si_code);
break;
case 5: printf(" SI code = %d (Privileged opcode)/n",si.si_code);
break;
case 6: printf(" SI code = %d (Privileged register)/n",si.si_code);
break;
case 7: printf(" SI code = %d (Coprocessor error)/n",si.si_code);
break;
case 8: printf(" SI code = %d (Internal stack error)/n",si.si_code);
break;
default: printf("SI code = %d (Unknown SI Code)/n",si.si_code);
break;
}

printf(" Machine State Register = %x /n",
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->msr));
printf(" Link register pointing to location = 0x%x, /
Opcode at the location = 0x%x /n",
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->link),
*(unsigned int *) /
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->link));
for(i=20,j=5;i>0;i-=4,j--)
printf(" Op-Code [nip - %d] = 0x%x at address = 0x%x /n"
,j,*(unsigned int *)(si.si_addr - i)
,(si.si_addr - i) );
printf(" Failed Op-code = 0x%x at address = 0x%x /n",
*(unsigned int*)(si.si_addr), (si.si_addr));
printf(" Op-Code [nip + 1] = 0x%x at address = 0x%x /n",
*(unsigned int *)(si.si_addr + 4), (si.si_addr + 4));
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip) += 4;
}

my()
{
__asm__ volatile ("add 4,5,6 /n/t":);
__asm__ volatile ("add 7,8,9 /n/t":);
__asm__ volatile ("mfmsr 3 /n/t":);
__asm__ volatile ("add 4,5,6 /n/t":);
__asm__ volatile ("add 7,8,9 /n/t":);
}

main()
{
struct sigaction s;

s.sa_flags = SA_SIGINFO;
s.sa_sigaction = (void *)myhandler;
if(sigaction (SIGILL,&s,(struct sigaction *)NULL)) {
printf("Sigaction returned error = %d/n", errno);
exit(0);
}
my();
return 0;
}

有些指令不允許在用戶空間中執行,例如試圖訪問 MSR 和 SRR0/SRR1(保存恢復寄存器)的指令。要執行這些指令,您必須切換到內核上下文。

清單 7 中的程序會試圖執行一條將一個值從 MSR 移動到 GPR 的指令。讀取 MSR 就是特權操作,因此就會產生一個 SIGILL 信號。輸出結果如清單 8 所示:


清單 8. 清單 7 的輸出結果


> ./mysigill
Signal number = 4, Signal errno = 0
SI code = 5 (Privileged opcode)
Machine State Register = 4d032
Link register pointing to location = 0x10000830, Opcode at the location = 0x38000000
Op-Code [nip - 5] = 0x9421ffe0 at address = 0x10000788
Op-Code [nip - 4] = 0x93e1001c at address = 0x1000078c
Op-Code [nip - 3] = 0x7c3f0b78 at address = 0x10000790
Op-Code [nip - 2] = 0x7c853214 at address = 0x10000794
Op-Code [nip - 1] = 0x7ce84a14 at address = 0x10000798
Failed Op-code = 0x7c6000a6 at address = 0x1000079c
Op-Code [nip + 1] = 0x7c853214 at address = 0x100007a0

正如我們期望的一樣,這個程序會接收到一個 SIGILL (信號編號爲 4)信號,其 si 代碼爲 5,這是在用戶空間的程序執行特權操作時產生的。

正如清單 8 所示,這個程序輸出了 6 條連續的指令,包括出錯的那條指令。要查看代碼中是哪條指令出錯了,可以使用 objdump 命令輸出可執行文件的代碼,該命令會列出編譯器所生成的指令。(從 objdump 的幫助頁中可獲得關於此工具的更多信息。)


清單 9. objdump 命令


> objdump -S mysigill >> /tmp/mdmp

/tmp/mdmp 文件中保存了可執行文件 mysigill 執行 objdump 之後的結果。首先,查找出錯的操作碼/指令。在本例中,出錯的操作碼是 7c6000a6。


清單 10. 對象 dump 文件


<Search the output for the opcode "7c6000a6">

10000788 <my>:
10000788: 94 21 ff e0 stwu r1,-32(r1)
1000078c: 93 e1 00 1c stw r31,28(r1)
10000790: 7c 3f 0b 78 mr r31,r1
10000794: 7c 85 32 14 add r4,r5,r6
10000798: 7c e8 4a 14 add r7,r8,r9
1000079c: 7c 60 00 a6 mfmsr r3 <== Bingo!!!
100007a0: 7c 85 32 14 add r4,r5,r6
100007a4: 7c e8 4a 14 add r7,r8,r9
100007a8: 7c 03 03 78 mr r3,r0
100007ac: 81 61 00 00 lwz r11,0(r1)
100007b0: 83 eb ff fc lwz r31,-4(r11)
100007b4: 7d 61 5b 78 mr r1,r11
100007b8: 4e 80 00 20 blr

如果這個程序中一條操作碼出現了多次,請嘗試在 dump 文件中尋找處理程序代碼所打印的序列。這讓您可以將程序中導致執行或生成這條指令的函數隔離開來。當使用 -g 選項來編譯源代碼時,dump 文件通常會包含有一行行的源代碼以及對應的實現指令。

下面讓我們來看一種程序員經常會遇到的、由信號引起的錯誤的調試方法。SIGSEGV 信號是在特定的條件下生成的,例如當進程試圖在一個尚未分配的內存區域中加載或保存數據時、或程序試圖對只讀內存進行寫操作時都會產生這個信號。清單 11 所示程序是一個段錯誤的典型例子。


清單 11. 處理 SIGSEGV 的程序


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <ucontext.h>

static void seghandler (unsigned int sn , siginfo_t si , /
struct ucontext *sc)
{
unsigned int mnip;
int i;

mnip=*(unsigned int *)(((struct pt_regs *) /
((&(sc->uc_mcontext))->regs))->nip);
printf(" Signal number = %d, Signal errno = %d/n",
si.si_signo,si.si_errno);
switch(si.si_code)
{
case 1: printf(" SI code = %d (Address not mapped to object)/n",
si.si_code);
break;
case 2: printf(" SI code = %d (Invalid permissions for /
mapped object)/n",si.si_code);
break;
default: printf("SI code = %d (Unknown SI Code)/n",si.si_code);
break;
}
printf(" Intruction pointer = %x /n",mnip);
printf(" Fault addr = 0x%x /n",si.si_addr);
printf(" dar = 0x%x /n",
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->dar));
printf(" trap = 0x%x /n",
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->trap));
printf(" Op-Code [nip - 4] = 0x%x at address = 0x%x /n",
*(unsigned int *)/
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip-4),
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip-4) );
printf(" Failed Op-code = 0x%x at address = 0x%x /n",
*(unsigned int *)/
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip),
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip));
printf(" Op-Code [nip + 1] = 0x%x at address = 0x%x /n",
*(unsigned int *) /
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip+4),
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip + 4));
printf("***GPR values are the time of fault*** /n");
for (i=0;i<11;i++)
printf(" Gpr[%d] = 0x%x /n",i, /
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->gpr[i]));
(((struct pt_regs *)((&(sc->uc_mcontext))->regs))->nip)+=4;
}

main()
{
struct sigaction m;
char *p,*q, arr[]="Ma";
q=arr;

m.sa_flags = SA_SIGINFO;
m.sa_sigaction = (void *)seghandler;
sigaction (SIGSEGV,&m,(struct sigaction *)NULL);
*p++ = *q++;
return 0;
}

這個程序試圖在一個尚未分配的內存中保存數據:它執行一個字符串複製操作,將 arr 中的數據複製到 p 變量中。這樣做的結果是產生一個 SEGSEGV 信號,如清單 12 所示:


清單 12. 清單 11 的輸出結果


> ./sigsegv
Signal number = 11, Signal errno = 0
SI code = 1 (Address not mapped to object)
Intruction pointer = 98080000
Fault addr = 0x0
dar = 0x0
trap = 0x300
Op-Code [nip - 4] = 0x88090000 at address = 0x10000760
Failed Op-code = 0x98080000 at address = 0x10000764
Op-Code [nip + 1] = 0x396b0001 at address = 0x10000768
***GPR values are the time of fault***
Gpr[0] = 0x4d
Gpr[1] = 0xffffe070
Gpr[2] = 0x4001ee20
Gpr[3] = 0x0
Gpr[4] = 0xffffdf30
Gpr[5] = 0x0
Gpr[6] = 0xffffe110
Gpr[7] = 0xffffe114
Gpr[8] = 0x0
Gpr[9] = 0xffffe120
Gpr[10] = 0x0

這個示例程序還輸出了當時通用寄存器的值。調試這個問題的一種方法是對這個可執行程序執行 objdump 命令,並將其結果保存到一個文件中;然後查找出錯的指令(在本例中,出錯的操作碼是 98080000)。


清單 13. 對象 dump 文件


<Search output for the opcode "98080000">

10000744: 48 01 07 21 bl 10010e64 <__bss_start+0x48>
*p++ = *q++;
10000748: 38 df 00 a0 addi r6,r31,160
1000074c: 81 46 00 00 lwz r10,0(r6)
10000750: 38 ff 00 a4 addi r7,r31,164
10000754: 81 67 00 00 lwz r11,0(r7)
10000758: 7d 48 53 78 mr r8,r10
1000075c: 7d 69 5b 78 mr r9,r11
10000760: 88 09 00 00 lbz r0,0(r9)
10000764: 98 08 00 00 stb r0,0(r8) <==Failed instruction
10000768: 39 6b 00 01 addi r11,r11,1
1000076c: 91 67 00 00 stw r11,0(r7)
10000770: 39 4a 00 01 addi r10,r10,1
10000774: 91 46 00 00 stw r10,0(r6)
return 0;
10000778: 38 00 00 00 li r0,0
}

由於這個程序是使用 -g 選項編譯的,因此對象 dump 文件中就包含了源代碼。此處出錯的指令是 stb 。這個進程試圖將一個字節從寄存器 r0 保存到一個寄存器 r8 所指向的內存地址中,但是寄存器 r8 的值爲 0x0 —— 這可以從處理程序代碼所輸出的 gpr 的值中看出來,這就是產生信號的根源。





參考資料

學習

您可以參閱本文在 developerWorks 全球站點上的 英文原文
"掌握 Linux 調試技術 " (developerWorks,2002 年 8 月)展示了在 Linux 上運行的程序的調試方法。
"Linux 內核調試器內幕 " (developerWorks,2003 年 6 月)介紹瞭如何安裝、設置及使用 KDB 所提供的特性。
"安全編程: 避免競爭條件 " (developerWorks,2004 年 8 月)簡要介紹了與資源競爭有關的信號處理。
"使用 GDB 調試 Linux 軟件 " (developerWorks,2001 年 2 月)對 GDB 進行了介紹,使用 GDB 可以查看程序內部結構、打印變量值、設置斷點和跟蹤源代碼。
"使用可重入函數進行更安全的信號處理 " (developerWorks,2005 年 1 月)是有關信號結構的一份優秀指南。
有關 Linux 2.6 內核與 glibc 2.4 上的 crash 信號及其含義 的詳細列表,請參閱 Spike Developer 專區。
學習如何 調試生成信號的程序
在這篇 Linux Journal 中可以瞭解有關 Linux 信號處理模型 的詳細知識。
General Programming Concepts: Writing and Debugging Programs (來自 IBM AIX Documentation library)中,可以學習有關 信號管理 的更多知識。
請閱讀 developerWorks 上有關 Linux 調試的更多文章 .
developerWorks Linux 專區 中可以找到爲 Linux 開發人員準備的更多資源。
密切關注 developerWorks 技術動態和事件


獲得產品和技術

下載 KGDB ,這是 Linux 內核的一個源代碼級的調試器,可以與 GDB 一起使用對內核進行調試。
定購免費的 SEK for Linux ,共有兩張 DVD,包括最新的 IBM for Linux 的試用版軟件以及 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。
在您的下一個開發項目中使用 IBM 試用版軟件 ,可以從 developerWorks 上直接下載。


討論
通過參與 developerWorks blogs 來參與 developerWorks 社區。

發佈了31 篇原創文章 · 獲贊 0 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章