disassembleによるデバッグの方法

開発環境でのバイナリやデバッグバイナリではないもの、すなわち、

  • 障害解析対象のプログラムは-gが付いていない(プロダクション用なら当然)

というようなバイナリからとれたstacktraceを手がかりにsignal 11の位置を特定する方法について書きます。

(以前よりも少しやりかたを改善できたのでメモしておきます。もっと簡単にできる方法があればコメント下さい)

16進数のstacktraceを確認

まずエラーログを見る。mysqldの場合はエラーログにstacktraceを吐いてくれるので別にcoreファイルがなくても大丈夫。吐いてくれないプログラムの場合は"ulimit -c unlimited"でcoreを出すようにしておいてbtする。

こんな感じの16進数のstacktraceが取れる。

0x5c4ceb
0x6267de
0x626e63
...

hoge.logとでもしておく。

バイナリからシンボルを入手

strip済みのバイナリだと取れないので注意。stripしない、あるいは事前に入手しておくことが必要。

GNU binutilsのnmコマンドで採取する。

nm --numeric-sort --demangle=auto mysqld > fuga.sym

オプションの"--numeric-sort"はアドレス順にソートして出力してくれるようにするためのもの。

シンボルとstacktraceを突き合わせる

MySQLには、resolve_stack_dumpという便利なコマンドが付いています。これはシンボル情報とstacktraceをつき合わせて、stacktraceに書かれているアドレスがどの関数のもので、その関数の先頭アドレスから何バイト目のインストラクションのものなのかを判別してくれるものです。

./resolve_stack_dump fuga.sym hoge.log

このコマンドのやっていることは単純な突き合わせなので、使えない場合には人間の目による手動突き合わせでも代用可能です。(面倒くさいけど)

するとこんな感じになります。

0x5c4ceb handle_segfault + 593
0x6267de join_read_next(st_read_record*) + 60
0x626e63 sub_select(JOIN*, st_join_table*, bool) + 499
...

ここではjoin_read_next(st_read_record*)の開始アドレスから60bytesのところでsignal 11が起きたのではということが分かります。

対象の関数をdisassembleする

join_read_next関数が書かれたソースファイルを見ても、まだどの行が何バイト目なのかが分からないため、ソースの行の特定ができません。

そこでdisassembleしてまずはどのインストラクションを実行する際に問題が起きたのかを調べます。

disassembleはgdbなどで行えます。この時に使うバイナリは障害が起きたものと同じ-g無しのバイナリです。

