これは何?
プログラムのデバッグをしていて、あるいは既存のプログラムの挙動を調べていて、ブレークポイントがある経路を通ってきたときのみ停止してほしいということが結構ある気がしたので、そういうブレークポイントが作れないかをやってみました。その経路の指定のために、各所に「ラベルつきのブレークポイント」が置けるようにして、そのラベルたちの吐いたログが、予め設定しておいた正規表現にマッチするときのみ停止するという風にしてみたらいい感じだったので書いてみます。
Motivating Examples
ケース1: ループのなかの処理の最初のほうは無視して最後のほうだけブレークポイントを止めたい
例えばゲームのメインループや最適化法の反復処理で、ループの最後のほうに値や挙動がどうなっているか調べたいのに普通にブレークポイントを置くとそこに到達するまでcontinueし続けなければいけないということがあります。
このぐらい簡単な例であればiに関する条件でブレークポイントを仕掛けることもできるでしょうが、例えばループのなかから別の関数を呼び出してそこにブレークポイントを置いたとすると、その関数から直接ループカウンタiにアクセスすることはできないので「最後のほうだけみる」のが難しかったりします。そういったときに、先頭何回かのブレークポイントを飛ばして、最後のほうだけ停止する、ということができればコードを汚染せずにできると嬉しかったりします。
#include <cstdio> using namespace std; int main(){ for(int i=0; i < 100; i++){ printf("hoge %d\n", i); // ここにブレークポイント } return 0; }
例えば上のようなコードで、先頭95回は飛ばして残りのところをみたい、というような場合です。
ケース2: ある関数がいろいろな関数から呼ばれるが、特定の関数から呼ばれているようなときだけブレークポイントを止めたい
デバッグに限らず、何か既存のプログラムの挙動を調べるためにブレークポイントを置いて、適当な入力についてプログラムを回してみる、ということがあります。例えば、以下の例について、関数hの挙動が気になるのでここにブレークポイントを置いてみたとします。
#include <cstdio> using namespace std; void h(){ // fからもgからもよばれる printf("h is called!\n"); // gからよばれたときだけブレークポイントで止めたい } void f(){ printf("f is called!\n"); h(); } void g(){ printf("g is called!\n"); h(); } int main(){ for(int i=0; i < 100; i++){ if(i != 71){ f(); }else{ g(); } printf("i = %d\n", i); } return 0; }
この例だと、hはfから沢山よばれるのですが、gからは滅多に呼ばれません。なので、gからよばれたときのhの挙動が気になりますが、普通にhに置いてしまうと滅多に起こらないことのためにcontinueをカチカチする必要があります。一応gに置いてそこからステップインする方法もありますが、それを毎回やるのも面倒です。
このような問題をどう解決する?
以上のような問題をまとめて解決するために、次のようなブレークポイントがあればよいのではないかと考えました。
実装
gdbのPython拡張としてこれを実装してみました。実装は以下のとおりです。
import gdb import re bplog = "" pat = None class MyBreakPoint(gdb.Breakpoint): def __init__(self, label, spec): self.label = label super().__init__(spec) def stop(self): global bplog bplog += self.label # print("log:" + bplog) if re.match(pat, bplog): return True else: return False class RegisterNamedBreakPoint(gdb.Command): def __init__(self): super().__init__("register-labelled-break-point", gdb.COMMAND_USER) def invoke (self, arg, from_tty): splitted = arg.split(" ") label = splitted[0] rest = " ".join(splitted[1:]) # argをもとにブレークポイントをつくる print(f"Registered label {label}") MyBreakPoint(label, rest) class RegisterCondition(gdb.Command): def __init__(self): super().__init__("register-regular-expression", gdb.COMMAND_USER) def invoke(self, arg, from_tty): global pat pat = re.compile(arg) class ShowLogLabels(gdb.Command): def __init__(self): super().__init__("show-log-labels", gdb.COMMAND_USER) def invoke(self, arg, from_tty): global bplog print(bplog) RegisterNamedBreakPoint() RegisterCondition() ShowLogLabels()
使い方は簡単で、上のPythonファイルをbreakpoint-re.pyという名前で保存し、gdbの実行時オプションで「-x breakpoint-re.py」とするか、あるいは起動後に「source breakpoint-re.py」とします*1。この拡張を有効にすることによって、gdbに以下の3つのコマンドが追加されます。
- register-labelled-break-point (ラベル文字) (ブレークポイントを設置する位置)
- register-regular-expression (ブレークポイントを停止させる条件をあらわす正規表現パターン)
- show-log-labels
上2つのコマンドを使って、ラベルつきブレークポイントを設置し、パターンを設定した上であとは普通に実行すれば、パターンに一致するところでのみ停止します。show-log-labelsは、上でお話した「ブレークポイント用ログ」を出力するものです。様子をみるために使ってください。
使用例
ケース1について
以下の使用例では、7行目に「a」というラベルのブレークポイントを置き、正規表現パターンとして「a{95}」を指定しています。これによって、aが95回印字されたところで始めて停止するようになり、最初のほうはブレークポイントがスキップされてただ「hoge 5」のように出力されています。
ashiato45@DESKTOP-648N1H1:~/gdbtest$ gdb -x breakpoint-re.py ./a.out (中略) Reading symbols from ./a.out... (gdb) register-labelled-break-point a skip_first.cpp:7 Registered label a Breakpoint 1 at 0x1162: file skip_first.cpp, line 7. (gdb) register-regular-expression a{95} (gdb) run Starting program: /home/ashiato45/gdbtest/a.out hoge 0 hoge 1 hoge 2 (中略) hoge 92 hoge 93 Breakpoint 1, main () at skip_first.cpp:7 7 printf("hoge %d\n", i); (gdb) show-log-labels aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (gdb) c Continuing. hoge 94 (中略) Breakpoint 1, main () at skip_first.cpp:7 7 printf("hoge %d\n", i); (gdb) c Continuing. hoge 99 [Inferior 1 (process 19886) exited normally] (gdb) show-log-labels aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa (gdb)
ケース2について
以下の使用例では、6行目に「h」というラベルのブレークポイントを、15行目に「g」というラベルのブレークポイントを置き、正規表現パターンとして「.*gh$」を指定しています。これによって、gからhがよばれたところで停止するようになり、かつ他のところではラベルの末尾が「hh」になるはずなので停止せずスキップされます。
ashiato45@DESKTOP-648N1H1:~/gdbtest$ gdb -x breakpoint-re.py ./a.out (中略) Reading symbols from ./a.out... (gdb) register-labelled-break-point h from_one_function.cpp:6 Registered label h Breakpoint 1 at 0x1171: file from_one_function.cpp, line 6. (gdb) register-labelled-break-point g from_one_function.cpp:15 Registered label g Breakpoint 2 at 0x11a4: file from_one_function.cpp, line 15. (gdb) register-regular-expression .*gh$ (gdb) run Starting program: /home/ashiato45/gdbtest/a.out f is called! h is called! i = 0 f is called! h is called! (中略) i = 69 f is called! h is called! i = 70 g is called! Breakpoint 1, h () at from_one_function.cpp:6 6 printf("h is called!\n"); // gからよばれたときだけブレークポイントで止めたい (gdb) c Continuing. h is called! i = 71 f is called! h is called! (中略) i = 98 f is called! h is called! i = 99 [Inferior 1 (process 24883) exited normally] (gdb)