SIGSEGVを捕捉する

はじめに

シグナルを捕捉する手法として,sigaction関数やsignalfd関数がある. sigaction関数では,独自のハンドラを登録することができる.シグナルが発生すると登録したハンドラが実行されるので,発生したシグナルに対して任意の処理を追加することができる.ハンドラでは,発生したシグナルの情報(signalfd_siginfo 構造体)と,シグナルが発生した時のコンテキスト(ucontext_t へのポインタ)を取得できる. 一方,signalfd関数では,poll関数やselect関数で,シグナルディスクリプタを監視できる利点があるが,シグナルが発生した時のコンテキストは取得できず,非同期であるSIGSEGVを捕捉することができない.

シグナルが発生すると,現在のコンテキストはユーザ空間のスタック(デフォルト)に退避され,登録しているハンドラが実行される.例えば,SIGSEGVの場合,デフォルトで登録されているハンドラはcore(5)であり,プロセスの終了とコアダンプ出力となっている.ハンドラが終了すると,退避されていたコンテキストが復帰され,プログラムの実行が再開される.

プログラム

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#define __USE_GNU
#include <signal.h>
#include <ucontext.h>
#include <string.h>

#define STACK_SIZE (4096*8)

static void signal_handler(int sig, siginfo_t* sig_info, void *sig_data)
{
  ucontext_t *ctx = sig_data;
  size_t *gregs = (size_t *)ctx->uc_mcontext.gregs;
  if (sig = SIGSEGV) {
     fprintf(stderr, "si_addr %p\n", sig_info->si_addr);
     fprintf(stderr, "ucontext ptr %p\n", sig_data);
     fprintf(stderr, "REG_RIP %p\n", (void *)gregs[REG_RIP]);
//     exit(1);
  }
}

int main()
{
  stack_t ss;
  ss.ss_sp = malloc(STACK_SIZE);
  ss.ss_size = STACK_SIZE;
  ss.ss_flags = 0;
  if (sigaltstack(&ss, NULL)) {
    fprintf(stderr, "Failed sigaltstack\n");
    exit(1);
  }

  struct sigaction act;
  sigemptyset(&act.sa_mask);
  sigaddset(&act.sa_mask, SIGSEGV);
  act.sa_sigaction = signal_handler;
  act.sa_flags = SA_SIGINFO|SA_RESTART|SA_ONSTACK;
  if ( sigaction( SIGSEGV, &act, NULL ) == -1 ) {
    /* error */
    fprintf(stderr, "Failed to set my signal handler.\n" );
    exit(1);
  }

  void *ptr;
  strcpy(ptr, "AAA");

  return 0;
}

main関数では,malloc関数を用いて,シグナルスタックを確保し,SIGSEGVに対してハンドラの登録を行っている.最後に,strcpy関数で,SIGSEGVを発生させている. ハンドラでは,SIGSEGVが発生したメモリアドレス(si_addr)と,コンテキストのアドレス(sig_data),SIGSEGVを発生させた命令のアドレス(gregs[REG_RIP])を出力している.
実行結果は次の通りである.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
si_addr (nil)
[ucontext] 0x205ab00
[REG_RIP] 0x400986
si_addr (nil)
[ucontext] 0x205ab00
[REG_RIP] 0x400986
si_addr (nil)
[ucontext] 0x205ab00
[REG_RIP] 0x400986
si_addr (nil)
[ucontext] 0x205ab00
[REG_RIP] 0x400986
si_addr (nil)
[ucontext] 0x205ab00
[REG_RIP] 0x400986
si_addr (nil)
[ucontext] 0x205ab00
[REG_RIP] 0x400986
si_addr (nil)
[ucontext] 0x205ab00
[REG_RIP] 0x400986
si_addr (nil)
(snip)

ハンドラ内で出力が終了すると退避されていたコンテキトが復帰される.しかし,再びSIGSEGVを発生させた命令を実行するため,ハンドラが実行され続ける.

結果からわかる通りSIGSEGVが発生したアドレスを取得することができなかった. マップされていないアドレスだからだろうか,,,理由が分かり次第追記します.

まとめ

sigactionを使ってSIGSEGVを捕捉した. ハンドラでは,発生したシグナルの情報や,退避されているコンテキストを取得できる.取得できる情報については,manページやヘッダファイルを見るとよくわかる.

参考

sigaction
signalfd
ucontext
gregs