(gdb) disass join_read_next
Dump of assembler code for function _Z14join_read_nextP14st_read_record:
0x00000000006267a2 <_Z14join_read_nextP14st_read_record+0>:     push   %rbp
0x00000000006267a3 <_Z14join_read_nextP14st_read_record+1>:     mov    %rsp,%rbp
0x00000000006267a6 <_Z14join_read_nextP14st_read_record+4>:     sub    $0x20,%rsp
0x00000000006267aa <_Z14join_read_nextP14st_read_record+8>:     mov    %rdi,0xffffffffffffffe8(%rbp)
0x00000000006267ae <_Z14join_read_nextP14st_read_record+12>:    callq  0x88dbe4 <_my_thread_var>
0x00000000006267b3 <_Z14join_read_nextP14st_read_record+17>:    mov    0xa8(%rax),%eax
0x00000000006267b9 <_Z14join_read_nextP14st_read_record+23>:    and    $0x1,%eax
0x00000000006267bc <_Z14join_read_nextP14st_read_record+26>:    xor    $0x1,%eax
0x00000000006267bf <_Z14join_read_nextP14st_read_record+29>:    test   %al,%al
0x00000000006267c1 <_Z14join_read_nextP14st_read_record+31>:    jne    0x6267f9 <_Z14join_read_nextP14st_read_record+87>
0x00000000006267c3 <_Z14join_read_nextP14st_read_record+33>:    callq  0x88dbe4 <_my_thread_var>
0x00000000006267c8 <_Z14join_read_nextP14st_read_record+38>:    mov    0xa8(%rax),%eax
0x00000000006267ce <_Z14join_read_nextP14st_read_record+44>:    and    $0x1a,%eax
0x00000000006267d1 <_Z14join_read_nextP14st_read_record+47>:    cmp    $0xa,%eax
0x00000000006267d4 <_Z14join_read_nextP14st_read_record+50>:    jne    0x6267f9 <_Z14join_read_nextP14st_read_record+87>
0x00000000006267d6 <_Z14join_read_nextP14st_read_record+52>:    mov    0xffffffffffffffe8(%rbp),%rax
0x00000000006267da <_Z14join_read_nextP14st_read_record+56>:    mov    0x20(%rax),%rdx
0x00000000006267de <_Z14join_read_nextP14st_read_record+60>:    mov    0xbe0(%rdx),%rax
0x00000000006267e5 <_Z14join_read_nextP14st_read_record+67>:    add    $0x1,%rax
0x00000000006267e9 <_Z14join_read_nextP14st_read_record+71>:    mov    %rax,0xbe0(%rdx)
0x00000000006267f0 <_Z14join_read_nextP14st_read_record+78>:    movl   $0x0,0xffffffffffffffe4(%rbp)
0x00000000006267f7 <_Z14join_read_nextP14st_read_record+85>:    jmp    0x626843 <_Z14join_read_nextP14st_read_record+161>
0x00000000006267f9 <_Z14join_read_nextP14st_read_record+87>:    mov    0xffffffffffffffe8(%rbp),%rax
0x00000000006267fd <_Z14join_read_nextP14st_read_record+91>:    mov    0x8(%rax),%rax
0x0000000000626801 <_Z14join_read_nextP14st_read_record+95>:    mov    (%rax),%rax
0x0000000000626804 <_Z14join_read_nextP14st_read_record+98>:    add    $0xc0,%rax
0x000000000062680a <_Z14join_read_nextP14st_read_record+104>:   mov    (%rax),%rdx
...

この行で落ちたということになります。

0x00000000006267de <_Z14join_read_nextP14st_read_record+60>:    mov    0xbe0(%rdx),%rax

ソースコードデバッグ情報付きでアセンブラにする

これだけだとまだソースコードの行とアセンブラのインストラクションの対応の裏付けができません。

そこでgccデバッグ情報付きのアセンブラコードを作成します。

make実行時のログなどから、対象のソースファイルのコンパイル実行コマンドを抜き出してきます。join_read_next関数はsql/sql_select.ccというファイルに書かれていまして、以下のようにコンパイルされていました。

gcc -DMYSQL_SERVER -DDEFAULT_MYSQL_HOME="\"/usr/local/mysql\"" -DDATADIR="\"/usr/local/mysql/data\"" -DSHAREDIR="\"/usr/local/mysql/share/mysql\"" -DHAVE_CONFIG_H -I. -I. -I.. -I../zlib -I../innobase/include -I../innobase/include -I../include -I../include -I../regex -I.  -I/usr/include/senna      -DDBUG_OFF  -g   -fno-implicit-templates -fno-exceptions -fno-rtti -c -o sql_select.o sql_select.cc

ここに、--save-tempsオプションを付与することでgccが作る中間ファイル(プリプロセッサ実行後のファイル、アセンブラソースファイル)が残るようになり、さらに-dAオプションを付与することでアセンブラソースファイルにデバッグ情報が付与されるようになります。

gcc -DMYSQL_SERVER -DDEFAULT_MYSQL_HOME="\"/usr/local/mysql\"" -DDATADIR="\"/usr/local/mysql/data\"" -DSHAREDIR="\"/usr/local/mysql/share/mysql\"" -DHAVE_CONFIG_H -I. -I. -I.. -I../zlib -I../innobase/include -I../innobase/include -I../include -I../include -I../regex -I.  -I/usr/include/senna      -DDBUG_OFF  -g   -fno-implicit-templates -fno-exceptions -fno-rtti -c -o sql_select.o sql_select.cc --save-temps -dA

