 | レベル: 中級 Madhavan Srinivasan (masriniv@in.ibm.com), Developer, PowerPC Tools Development, IBM
2005年 11月 29日 シグナル・ハンドラーを使用してキャプチャーしたデータの解析に集中することで、デバッグで最も時間のかかる部分、すなわちバグの発見をスピードアップすることができます。この記事では、特にPPC Linuxでテストした例を交えて、Linux®シグナルの背景を解説します。さらに、ハンドラーの設計によって、エラーのあるコード部分をすばやく特定できる情報を出力する方法も解説します。
シグナルは、非同期イベントの発生に関する情報を実行中のプログラムまたはプロセスに送信するソフトウェア割り込みです。シグナルは、タイマー切れなど、さまざまな理由で生成されます。ほとんどのハードウェア・トラップ(違反な命令、無効なアドレスへのアクセスなど)は、シグナルに変換されます。
シグナルは、プロセスそのものによって生成されることもあり、プロセスから別のプロセスに送信されることもあります。多種多様なシグナルを生成または配信することができ、プログラマーにとって多くの用途があります。(Linux®環境での全てのシグナルの一覧を確認するには、コマンド kill ?lを使用してください。)
この記事で述べる原則は一般的なものであり、サンプル・プログラムはgccバージョン3.3.3とSUSE Linux Enterprise Server 9(PPC版)オペレーティング・システムでコンパイルされたものです。
デバッギング・ツールとしてのシグナル
デバッグするとき、おそらく90%の時間は、単に問題を見つけるためだけに費やされます。シグナルを使用すると、この時間を減らすことができます。シグナルは、ユーザー空間プロセスに(またはユーザー空間プロセスに関する)多くの情報を提供します。シグナル情報からアクションの進路を決定するようにアプリケーションを設計すれば、アプリケーションの実行コンテキストを細かに制御することができます。
SIG_IGNによってシグナルを無視することもできます。無視されたシグナルはプロセスに配信されません。リスト1に、SIGINTを無視する方法を示します。(プロセスはSIGINTを無視するので、プロセスを停止するにはCtrl-Zを、プロセスを終了するにはCtrl-\を使用する必要があります。)
リスト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;
}
|
シグナルがプロセスに配信されたときのアクションには2種類があります。
- デフォルトのアクションでは、カーネルがシグナルを処理して、シグナルに応じてアクションをとります。カーネルには、各シグナルについて独自のシグナル処理ルーチンがあります。ルーチンのデフォルトの動作は、プロセスをkillすることです。
- ユーザー定義のアクションでは、シグナルはユーザー定義のシグナル・ハンドラーによって処理されます。
ユーザー空間のシグナル・ハンドラーについて詳しく見てみましょう。
ユーザー空間のシグナル・ハンドラー
シグナル・ハンドラーは、シグナル配信時に実行されるコードです。ユーザー空間のプログラム・コードの一部であり、ユーザー空間コンテキストで実行されます。シグナル・ハンドラーは、シグナル発生時にとるべきアクションについての情報を提供します。シグナルを無視するようにシグナル・ハンドラーを作成することもできます。
ユーザー・プロセスでは、すべてのシグナルについてハンドラーをインストールできるわけではありません。たとえば、SIGKILLやSIGSTOPのハンドラーをインストールすることはできません。プロセスが制御不能になった場合、誰かが(少なくともカーネルが)プロセスをkillしたり停止したりできなければなりません。これらのシグナルのハンドラーを登録するプロセスをオペレーティング・システムが許可している場合、また、ハンドラーがこれらを無視するように設計されていた場合、事実上、プロセスを停止するにはハード・リブートしか手段がありません。
リスト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.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_sigactionに設定されなければなりません。SA_SIGINFOは、次の3つのパラメーターでシグナル・ハンドラーを呼び出します。
- シグナル番号
- シグナル情報
- ハードウェア・コンテキストのスナップショット
mysig_handlerは、シグナル配信時に呼び出されるハンドラー関数です。mysig_actは、この情報のすべてを含んでいるsigaction構造体です。
UNIX®では、すべてのシグナルに一意なシグナル番号が付いています。すでに述べたように、kill ?lを実行すると、すべてのシグナルとシグナル番号の一覧が表示されます。
2番目のパラメーターは、シグナル情報(signal information)構造体です。構造体の名前は、siginfo_tです。この構造体には、生成されたシグナルに基づいて、カーネルによって値が入れられます。この構造体を使用して、送信側のpid、uid、フォールト・アドレスなどの情報を取得することができます。この構造体は、エラー・コードとsiコードも提供します。構造体の定義を含んでいるヘッダー・ファイルは、bits/siginfo.hです。
3番目のパラメーターは、ucontext構造体です。この構造体(User Context Structureの省略形)は、mcontext_t、sigset_tなど、他のさまざまな構造体のポインターを含みます。mcontext_tは、フォールトの時点でシステム内で見つかったすべてのレジスター値に関するデータを提供します。レジスター値は、シグナルとして順番にプロセスに配信されます。カーネルはシステム内のすべてのプロセスのコンテキスト構造体を保持します。これらは、カーネルが異なるプロセス間で効果的にコンテキストを切り替えるために必要な情報です。
カーネルは、pt_regsおよびmcontext_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)、トラップ、データ・アドレス・レジスター(DAR)などです。しかし、レジスターのすべてがすべてのシグナルに関連しているわけではありません。たとえば、DARはSIGSEGVの場合のフォールト・アドレスを格納するためのものなので、SIGILLの場合、このレジスターは有効なデータを提供しません。
では、シグナルの背景がいくらかわかってきたところで、シグナルの使い方を見てみましょう。次のサンプル・プログラムでは、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は、違反な命令の実行によって生成されます。違反なopcode、違反なオペランド、特権付きopcodeなど、さまざまな条件で生成されます。
このプログラムは、特権付きopcodeの実行を試みます。
リスト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(Save Restore Register)へのアクセスを試みるものなど、ユーザー空間からは実行が許されない命令があります。これらの命令を実行するには、カーネル・コンテキストに切り替える必要があります。
リスト7のプログラムは、MSRの値をGPRに移動する命令の実行を試みます。MSRの読み取りは特権を必要とする操作なので、予想される結果はSIGILLになります。出力は、次のようになります。
リスト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を受け取りました。このコードは、特権付きopcodeがユーザー空間プログラムによって実行されるときにセットされます。
リスト8に示されているように、プログラムは、失敗した命令も含めて6つの連続した命令をダンプしています。コードの中から失敗した命令を見つけるには、objdumpを使用して、実行ファイルのオブジェクト・ダンプを行います。objdumpは、コンパイラーによって生成された命令を一覧表示します。(このツールの詳細については、objdumpのmanpageを参照してください。)
リスト9.objdumpコマンド
> objdump -S mysigill >> /tmp/mdmp
|
/tmp/mdmpファイルに、実行ファイルmysigillのオブジェクト・ダンプが格納されます。まず、失敗したopcodeや命令を検索します。この場合、失敗したopcodeは7c6000a6です。
リスト10.オブジェクト・ダンプ・ファイル
<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
|
このopcodeがプログラム内に複数回現れる場合は、ハンドラー・コードがダンプ・ファイル内に出力した順序を見てください。このようにすると、プログラム内で命令の実行または生成の原因となった関数を特定することができます。?g オプションを指定してソースをコンパイルすると、ダンプ・ファイルに行ごとのソースとそれに対応する実行の命令が格納されます。
今度は、プログラマーがよく目にする、シグナルによって生成されたエラーをデバッグする方法を調べてみましょう。SIGSEGVシグナルは、プロセスが割り当てられていないメモリー領域の読み込みまたは保存を試みたときや、プログラムが読み取り専用メモリーへの書き込みを試みたときなど、さまざまな条件で生成されます。次のサンプル・プログラムは、セグメンテーション・フォールトの典型的な例です。
リスト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.リスト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 |
この場合、このサンプル・プログラムは、汎用レジスターの値もダンプします。このエラーをデバッグする1つの方法は、実行ファイルのオブジェクト・ダンプを行って、それをファイルに保存し、失敗した命令を検索する方法です(この例では、失敗したopcodeは98080000です)。
リスト13.オブジェクト・ダンプ・ファイル
<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オプションでコンパイルされているので、オブジェクト・ダンプにはソース・リストが含まれます。この例では、失敗した命令はstbです。プロセスはレジスターr0からレジスター8によって参照されたメモリー位置に1バイトを格納しようとしますが、レジスターr8の値は0x0です。これは、ハンドラー・コードによるgpr値のダンプによって確認でき、これが、このシグナルが生成された原因です。
参考文献 学ぶために
製品や技術を入手するために
-
KGDBをダウンロードしてください。KGDBは、GDBと合わせて使用される、ソース・レベルでのLinuxカーネル用デバッガーです。
-
無料の2枚組DVDセット、SEK for Linuxをご注文ください。DB2®やLotus®、Rational®、Tivoli®、WebSphere® など、Linux用の最新IBMソフトウェア試用版が含まれています。
- 皆さんの次期Linux開発プロジェクトを、IBM trial softwareを使って構築してください。developerWorksから直接ダウンロードすることができます。
議論するために
著者について  | |  | Madhavan Srinivasanは、インドのマドラス大学(Madras University)にて電子電気工学で学位を取得しており、2003年11月から、インドにあるIBM Global Services(Software Labs)で働いています。IBMでのPowerPC ツール開発チームのメンバーとして、LinuxとAIXでのPowerPCサーバー・プロセッサー用の診断ツールの設計、開発に専念しています。彼の関心領域としては他に、PowerPCアーキテクチャーやオペレーティング・システムの内部構成などがあります。 |
記事の評価
|  |