# デバッガー

読者は複雑なコードを書く際に間違ったコードを書くことだろう。間違ったコードは直せばよい。問題はどこが間違っているのかわからない場合だ。

例えば以下のコードは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行目に変数i500に等しい条件を設定すると

(gdb) break 7 if i == 500

変数i500でありかつ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から呼ばれるし、関数applebananaからも呼ばれる。特に、関数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

今度はmainbananafになった。次はどうだろうか。

(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

最後はmaincherryapplefだ。

このようにバックトレースを使うことでプログラムの状態を調べることができる。

# 変数の値を確認

変数の値を確認するには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

デバッガーはとても役に立つ。本書では少しだけしか解説しなかったが、このほかにも強力な機能がたくさんある。