26
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

gdbを使ったdynamic debugの小技集

Posted at

はじめに

こ、えだ臭?(デブ活

ソフトウェア開発をするとどうしてもデバッグに割かれる時間は長くなります。効率的にデバッグをするためにはデバッグをしやすい環境を整えておくことが不可欠です。特にLinuxだと標準的なデバッガとしてgdbが使えることが多いので、gdbを中心にデバッグ環境を整えることも多いと思います。

この記事では、gdbを使ったdynamicな方法でのデバッグの小技をいくつか紹介します。逆に、一般的なgdbの使い方の、break入れてcontinueしたり変数の中身を見たりといったとこは他の記事に任せることとします。

とはいえまずはgdbの一般的な話から

dynamicにといっても、素性のgdbのままでは辛いことも多い。個人的には下記を常用している。

.gdbinit
set print pretty on
set print object on
set print static-members on
set print vtbl on
set print demangle on
set print asm-demangle on
set print elements 0
set demangle-style gnu-v3
set pagination off

ほかは「set print repeats」とか、ポインタを配列表示したいときの「p some_array[0]@16」とか、C++のSTLコンテナをうまく表示できないときのためのマクロあたりか。命令ステップ実行(si)するときは「display/i $pc」もよく使う。

とはいえ、私自身も設定項目をいちいち覚えていないので、この手のはgithubにsettingレポジトリとして置くようにしている。逆に言えば、githubで他人の.gdbinitを検索すれば有用な設定やマクロが見つかる。

一般的なgdbの使い方としては、私もたまたま見つけたけど、@tsuyopon氏のgdbメモ(gdb.md)が非常に詳しい。日本語でここまでまとまったものはなかなかないので、gdbをよく使う人は一通り読んでおいて損はない。

変数・メモリ・レジスタ書き換え

pを使った書き換え

gdbを使うと変数やメモリの中身が見れると紹介した記事が多いが、むしろ、変数やメモリやレジスタの中身を書き換えるのがgdbの本来の使い方じゃないかと思っている。表示するときはp(print)を使うが、書き換えるときもpを使えば良い。

(gdb)
(gdb) p val
$3 = 2
(gdb) p val = 3
$4 = 3
(gdb) p/x *(char*)(0x555555555158)
$10 = 0x1
(gdb) p/x *(char*)(0x555555555158) = 2
$11 = 0x2
(gdb) p/x $edi
$1 = 0x1
(gdb) p/x $edi = 2
$2 = 0x2

関数書き換え

特に書き換えは、権限さえあればmmapprot属性を無視して操作できるので(ptrace使うから回避している?)、関数を書き換えてしまうことも可能。

print_increment_val.cpp
static void print_increment_val(int val){
    int newval = val + 1;
    printf("%d\n", newval);
}
(gdb)
(gdb) disas print_increment_val(int)
Dump of assembler code for function print_increment_val(int):
=> 0x0000555555555149 <+0>: sub    $0x8,%rsp
   0x000055555555514d <+4>: lea    0x1(%rdi),%edx
   0x0000555555555150 <+7>: lea    0xead(%rip),%rsi        # 0x555555556004
   0x0000555555555157 <+14>:    mov    $0x1,%edi
   0x000055555555515c <+19>:    mov    $0x0,%eax
   0x0000555555555161 <+24>:    callq  0x555555555050 <__printf_chk@plt>
   0x0000555555555166 <+29>:    add    $0x8,%rsp
   0x000055555555516a <+33>:    retq
End of assembler dump.
(gdb) call print_increment_val(11)
12
(gdb) p/x *(unsigned char*)0x000055555555514f
$21 = 0x1
(gdb) p/x *(unsigned char*)0x000055555555514f = 2
$22 = 0x2
(gdb) disas print_increment_val(int)
Dump of assembler code for function print_increment_val(int):
=> 0x0000555555555149 <+0>: sub    $0x8,%rsp
   0x000055555555514d <+4>: lea    0x2(%rdi),%edx
   0x0000555555555150 <+7>: lea    0xead(%rip),%rsi        # 0x555555556004
   0x0000555555555157 <+14>:    mov    $0x1,%edi
   0x000055555555515c <+19>:    mov    $0x0,%eax
   0x0000555555555161 <+24>:    callq  0x555555555050 <__printf_chk@plt>
   0x0000555555555166 <+29>:    add    $0x8,%rsp
   0x000055555555516a <+33>:    retq
End of assembler dump.
(gdb) call print_increment_val(11)
13

関数呼び出し

callコマンド

callもしくはp(print)を使うと関数呼び出しができる。pを使うと関数の返り値をgdbの変数に代入できるのでpのほうがよいかと思う。

(gdb) p $testret = printf("Hello %s\n", "rarul")
Hello rarul
$2 = 12
(gdb) p $testret
$3 = 12

gdbマクロにcallをいっぱい並べて単体テストっぽいこともできると思う。ただ一直線に呼ぶだけだったならば普通にコンパイルして実行して**assert()**で比較するだけでよい気もする。いい応用例があるかどうかについてはまだ模索中...

gdbで止めた状態のまま関数を呼ぶ仕組みのため、リエントラントな関数群でないと気軽には呼べない。例えば、下記でmalloc(), free()を呼ぶ例を出すけど、libcのmallocの中のmutex資源がリエントラントにならないので、妙な箇所で止めた状態でcallするとデッドロックで抜けてこないかもしれない。

また、callの途中でsignalや例外が出るといろいろめんどい。gdbマニュアルにunwindonsignalの記載があるとはいえ、あまり面倒なことはやらないほうがよいんじゃないかと思う。

メモリ管理

callしたい内容によっては一時メモリが必要になる。でも大丈夫、**malloc()free()**を呼べば良い。

(gdb) p/x $tmpbuf = ((void* (*) (size_t)) malloc)(32)
$15 = 0x7ffff7fc0f80
(gdb) call strcpy($tmpbuf, "Hello World!\n")
$16 = 0x7ffff7fc0f80 "Hello World!\n"
(gdb) call puts($tmpbuf)
Hello World!
$17 = 14
(gdb) call ((void(*)(void*))free)($tmpbuf)

C++だとnew, deleteしたくなるけど、stlコンテナの場合は**malloc()**した領域を無理やり使う方法もある模様。Creating C++ string in GDB - Stack Overflow (Thanks to and via gdb覚書 - Qiita)

...まで書いてから追記するのもなんだけど、先に書いたとおり、malloc()は内部でmutexを持つので、だったらもう直接anonymous mmapしたほうが早いんじゃね?ってことで、試してみると、下記のようにできた。

(gdb) call mmap(0, 4096, 0x7, 0x22, -1, 0)
$7 = (void *) 0x7ffff7ffb000
(gdb) x/16wx $7
0x7ffff7ffb000: 0x00000000  0x00000000  0x00000000  0x00000000
0x7ffff7ffb010: 0x00000000  0x00000000  0x00000000  0x00000000
0x7ffff7ffb020: 0x00000000  0x00000000  0x00000000  0x00000000
0x7ffff7ffb030: 0x00000000  0x00000000  0x00000000  0x00000000
(gdb) call memset($7, 0x5a, 4096)
$8 = (void *) 0x7ffff7ffb000
(gdb) x/16wx $7
0x7ffff7ffb000: 0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a
0x7ffff7ffb010: 0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a
0x7ffff7ffb020: 0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a
0x7ffff7ffb030: 0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a  0x5a5a5a5a
(gdb) call ((int(*)(void*,size_t))munmap)($7, 4096)
$9 = 0

mmap, munmapをgdbマクロでdefineして関数化しておけばいろいろと便利、かもしれない。なお「0x7 == PROT_READ|PROT_WRITE|PROT_EXEC」「0x22 == MAP_PRIVATE|MAP_ANONYMOUS」です。キャストとか#defineの値の中身とか、説明が少々前後してしまっている点はもうしわけない。

ttyをつなぎ替える

ttyを失ったりstdout, stderrが閉じられたりしたプロセスの出力を再び奪う応用例も比較的有名。

(gdb) call puts("test")
test
(gdb) call close(1)
(gdb) call open("/dev/null", 1)
$24 = 1
(gdb) call puts("test")
(gdb) call close(1)
(gdb) call open("/dev/pts/1", 1)
$27 = 1
(gdb) call puts("test")
test

ここではfile descriptorの1番を閉じた直後にopen()してるので、そのまま1が割り当たって都合が良い。番号いじりにくいときはopen()してdup2()してclose()する流れになるかと思う。ちなみにopen()の引数「1」はLinuxでO_WRONLYの意味。O_RDONLYが0でO_WRONLYが1でO_RDWRが2の模様。(asm-generic/fcntl.hより。固定値だと決めつけるのは怖いけどABIだバイナリコンパチだ守る限りは変わらないだろうからもう固定値で覚えといてよいんじゃないだろうか)

ネタの提供元

環境変数を書き換える

同様に環境変数を書き換える応用例も比較的有名。

(gdb) p getenv("TESTENV")
$36 = 0x0
(gdb) p setenv("TESTENV", "test test", 1)
$37 = 0
(gdb) p getenv("TESTENV")
$38 = 0x5555555593f8 "test test"

ネタ提供元

直接関係しない話だけど、Linux kernelは環境変数をプロセス起動時に引き継ぐ機能しか提供しないため、/proc/[PID]/environからはプロセス起動時の環境変数しか取れない。この説明に疑問を持った人はman environをちゃんと読んで置こう。

prototypeがわからない場合

compile & link したときに見えていたシンボル状況によっては関数のプロトタイプが見えないことがある。その場合は無理やり関数ポインタにキャストすればよい。

(gdb) p $tmpbuf = ((void* (*) (size_t)) malloc)(32)
$13 = (void *) 0x7ffff7fc8030
(gdb) p ((void(*)(void*))free)($tmpbuf)
$14 = void

このあたりも先ほどのgdbマニュアルに記載されている。.gotや**.plt**が気になるところだけど、シンボルが見えてさえいれば通常のshared libraryのときと同じ動きをするので、特に問題はない模様。ただ、当然ではあるけど、unused functionでリンク時に消された関数などは呼び出しは不可能。(malloc()してマシン命令列を代入してmprotect()でPROT_EXECをつければcallできるとは思うけどさすがにそこまで試す気にはならなかった)

動的にprintfを挿入する

dprintf

gdbのdprintfを使えば動的にデバッグ出力分を挿入できる。コンパイルし直し不要だし、引数や関数トレースポイントがわかってさえいれば問題切り分けも容易になるし、ロギング機能が弱い環境にも適用しやすい。

(gdb) dprintf puts, "puts(%s) is called!\n", str
Note: breakpoint 4 also set at pc 0x7ffff7e545a0.
Dprintf 5 at 0x7ffff7e545a0: file ioputs.c, line 33.

ネタ提供元

gdbのマニュアルを参考に、dprintfの出力先を変更することもできるが、正直、拡張性や連携の面では弱い。dprintfが使いにくいと感じる場合は、下記で説明するcommandsを使い自分でマクロを組むとよい。

なお、dprintfは内部的にbreak,gdbマクロ実行,continueをするため、実行速度面ではどうしても不利になる。システムレベルでのトレースが目的の場合は、straceperfのほうがよい。

commands

commandsを使うと、breakpointにヒットしたときに実行するものをマクロ的に定義できる。下記では、puts()にbreakpointをはり、ヒットしたときに引数の文字列を表示しつつ、そのままcontinueする例を示している。これでdprintfと同等のことを実現できる。

(gdb) b puts
Breakpoint 3 at 0x7ffff7e545a0: file ioputs.c, line 33.
(gdb) commands
Type commands for breakpoint(s) 3, one per line.
End with a line saying just "end".
>silent
>printf "puts is called with %s\n", str
>continue
>end

任意のgdbマクロ文を記述できるので、intをenumにキャストしてprintしたり、特定条件のときだけcontinueせずに止めたままにしたり、特定の値だったら引数を書き換えて実行を継続したり、といったことができる。特定の値のときだけbreakしたい場合はBreak Conditionsで条件付きでbreakpointをはる方法でもよい。

外部shellと連携する

shellコマンド

shellを使えばgdbプロンプトの中で外部シェルコマンドを実行できる

(gdb) shell ls
Makefile  test  test.cpp  test.o

とはいえ、一般的には、もっと特化したmakeの方を便利に使ってる人が多いんじゃないかと思う。kill, make, file, run と順に実行するようマクロを組んでおけば、繰り返しの実行を素早く執り行いやすい。(set argsももちろん使ってね) pipeにつなげる応用もある(gdbマニュアルより)ものの、shell単体だと「gdb中でshellを立ち上げられるよね」以上の使い所がいまいちないように見える。

凝ったことをしたい場合はevalコマンドを使うと良いという例も見えるが、実施に使ってみると、二重に変数展開したいような場面で非常に使いづらいことがわかった。どうせなら次に書くsourceを介したやり方がよいと思う。

shell & source

shellの実行結果をgdbで受け取るやり方はどうもないようだ。代わりに、それを無理やりやる方法として、shellを実行した結果をリダイレクトでファイルに保存し、その保存したファイルをsourceでgdbから読む方法がある。下記は、pgrep使って目的のプログラムのPIDを探してattachする例になる。

(gdb) shell echo attach `pgrep target_program` > attach_pid.gdbinit
(gdb) source attach_pid.gdbinit
0x00007fa69bf0b334 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7fff4b68f010, rem=re\
m@entry=0x7fff4b68f010) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
78  ../sysdeps/unix/sysv/linux/clock_nanosleep.c: No such file or directory.
(gdb) bt
#0  0x00007fa69bf0b334 in __GI___clock_nanosleep (clock_id=<optimized out>, clock_id@entry=0, flags=flags@entry=0, req=req@entry=0x7fff4b68f010, re\
m=rem@entry=0x7fff4b68f010) at ../sysdeps/unix/sysv/linux/clock_nanosleep.c:78
#1  0x00007fa69bf11047 in __GI___nanosleep (requested_time=requested_time@entry=0x7fff4b68f010, remaining=remaining@entry=0x7fff4b68f010) at nanosl\
eep.c:27
#2  0x00007fa69bf10f7e in __sleep (seconds=0) at ../sysdeps/posix/sleep.c:55
#3  0x00005613614e812a in main () at test.cpp:27

