GCCの-ftrapv (2)

前回GCCの-ftrapvを使用すると、符号あり整数同士の演算におけるオーバーフローを検出し、オーバーフロー時にabort()が呼ばれることを示しました。しかし、「abortじゃ意味ないんだよねー、C++の例外をthrowするとか、せめてbacktraceを表示するとかしてくれないと」というご意見もあることでしょう。その点を改善してみます。内容の割に長いです。-gつけてコンパイルしてgdb上で実行すればいいじゃんというツッコミは野暮です。

■ abortする前にbacktraceを表示したり、プロセスの状態を表示したりしたい


これは、デバッグ用と割り切って細かいこと*1を言わなければ比較的簡単です。

  1. SIGABRTのハンドラをインストールしておく
  2. abort()をLD_PRELOADで乗っ取る

のどちらかでいいでしょう。1. から。

// install.c

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

static void sigabrt_handler(int signo __attribute__((unused))) {
  printf("backtraceやプロセスの状態表示のつもり\n");
  fflush(stdout);
}

__attribute__((constructor)) void install_sigabrt_handler(void) {
  struct sigaction sa = {
    .sa_handler = sigabrt_handler,
    .sa_flags   = 0
  };
  sigemptyset(&sa.sa_mask);
  if (! sigaction(SIGABRT, &sa, NULL)) {
    printf("SIGABRT handler instlled.\n");
  }
}

を用意して、次のようにPRELOADすればいいです。

$ gcc -D_REENTRANT -DDEBUG -fPIC -Wall -W -c install.c 
$ gcc -shared -Wl,-soname,libabrt.so.1 -o libabrt.so.1.0.0 install.o

$ ./a.out  (main内ですぐにabortするプログラムを準備)
Aborted

$ LD_PRELOAD=./libabrt.so.1.0.0 ./a.out
SIGABRT handler instlled.
backtraceやプロセスの状態表示のつもり
Aborted

abort()前に自分のハンドラが呼ばれるようになりました。めでたしめでたし。backtraceする処理は、高林さんの記事を参考にして書いてみてください。


C99のdesignated initializer機能で構造体をオシャレに初期化していますが、気にしないでください。それと、abort()時のSIGABRTシグナルは、abort()を呼んだスレッド宛に送られる同期的なシグナルですから*2、通常の(非同期なシグナル用の)シグナルハンドラと違って、ハンドラの中でprintfしてもかまいません (→ ほんとかよ。間違ってたら誰か教えてください。あ、POSIXじゃなくてC99だけど、7.14.1.1でraiseとabortは特別扱いされているなぁ…)。


signal handlerからbacktraceするのはちょっと面倒だなーという場合は、2.でお願いします。コンパイル方法とPRELOAD方法は1.と一緒です。この実装例は、動くけど手抜きです。細部にこだわりたい場合は、glibcのソースや「詳解UNIXプログラミング」のp.300 などを参考に改良すると良いと思います。

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

void abort(void) {
  printf("backtraceやプロセスの状態表示のつもり\n");
  fflush(stdout);

  raise(SIGABRT);
  _exit(1);
}

■ abortしないで、C++の例外をthrowする


こっちは結構(いや、すごく)大変です。

  • (1) まず、abort()を乗っ取ってthrowするのはNG
    • stdlib.h で、abort()は __attribute__((nothrow)) とされているから
    • しかも、libgcc が、gcc -fexceptions でコンパイルされていない(かもしれない)から。詳細はこの記事を参照のこと
  • (2) SIGABRTのハンドラでthrowするのもだめ
    • シグナルハンドラからthrowしてよいかは implementation-defined*3だから。ちなみに dwarf2 な GCC では投げられない
    • しかも、abort()で投げられたSIGABRTのハンドラから戻ると、abort()側でプロセスを終了するようになっているから
  • (3) では、__addvsi3()を乗っ取ってthrowすればいいかというと、それはそれで結構困難
    • __addvsi3()などが含まれているのは libgcc.a または libgcc_s.so ですが、前者が静的にリンクされてしまっていると、当然LD_PRELOADできないから。どういうときにどちらがリンクされるかは後述
    • しかも、__addvsi3()などは、gcc-4.0.x/gcc/libgcc2.c で実装されていますが、オーバーフロー検出のロジックをパチるために、このファイルだけを切り出してきて手元でコンパイルするのは結構大変だから

なんというか、そこまでするならGCCを改造するほうが早いような・・。私はあきらめましたが、一応情報へのポインタだけ。(3)で、乗っ取らなければならない関数の一覧は、一応文書化されてます。GCC Internalsというドキュメントの4.1.3がそれです。また、__addvsi3()等の関数は、GCCが内部で勝手にリンクするライブラリである、libgcc_s.so および libgcc.a に格納されているわけですが、どちらがリンクされるのかについては次のようになります:

  • C++*4なプログラムをコンパイルすると、GCCはlibgcc_s.so をリンクする
    • 詳細はGCCのman pageの、-static-libgcc オプションのところを読んでください
  • C言語なプログラムをリンクすると、GCCはlibgcc.aをリンクする

Cの場合は、__addvsi3()をPRELOADしたかったら次のようにしてデバッグ対象のプログラムにあらかじめ libgcc_s.so を(明示的に)リンクしておかなければなりません。

 $ gcc -ftrapv test.c -lgcc_s
 $ ldd a.out
        libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x007d0000)
        libc.so.6 => /lib/tls/libc.so.6 (0x00360000)
        /lib/ld-linux.so.2 (0x00347000)

■ (おまけ) libgcc.a と libgcc_s.so.1 の違い


libgcc.a と libgcc_s.so では、含まれるシンボルが少し違います。

 $ nm -g /usr/lib/gcc/i386-redhat-linux/3.4.3/libgcc.a 2>/dev/null | grep " T " 
    | awk '{print $3;}' | sort > /tmp/gcc
 $ nm -D /lib/libgcc_s.so.1 2>/dev/null                            | grep " T " 
    | awk '{print $3;}' | sort > /tmp/gcc_s

として /tmp/gcc と /tmp/gcc_s のdiffを見ていただければわかるんですけど、.so のほうが含まれるシンボルが多いです。ざっくり言うと、.soにだけ _Unwind_XXX が含まれます。まず、プログラムを g++ でコンパイルすると、普通に -lgcc_s がリンクされます。これは、g++ -v とすれば確認できます。プログラムを gccコンパイルした場合、gcc -v で確認してみると、最後のリンクのところが次のようになっていると思います。

 /usr/libexec/gcc/i386-redhat-linux/3.4.4/collect2 (中略) -lgcc --as-needed -lgcc_s --no-as-needed .....

これは、平たく言うと「まずlibgcc.aをリンクしてみて、それで不足があればlibgcc_s.soをリンクする」という意味です。普通、Cで書かれたプログラムは _Unwind_XXX を必要としませんので、libgcc_s.so はリンクされないことになります。libgcc_eh.a の話は略。


本日のエントリは、最後のほうの話題が書きたかっただけです。libgcc_s.so と言えればなんでもよかった。忘れないうちにメモしておくのが目的です。Daiさん、ご協力ありがとうございました。ディストロ作ってる人って、大抵GCCに詳しくて驚きます。

*1:マルチスレッド時のシグナルのマスクが…とか、ELF/GCC依存は…とか

*2:abortと、それが参照しているraiseの説明を参照

*3:ISO/IEC 14882:2003 §18.7/5

*4:正確には例外機構を持つ言語: C++, Java ...