これで生成されるsql_select.sというファイルがアセンブラソースファイルです。

disassemble結果、アセンブラソース、C++ソースファイルを比較する

では必要なものがそろったのでそれぞれ比較してみましょう。

sql_select.sでの該当箇所を調べてみます。join_read_nextで検索します。

_Z14join_read_nextP14st_read_record:
.LFB5275:
        # sql_select.cc:11295
        .loc 16 11295 0
        # basic block 0
        pushq   %rbp
.LCFI1430:
        movq    %rsp, %rbp
.LCFI1431:
        subq    $32, %rsp
.LCFI1432:
        movq    %rdi, -24(%rbp)
.LBB285:
        # sql_select.cc:11299
        .loc 16 11299 0
        call    _my_thread_var
        movl    168(%rax), %eax
        andl    $1, %eax
        xorl    $1, %eax
        testb   %al, %al
        jne     .L2870
        # basic block 2
        call    _my_thread_var
        movl    168(%rax), %eax
        andl    $26, %eax
        cmpl    $10, %eax
        jne     .L2870
        # basic block 3
        # sql_select.cc:11304
        .loc 16 11304 0
        movq    -24(%rbp), %rax
        movq    32(%rax), %rdx
        movq    3040(%rdx), %rax
        addq    $1, %rax
        movq    %rax, 3040(%rdx)
        # sql_select.cc:11305
        .loc 16 11305 0
        movl    $0, -28(%rbp)
        jmp     .L2873

このうち、以下の行と同じものは、、

0x00000000006267de <_Z14join_read_nextP14st_read_record+60>:    mov    0xbe0(%rdx),%rax

10進数表記と16進数表記の違いによりちょっと分かりにくくなっていましたが、sql_select.cc:11304行目に相当するこの部分ですね。

        .loc 16 11304 0
        movq    -24(%rbp), %rax
        movq    32(%rax), %rdx
        movq    3040(%rdx), %rax
        addq    $1, %rax
        movq    %rax, 3040(%rdx)
        # sql_select.cc:11305

ここの"movq 3040(%rdx), %rax"がこれに該当します。

sql_select.ccの11304行目を見てみると、、

static int
join_read_next(READ_RECORD *info)
{
  int error;
#ifdef ENABLE_SENNA
  if ((my_thread_var->sen_flags & SENNA_USE_2IND) &&
      (my_thread_var->sen_flags &
       (SENNA_MATCH | SENNA_DO_READ_RECORD | SENNA_IF_READ_RECORD))
      == (SENNA_MATCH | SENNA_DO_READ_RECORD)) {
    //error=info->file->index_next(NULL);                                                                                                                                   
    statistic_increment(info->thd->status_var.senna_2ind_count, &LOCK_status); ←ここが該当
    return 0;
  } else

このstatistic_incrementしている部分で落ちていたようです。

ちなみにstatistic_incrementはマクロなのですが、sql_select.ii(プリプロセッサ後)で見ると、以下のようになっています。

join_read_next(READ_RECORD *info)
{
  int error;

  if (((_my_thread_var())->sen_flags & (1 << 0)) &&
      ((_my_thread_var())->sen_flags &
       ((1 << 1) | (1 << 3) | (1 << 4)))
      == ((1 << 1) | (1 << 3))) {

    (info->thd->status_var.senna_2ind_count)++;
    return 0;
  } else

といことで以下の最初の3つのmovqはinfo->thd->status_var.senna_2ind_countをaddqするためにレジスタに入れるための作業だったようです。

        .loc 16 11304 0
        movq    -24(%rbp), %rax
        movq    32(%rax), %rdx
        movq    3040(%rdx), %rax
        addq    $1, %rax
        movq    %rax, 3040(%rdx)
        # sql_select.cc:11305

ということでstatus_varが0x0だったか、あるいは不正なアドレスを指していたか、ということが分かりました。

最初に得られたのは16進数のstacktraceだけだったのですが、ここまでくると多少はデバッグも進められそうと思います。

おしまい。