# デバッガー
読者は複雑なコードを書く際に間違ったコードを書くことだろう。間違ったコードは直せばよい。問題はどこが間違っているのかわからない場合だ。
例えば以下のコードは1
から10
までの整数を標準出力するはずのプログラムだ。
int main()
{
for ( int i = 1 ; i < 10 ; ++i )
std::cout << i ;
}
しかし実際に実行してみると、1
から9
までの整数しか標準出力しない。なぜだろうか。
読者の中にはコード中の問題のある箇所に気が付いた人もいるだろう。これはたったの5行のコードで、問題の箇所も1箇所だ。これが数百行、数千行になり、関数やクラスを複雑に使い、問題の原因は複数の箇所のコードの実行が組み合わさった結果で、しかも自分で書いたコードなので正しく書いたはずだという先入観がある場合、たとえコードとしてはささいな間違いであったとしても、発見は難しい。
こういうとき、実際にコードを1行ずつ実行したり、ある時点でプログラムの実行を停止させて変数の値を見たりしたいものだ。
そんな夢を実現するのがデバッガーだ。この章ではデバッガーとしてGDB(GNUプロジェクトデバッガー)の使い方を学ぶ。
GDBで快適にデバッグするには、プログラムをコンパイルするときにデバッグ情報を出力する必要がある。そのためには、GCCに-g
オプションを付けてプログラムをコンパイルする。
$ g++ -g -o program program.cpp
本書の始めに作った入門用のMakefile
を使う場合は、$gcc_options
に-g
を加えることになる。
gcc_options = -std=c++17 -Wall --pedantic-error -g
コンパイラーのオプションを変更したあとは、make clean
を実行してコンパイル済みヘッダーファイルを生成し直す必要がある。
$ make clean
# GDBのチュートリアル
では具体的にデバッガーを使ってみよう。以下のようなソースファイルを用意する。
int main()
{
int val = 0 ;
val = 10 ;
val += 1 ;
val *= 2 ;
val *= 2 ;
val /= 4 ;
}
このプログラムをコンパイルする。
$ g++ -g program.cpp -o program
GDBを使ってプログラムのデバッグを始めるには、GDBのオプションとして-g
オプション付きでコンパイルしたプログラムのファイル名を指定する。
$ gdb program
すると以下のように出力される。
GNU gdb (Ubuntu 8.2-0ubuntu1) 8.2
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from program...done.
(gdb)
大量のメッセージに戸惑うかもしれないが、最後の行以外はGDBのライセンス表記やドキュメントだ。細部は環境ごとに異なる。
ここで重要なのは最後の行だ。
(gdb)
ここにGDBのコマンドを入力する。ヘルプを表示するコマンドhelp
と入力してみよう。
(gdb) help
ヘルプメッセージが表示される。あるコマンドのヘルプを見たい場合はhelp コマンド
と入力する。いまから使う予定のコマンドであるlist
のヘルプを見てみよう。
(gdb) help list
list
コマンドは現在のソースファイルの前後10行を表示する。
(gdb) list
1 int main()
2 {
3 int val = 0 ;
4 val = 10 ;
5 val += 1 ;
6 val *= 2 ;
7 val *= 2 ;
8 val /= 4 ;
9 }
さっそく実行してみよう。実行するコマンドはrun
だ。
(gdb) run
Starting program: 実行可能ファイルへのパス
[Inferior 1 (process PID) exited normally]
run
コマンドを使うとデバッガーはプログラムを実行する。
プログラムの実行を特定の場所で止めるにはbreak
コマンドを使ってブレイクポイントを設定する。
(gdb) help break
break
コマンドには関数や行番号を指定できる。
(gdb) break main
(gdb) break 4
(gdb) break 5
これで、main
関数、4行目、5行目にブレイクポイントを設定した。さっそくもう一度最初から実行してみよう。
(gdb) run
Starting program: プログラムへのファイルパス
Breakpoint 1, main () at main.cpp:3
3 int val = 0 ;
main
関数にブレイクポイントを設定したので、プログラムはmain
関数が呼ばれたところ、最初のコードである3行目を実行する手前で止まる。
プログラムの実行を再開するにはcontinue
コマンドを使う。
(gdb) continue
Continuing.
Breakpoint 2, main () at main.cpp:4
4 val = 10 ;
4行目にブレイクポイントを設定したので、4行目を実行する手前で止まる。
この時点で、変数val
が初期化され、その値は0になっているはずだ。確かめてみよう。変数の値を調べるにはprint
コマンドを使う。
(gdb) print val
$1 = 0
値が0
になっていることが確認できた。実行を再開しよう。
(gdb) continue
Continuing.
Breakpoint 3, main () at main.cpp:5
5 val += 1 ;
4行目を実行し、5行目のブレイクポイントで止まる。4行目を実行したということは、変数val
の値は10
になっているはずだ。もう一度print
コマンドで調べてみよう。
(gdb) print val
$2 = 10
値は10
だ。GDBはprint
の結果の履歴を記録している。$1
や$2
というのはその記録を参照するための名前だ。その値はprint
コマンドで確認できる。
(gdb) print $1
$3 = 0
(gdb) print $2
$4 = 10
現在、プログラムは5行目を実行する手前で止まっている。このままcontinue
コマンドを使うとプログラムの終了まで実行されてしまう。もう一度1行だけ実行するにはbreak 6
で6行目にブレイクポイントを設定すればよいのだが、次の1行だけ実行したいときにいちいちブレイクポイントを設定するのは面倒だ。
そこで使うのがstep
だ。次の5行目を実行すると、変数val
の値は11
になっているはずだ。
(gdb) step
6 val *= 2 ;
(gdb) print val
$5 = 11
さて、残りの行もstep
して実行を1行ずつ確かめてみよう。
GDBの基本的な使い方を覚えたので、これから詳細な使い方を学んでいく。
# プログラムの実行
GDBでプログラムをデバッグするには、GDBの起動時にプログラムのオプションとしてプログラムのファイル名を指定する。プログラムのファイル名がprogram
の場合、以下のようにする。
$ ls
program
$ gdb program
起動したGDBでプログラムを実行するには、run
コマンドを使う。
(gdb) run
このとき、プログラムにオプションを指定したい場合はrun
に続けて記述する。例えばプログラムの標準出力をout.txt
にリダイレクトしたいときは以下のようにする。
(gdb) run > out.txt
# プログラムの停止方法
デバッガーの機能として一番わかりやすいのが、実行中のプログラムを一時停止させる機能だ。
# ブレイクポイント
コマンドbreak
はブレイクポイントを設定する。プログラムの実行がブレイクポイントに達した場合、GDBはブレイクポイントの直前でプログラムの実行を中断する。
ブレイクポイントを設定する場所はbreak
コマンドへの引数で指定する。省略してb
だけでもよい。
(gdb) break 場所
(gdb) b 場所
場所として使えるのは行番号と関数名だ。
# 行番号へのブレイクポイント
現在のソースファイルの123行目にブレイクポイントを設定する場合は以下のように書く。
(gdb) break 123
ソースファイルが複数ある場合は、
(gdb) break ファイル名:行番号
と書く。例えばfoo.cpp
の8行目にブレイクポイントを仕掛ける場合は、
(gdb) break foo.cpp:8
と書く。
# ブレイクポイントの確認
設定したブレイクポイントの一覧は、info breakpoints
コマンドで確認できる。
(gdb) break 5
Breakpoint 1 at 0x1150: file main.cpp, line 5.
(gdb) break 6
Breakpoint 2 at 0x1157: file main.cpp, line 6.
(gdb) break 7
Breakpoint 3 at 0x115b: file main.cpp, line 7.
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000001150 in main() at main.cpp:5
2 breakpoint keep y 0x0000000000001157 in main() at main.cpp:6
3 breakpoint keep y 0x000000000000115b in main() at main.cpp:7
これは5,6,7行目にそれぞれブレイクポイントを設定したあとのinfo breakpoints
の結果だ。
この表の意味は、左から番号(Num, Number)、種類(Type)、中断後の処理(Disposition), 有効/無効(Enb, Enable/Disable)、アドレス(Address), 内容(What)となっている。
ブレイクポイントには作成された順番に番号が振られる。ブレイクポイントの設定を変えるには、この番号でブレイクポイントを参照する。
ブレイクポイントには3種類ある。普通のブレイクポイントであるbreakpoint
のほかに、特殊なブレイクポイントであるウォッチポイント(watchpoint)、キャッチポイント(catchpoint)がある。
中断後の処理と有効/無効の切り替えはあとで説明する。
アドレスというのはブレイクポイントを設定した場所に該当するプログラムのコード部分であり、本書では解説しない。
内容はブレイクポイントを設定した場所の情報だ。
# ブレイクポイントの削除
ブレイクポイントを削除するにはdelete
コマンドを使う。削除するブレイクポイントは番号で指定する。
(gdb) delete 1
番号を指定しないとすべてのブレイクポイントを削除することができる。
(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) info breakpoints
No breakpoints or watchpoints.
# ブレイクポイントの有効/無効
ブレイクポイントは有効/無効を切り替えることができる。
ブレイクポイントを無効化するにはdisable
コマンドを使う。
(gdb) disable 1
ブレイクポイントを有効化するにはenable
コマンドを使う。
(gdb) enable 1
ブレイクポイントは発動したあとに自動で無効化させることができる。
enable [breakpoints] once
コマンドで、ブレイクポイントが一度発動すると自動的に無効化されるブレイクポイントを設定できる。
(gdb) enable 1 once
このコマンドは、ブレイクポイント番号1が一度発動したら自動的に無効化する設定をする。
ブレイクポイントは$n$回発動したあとに自動的に無効化することもできる。そのためのコマンドはenable [breakpoints] count n
だ。
(gdb) enable 1 count 10
上のコマンドは、ブレイクポイント番号1が10回発動したら自動的に無効化されるよう設定している。
# 関数名へのブレイクポイント
ブレイクポイントの場所として関数名を書くと、その関数名が呼び出されたあと、関数の本体の1行目が実行されるところにブレイクポイントが設定される。
現在のソースファイルの関数main
にブレイクポイントを設定する場合は以下のように書く。
(gdb) break main
ソースファイルが複数ある場合は、
(gdb) ファイル名:関数名
と書く。
C++では異なる引数で同じ名前の関数が使える。
void f() { }
void f(int) { }
void f(double) { }
int main()
{
f() ;
f(0) ;
f(0.0) ;
}
このようなプログラムで関数f
にブレイクポイントを設定すると、f
という名前の関数すべてにブレイクポイントが設定される。
ブレイクポイントの一覧を表示するinfo breakpoints
コマンドで確かめてみよう。
(gdb) break f
Breakpoint 1 at 0x1149: f. (3 locations)
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y <MULTIPLE>
1.1 y 0x0000000000001149 in f() at main.cpp:1
1.2 y 0x0000000000001153 in f(int) at main.cpp:2
1.3 y 0x000000000000115f in f(double) at main.cpp:3
関数名f
に該当するすべての関数に、ブレイクポイント番号1としてブレイクポイントが設定される。関数にはそれぞれサブの番号が振られる。
この状態でブレイクポイント番号1を削除すると、1.1, 1.2, 1.3はすべて削除される。
(gdb) delete 1
(gdb) info breakpoints
No breakpoints or watchpoints.
もし、オーバーロードされた同名の関数のうちの一部だけにブレイクポイントを仕掛けたい場合、曖昧性を解決するメニューを表示する設定にすることで、一部の関数だけを選ぶことができる。メニューを表示する設定にするには、
(gdb) set multiple-symbols ask
というコマンドを使う。これ以降のbreak
コマンドが名前が曖昧であることを検出した場合、以下のようなメニューを表示する。
(gdb) break f
[0] cancel
[1] all
[2] run.cpp:f()
[3] run.cpp:f(double)
[4] run.cpp:f(int)
>
ここで0
を入力するとキャンセル。1
を入力するとすべての関数にブレイクポイントを設定する。
特定の関数だけにブレイクポイントを設定したい場合、その関数に対応する番号を入力する。例えば、f()
とf(int)
だけにブレイクポイントを設定したい場合は、
> 2 4
と入力する。
# 条件付きブレイクポイント
(gdb) break ... if 条件
と入力すると、条件
がtrue
となるときのみブレイクポイントが発動する。
例えば以下のようなコードで、
int main()
{
int i { } ;
while ( i != 1000 )
{
++i ;
std::cout << i ;
}
}
以下のように7行目に変数i
が500
に等しい条件を設定すると
(gdb) break 7 if i == 500
変数i
が500
でありかつ7行目が実行される直前でブレイクポイントが発動する。
# プログラムの実行再開とステップ実行
以下のようなプログラムがあるとする。
int f( int x )
{
return x + 1 ;
}
int main()
{
int i = 0 ;
i = f(i) ;
i = f(i) ;
i = f(i) ;
}
このプログラムをmain
関数から1行ずつ実行してその挙動を確かめたい。その場合に、すべての行にブレイクポイントを設定するのは面倒だ。GDBではこのような場合に、現在中断している場所から1行だけ実行する方法がある。
# 実行再開(continue)
continue
コマンドは実行を再開する。省略してc
でもよい
(gdb) continue
(gdb) c
実行を再開すると、次のブレイクポイントが発動するか、プログラムが終了するまで実行が続く。
# ステップ実行(step)
step
コマンドは現在実行が中断している場所から、ソースファイルで1行分の実行をして中断する。
(gdb) step
(gdb) s
step
コマンドは省略してs
でもよい。
先ほどのソースファイルで、まずmain
関数にブレイクポイントを設定して実行すると、
(gdb) break main
(gdb) run
main
関数に入った直後で実行が中断する。
int main()
{
>> int i = 0 ;
i = f(i) ;
i = f(i) ;
...
この状態でstep
コマンドを使うと
(gdb) step
1行分にあたる実行が行われ、また中断される。
int main()
{
int i = 0 ;
>> i = f(i) ;
i = f(i) ;
...
もう一度step
コマンドを使うと、今度は関数f
の中で実行が中断する。
int f( int x )
{
>> return x + 1 ;
}
int main()
...
このままstep
コマンドを続けていくと、またmain
関数に戻り、また次の行が実行され、また関数f
が実行される。
1行ずつ実行するのではなく$n$行実行したい場合は、step
コマンドに$n$を指定する。
(gdb) step n
するとソースファイルの$n$行分実行される。例えば以下のように書くと、
(gdb) step 3
3行分実行される。
# ネクスト実行(next)
step
コマンドはソースファイルの1行分を実行してくれるが、途中に関数呼び出しが入る場合、その関数のソースファイルがある場合はその関数の中も1行とカウントする。next
コマンドは現在実行が中断しているソースファイルの次の行を1行として扱い、次の行まで実行して中断する。
例えばプログラムが以下の状態で中断しているとする。
int main()
{
int i = 0 ;
>> i = f(i) ;
i = f(i) ;
...
このままstep
コマンドを実行すると、関数f
の中の1行で実行が中断する。一方next
コマンドを使うと、
(gdb) next
現在止まっているソースファイルの次の1行の手前まで実行して中断する。途中の関数呼び出しはすべて実行される。
int main()
{
int i = 0 ;
i = f(i) ;
>> i = f(i) ;
...
step
コマンドと同じく、next
コマンドも$n$行分一度に実行することができる。
(gdb) next n
# 関数から抜けるまで実行(finish)
finish
コマンドは現在の関数からreturn
するまで実行する。
# バックトレース
バックトレースは中断しているプログラムの情報を得るとても強力なコマンドだ。
(gdb) backtrace
(gdb) bt
バックトレースを表示するにはbacktrace
もしくはbt
というコマンドを使う。
例えば以下のようなソースファイルがある。
void f() { }
void apple() { f() ; }
void banana() { f() ; }
void cherry() { apple() ; }
int main()
{
f() ;
apple() ;
banana() ;
cherry() ;
}
ここで関数f
に注目してみよう。関数f
はさまざまな関数から呼ばれる。関数main
から呼ばれるし、関数apple
やbanana
からも呼ばれる。特に、関数cherry
は関数apple
を呼び出すので、間接的に関数f
を呼ぶ。
関数f
にブレイクポイントを仕掛けて実行してみよう。
(gdb) break f
(gdb) run
(gdb) continue
(gdb) continue
(gdb) continue
(gdb) continue
関数f
が呼ばれるたびに実行が中断するが、関数f
がどこから呼ばれたのかがわからない。
こういうときにバックトレースが役に立つ。
上のコマンドを実行しながら、関数f
のブレイクポイントが発動するたびに、backtrace
コマンドを入力してみよう。
(gdb) break f
(gdb) run
(gdb) backtrace
#0 f () at main.cpp:2
#1 0x0000555555556310 in main () at main.cpp:10
#0
がバックトレースの最も深い現在のスタックフレームだ。これは関数f
でソースファイルmain.cpp
の2行目だ。#1
が#0
の上のスタックフレームで、これは関数main
で10行目にある。
実行を再開して、次の関数f
のバックトレースを見よう。
(gdb) continue
(gdb) backtrace
#0 f () at main.cpp:2
#1 0x00005555555562ec in apple () at main.cpp:4
#2 0x0000555555556315 in main () at main.cpp:11
今回はスタックフレームが3つある。最も外側のスタックフレームは関数main
で、そこから関数apple
が呼び出され、そして関数f
が呼び出される。
さらに進めよう。
(gdb) continue
(gdb) backtrace
#0 f () at main.cpp:2
#1 0x00005555555562f8 in banana () at main.cpp:5
#2 0x000055555555631a in main () at main.cpp:12
今度はmain
→banana
→f
になった。次はどうだろうか。
(gdb) continue
(gdb) backtrace
#0 f () at main.cpp:2
#1 0x00005555555562ec in apple () at main.cpp:4
#2 0x0000555555556304 in cherry () at main.cpp:6
#3 0x000055555555631f in main () at main.cpp:13
最後はmain
→cherry
→apple
→f
だ。
このようにバックトレースを使うことでプログラムの状態を調べることができる。
# 変数の値を確認
変数の値を確認するにはprint
コマンドを使う。
(gdb) print 式
print
コマンドは式を評価した結果を出力する。
例えば以下のようなソースファイルがある。
int main()
{
int x = 1 ;
x += 1 ;
x *= 2 ;
}
この変数x
の値を見ていこう。
まず変数x
が初期化されるところまで実行する。
(gdb) break main
(gdb) run
(gdb) step
(gdb) print x
$1 = 1
1行ずつ実行して値を見ていこう。
(gdb) step
(gdb) print x
$2 = 2
(gdb) step
(gdb) print x
$3 = 4
print 式
コマンドで注意すべき点としては、式
の副作用もプログラムに反映されるということだ。例えば以下のように変数x
を変更する式も使えるし、変数x
は実際に変更されてしまう。
(gdb) print ++x
(gdb) print x = 0
式では関数まで呼べてしまう。
void hello()
{
std::cout << "hello"
}
int main() { }
このプログラムは関数hello
を呼ばないし標準出力には何も出力しない。しかしこのプログラムをGDBでロードし、main
関数にブレイクポイントを設定してから実行し、ブレイクポイントが発動したらprint hello()
コマンドを使ってみると、
(gdb) break main
(gdb) run
(gdb) print hello()
なんと関数hello
が呼び出され、標準出力にhello
と出力されるではないか。
print
コマンドの式のもたらす副作用には注意しよう。
# シグナルによるプログラムの中断
プログラムはさまざまな理由によりシグナルを出して実行を強制的に終了する。このシグナルはGDBによってトラップされ、ブレイクポイントと同じくプログラムの中断として扱われる。
プログラムが実行を終了するようなシグナルは、プログラムの不具合によって生じる。具体的な不具合は実行環境に依存するが、たいていの環境で動く不具合は、nullポインターを経由した間接アクセスと、ゼロ除算だ。
// nullポインターを経由した間接アクセス
int * ptr = nullptr ;
*ptr = 0 ;
// ゼロ除算
1 / 0 ;
実際にそのようなプログラムを作ってGDBで実行し、プログラムが中断されることを確認してみよう。
int main()
{
int x { } ;
std::cin >> x ;
std::cout << 1 / x ;
}
このプログラムはユーザーが標準入力から0
を入力するとゼロ除算となり強制的に終了する。GDBで実行してみよう。
$ gdb program
(gdb) run
Starting program:
0
Program received signal SIGFPE, Arithmetic exception.
0x0000555555556336 in main () at main.cpp:5
5 std::cout << 1 / x ;
ちょうどゼロ除算を起こした箇所でプログラムの実行が中断する。
このとき中断した状態でプログラムのさまざまな状態を観測できる。例えばバックトレースを表示したり、変数の値を確認したりできる。
# コアダンプを使ったプログラムの状態の確認
プログラムがシグナルによって強制的に終了したときに、たまたまデバッガーで動かしていたならばプログラムの状態を調べられる。しかし都合よくデバッガーで実行していない場合はどうすればいいのか。
まずプログラムを普通に実行してみよう。
$ program
0
Floating point exception (core dumped)
core dumped
という文字が気になる。プログラムはシグナルで強制的に実行を終了するときコアファイルをダンプする。このファイル名は通常core
だ。通常はカレントディレクトリーにcore
という名前のファイルが生成されているはずだ。
もしカレントディレクトリーにcore
という名前のファイルがない場合、以下のコマンドを実行する。
$ ulimit -c unlimited
これによりcore
ファイルが生成されるようになる。
すでにコアファイルが存在する場合に上書きされるかどうかは環境により異なる。昔のコアファイルがいらないのであれば消しておこう。
$ rm ./core
$ ./program
0
Floating point exception (core dumped)
$ find core
core
このコアファイルはデバッガーに読み込ませることで、プログラムが強制的に終了するに至った瞬間のプログラムの状態を調べるのに使える。
使い方はGDBにプログラムファイルと一緒に指定するだけだ。
$ gdb program core
...
Core was generated by `./program'.
Program terminated with signal SIGFPE, Arithmetic exception.
#0 0x000055dcbfd3d336 in main () at main.cpp:5
5 std::cout << 1 / x ;
(gdb) backtrace
#0 0x000055dcbfd3d336 in main () at main.cpp:5
(gdb) print x
$1 = 0
デバッガーはとても役に立つ。本書では少しだけしか解説しなかったが、このほかにも強力な機能がたくさんある。
← 分割コンパイル