見つかるまでpgrepし続けるような例がStack Overflowにて紹介されている。

別の例として、cross buildなどのsysrootを設定しないといけない環境にて、pathの取得と設定を自動化する例を紹介する。sysrootのトップになる特徴的なpath部分文字列があれば容易に設定することができる。

(gdb) shell echo set sysroot ${PWD%%/usr/src*} > sysroot.txt
(gdb) source sysroot.txt

shellの結果をsourceするという方法は下記のStack Overflowにて初めて知った。

  • Memory dump formatted like xxd from gdb - Stack Overflow
    手を介して良いなら外部スクリプトを使っていくらでも整形できるけど、いちいち個別化したスクリプトを準備して実行していられないくらいの高頻度で行う作業は、こういうsourceを絡めたgdbマクロにしておくとよい。

その他

reverse step実行やextended-remoteのことも一通り試した上でまとめようと思っていたけど、ごめん文章を書くのに力尽きてしまった。紹介だけに留める。

あとがき

gdbの組み込みPythonを有効にした上でPythonでgdbマクロを書くのが今流だという話もある。ただ、悲しいかな、なぜかこれまでPython無効にされたgdbの環境ばかりしか経験してないんだ。

(gdb) python import os
Python scripting is not supported in this copy of GDB

自分でgdbビルドしてもいいんだけど、なぜかそこまでやる気力が今までは沸かなかった。個人的な話で悪いんだけど実はPython書いたことないんだ、ごめんね、どうもタブの思想が受け入れがたいようで。逆に、Pythonバリバリ使ってこうやるんだよという例を逆提案してもらうと嬉しい。gdb-pedaなんかもほとんど使ったことがない。

gdbマニュアルを読んでいると、systemtapdtraceとの連携の話も見かけたけど、正直よくわからん。だれか私みたいなPython使えない老害おじさんにもわかりやすいgdbの使い方を紹介した記事書いて教えて。

参考サイト

gdbマニュアル

gdbの便利な使い方を紹介した記事

その他

26
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?