GCCの-ftrapv (2)
前回、GCCの-ftrapvを使用すると、符号あり整数同士の演算におけるオーバーフローを検出し、オーバーフロー時にabort()が呼ばれることを示しました。しかし、「abortじゃ意味ないんだよねー、C++の例外をthrowするとか、せめてbacktraceを表示するとかしてくれないと」というご意見もあることでしょう。その点を改善してみます。内容の割に長いです。-gつけてコンパイルしてgdb上で実行すればいいじゃんというツッコミは野暮です。
■ abortする前にbacktraceを表示したり、プロセスの状態を表示したりしたい
これは、デバッグ用と割り切って細かいこと*1を言わなければ比較的簡単です。
- SIGABRTのハンドラをインストールしておく
- 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
- (2) SIGABRTのハンドラでthrowするのもだめ
- (3) では、__addvsi3()を乗っ取ってthrowすればいいかというと、それはそれで結構困難
なんというか、そこまでするなら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に詳しくて驚きます。