glibc でバックトレースを表示する方法(その2)


C/C++で関数を書きました.デバッグに超便利なので,まるごと公開します.

backtraceを取得して,FILE* に出力します.出力時の書式は format の引数で変更できます.CおよびC++両方に対応しています.
C++のシンボル名は,自動で demangle(デマングル)します.demangle の実装は abi::__cxa_demangle() を使っています.

backtraceについては過去の記事を参照してください http://d.hatena.ne.jp/pyopyopyo/20061018

コード

プロトタイプ宣言

extern "C" void dump_backtrace(FILE *fp, const char *format);

です.extern "C" をつけているので C言語でも C++からでも利用できます

実装

#include <execinfo.h>
#include <strings.h>
#include <stdio.h>

extern "C" void
dump_backtrace(FILE *fp, const char *format)
{
    void *trace[128];
    int n = backtrace(trace, sizeof(trace) / sizeof(trace[0]));
    char **strings = backtrace_symbols(trace, n);
    if (!strings) {
	perror("backtrace_symbols() returns NULL");
	return;
    }

    size_t demangled_sz = 1024;
    char *demangled_str = (char*)malloc(demangled_sz);
    if (!demangled_str) {
	perror("malloc() returns NULL");
	return;
    }
    int j;
    for (j = 0; j < n; j++) {
        //  strings[j] には "ファイル名 ( シンボル名 + オフセット) アドレス" という形式の文字列が格納されてるので
        //  "(" から "+" の部分を探して,シンボル名を切り出す
	char *start  = index(strings[j], '(');
	char *offset = index(start, '+');
	if (offset - start > 1) {
            // シンボル名が見つかった場合は demangle する
	    *start = '\0';
	    *offset = '\0';
	    char *symbol = (start+1);
	    char *ret = abi::__cxa_demangle(symbol, demangled_str, &demangled_sz, NULL);
	    if (ret) {
		demangled_str = ret;
	    } else {
		ret = NULL;
	    }

	    char tmp[1024*4];
	    const char *tail = tmp + sizeof(tmp);
	    char *cur = tmp;
	    cur += snprintf(cur, tail-cur, strings[j]);
	    cur += snprintf(cur, tail-cur, "(%s+%s)", ret?ret:"", (offset+1));

	    fprintf(fp, format, tmp);
	} else {
            // シンボル名が見つからない場合は,そのまま何も変換せず,出力
	    fprintf(fp, format, strings[j]);
	}
    }
    free(demangled_str);
    free(strings);
}

使い方

使用方法は関数を呼ぶだけです.たとえばバックトレースを /dev/stderr に出力する場合は

  dump_backtrace(stderr, "%s\n");

ファイルに出力する場合,たとえばファイル名が log.txt の場合は

  FILE *fp = fopen("log.txt", "w");
  if (fp) {
   dump_backtrace(fp, "%s\n");
   fclose(fp);
  }

という感じです

abi::__cxa_demangle() について

abi::__cxa_demangle() は気をつけないとメモリリークします.

abi::__cxa_demangle() の定義は以下の通りですが

char* abi::__cxa_demangle 	( 	const char *  	mangled_name,
		char *  	output_buffer,
		size_t *  	length,
		int *  	status	 
	) 	

ここで戻り値に注意しないと memory leak します

戻り値は

  • demangle に成功した場合は realloc(output_buffer) の結果
  • demangle に失敗した場合は NULL

になります.この点を考慮していないコード,たとえば以下のコードはメモリリークする場合があります

// メモリリークする可能性があるコード(その1)
size_t sz = 128;
char *buf = malloc(sz):
abi::__cxa_demangle ("シンボル名", buf, &sz, NULL);
free(buf);

このコードは,abi::__cxa_demangle ()が内部で realloc(buf) を実行し, バッファのアドレスがbufから別アドレスに変わった場合にメモリリークします.

// メモリリークする可能性があるコード(その2)
size_t sz = 128;
char *buf = malloc(sz):
buf = abi::__cxa_demangle ("シンボル名", buf, &sz, NULL);
free(buf);

abi::__cxa_demangle ()はdemangle に失敗するとNULLを返します.つまり buf がNULLに置き換わります.結果,末尾の free(buf)では mallocで確保したメモリが開放できず メモリリークします.

abi::__cxa_demangle の戻り値が NULL か 非NULLか判定するとこれらのリークを回避できます

// 正しい使い方の例
size_t sz = 128;
char *buf = malloc(sz):
char *ret = abi::__cxa_demangle ("シンボル名", buf, &sz, NULL);
if (ret) {
    buf = ret;
}
free(buf);