ソフトウェアブレークポイントとハードウェアブレークポイントの違い
ソフトウェアブレークポイントとハードウェアブレークポイント(プロセッサーブレークポイント, データブレークポイント)の違いを、Visual C++ や WinDbg を使いながら見てみます。
動作確認環境
- Windows 11 Home 22H2
- Visual Studio Community 2022 (Visual C++)
- WinDbg 10.0
ソフトウェアブレークポイントとは
ソフトウェアブレークポイントは、プログラムのコード(マシン語)を一時的にデバッグ用のトラップ命令に書き換えることによって実現するブレークポイントです。具体的には、停止したい箇所のマシン語を 0xCC に、アセンブリ言語でいうと INT 3 に書き換えます。
次のプログラムで試してみましょう。最初に eax レジスタを 0 に初期化し、以後順次 0x2222, 0x3333, 0x4444 を加算しています(処理内容に意味はありません)。
;アドレス マシン語 アセンブリ
00af8000 31c0 xor eax, eax
00af8002 0522220000 add eax, 2222h
00af8007 0533330000 add eax, 3333h
00af800c 0544440000 add eax, 4444h
このプログラムの 3 行目にソフトウェアブレークポイントを設定します(WinDbg を使います)。
0:000> bp af8007
1 行目から実行するよう指示します。
0:000> g =af8000
するとデバッガは、3 行目の先頭のマシン語の値が「05」であることを記憶し、「CC」で上書きしてから、プログラムを実行します。つまり、次のように改変したプログラムを実行します。
00af8000 31c0 xor eax, eax
00af8002 0522220000 add eax, 2222h
00af8007 CC33330000 int 3 (+ ゴミ) ★ デバッガが「55」を「CC」に書き換え
00af800c 0544440000 add eax, 4444h
1 行目が実行され、 2 行目が実行され、3 行目に達したところでマシン語の「CC」が解釈され、INT 3 のトラップが発生します。すると、本プログラムの実行は停止し、処理がデバッガに引き渡されます。
......: Breakpoint 0 hit
eax=00002222 ...... ★ 2 行目まで実行された
停止後にコードを逆アセンブルしても、「CC」や「int 3」は見当たりません。これは、デバッガが記憶しておいた値「05」に書き戻しているためです。
0:000> u af8000
00af8000 31c0 xor eax, eax
00af8002 0522220000 add eax, 2222h
00af8007 0533330000 add eax, 3333h ★ デバッガが「CC」を「05」に書き戻している
00af800c 0544440000 add eax, 4444h
ソフトウェアブレークポイントの「CC」を見る
プログラムの実行中にコードが「CC」に書き換わっている様子を見てみましょう。
次の C 言語プログラムを用意します。自作 none 関数の 1 バイト目の値を表示するプログラムです。
#include <stdio.h>
void none()
{
// 何もしない
}
int main()
{
printf("%02x", *(unsigned char*)none);
return 0;
}
ビルドします。ビルド条件は「Release ビルド, x86, 最適化なし(/Od)」にしておきます。
実行します。
55
「55」(= push ebp) と表示されました。none 関数はコンパイルすると以下のコードになります。表示されたのはこの先頭の「55」です。
;マシン語 アセンブリ
55 push ebp ★ この「55」が表示された
8bec mov ebp, esp
5d pop ebp
c3 ret
次に、none 関数の先頭にブレークポイントを設定します。
実行します。
cc
今度は「cc」(= int 3) と表示されました。ソフトウェアブレークポイントを仕掛けたことによって、none 関数が次のように改変されたためです。
;マシン語 アセンブリ
cc int 3 ★ 「55」が「cc」に書き換えられた
8bec mov ebp, esp
5d pop ebp
c3 ret
確かにコードが「cc」に書き換わっていました。
ハードウェアブレークポイントとは
ソフトウェアブレークポイントではコードの一部を書き換えてプログラムを実行しましたが、ハードウェアブレークポイント(= プロセッサブレークポイント)ではコードは書き換えません。その代わり、ブレーク対象のアドレスを CPU が用意しているデバッグ専用のレジスタに格納してからプログラムを実行します。
最初に挙げた以下のプログラムで試してみましょう。
00af8000 31c0 xor eax, eax
00af8002 0522220000 add eax, 2222h
00af8007 0533330000 add eax, 3333h
00af800c 0544440000 add eax, 4444h
このプログラムの 3 行目にハードウェアブレークポイントを設定します。
0:000> ba e 1 af8007
「ba e 1 af8007」は「アドレス af8007 から 1 バイト分のメモリがプログラム実行 (e = execute) のためにアクセスされたら中断する (ba = break on access)」の意味です。
1 行目からの実行を指示します。
0:000> g =af8000
するとデバッガは、設定されたブレークポイントの内容をもとにデバッグレジスタに適切な値を格納してから、プログラムを実行します。
そして、1 行目と 2 行目が実行され、3 行目の実行前に停止します。
......: Breakpoint 0 hit
eax=00002222 ...... ★ 2 行目までが実行された
デバッグレジスタの値を見てみます。
0:000> rM20
dr0=00af8007 dr1=20000000 dr2=30000000
dr3=40000000 dr6=ffff0ff1 dr7=00000101
図示します。「ba e 1 af8007」の設定が、下図の青色の箇所に格納されています。
ハードウェアブレークポイントのメリット
ハードウェアブレークポイントには、「指定のアドレスの命令が実行されるとき」だけでなく、「指定のアドレスのデータが読み書きされたとき」に処理を中断させることができるという大きなメリットがあります。この機能をデータブレークポイントと呼ぶことがあります。変数の値がいつの間にか書き換わっているが、どこでだれが書き換えたのかわからない、といった場合の調査に役立ちます。
次のプログラムで試してみましょう。上から順に、アドレス 0x012FF000 のリードとライト、アドレス 0x012FF004 のリードとライトをしています。両アドレスは読み書き可能な領域とします。
00af8000 a100f02f01 mov eax, [012FF000h] ★ 12FF000 のリード
00af8005 a300f02f01 mov [012FF000h], eax ★ 12FF000 のライト
00af800a a104f02f01 mov eax, [012FF004h] ★ 12FF004 のリード
00af800f a304f02f01 mov [012FF004h], eax ★ 12FF004 のライト
実行前に、データブレークポイントを設定します。アドレス 0x012FF000 から 4 バイト分についてはリード時とライト時 (r)、アドレス 0x012FF004 から 4 バイト分についてはライト時 (w) に中断するよう設定しています。
0:000> ba r 4 12FF000 ★ 12FF000 のリード時とライト時
0:000> ba w 4 12FF004 ★ 12FF004 のライト時
実行します。
0:000> g =af8000
......: Breakpoint 0 hit ......
eip=00af8005 ......
0:000> g
......: Breakpoint 0 hit ......
eip=00af800a ......
0:000> g
......: Breakpoint 1 hit ......
eip=00af8014
設定どおり、「12FF000 のリード時とライト時」および「12FF004 のライト時」にブレークポイントで停止しました。「12FF004 のリード時」には停止しませんでした。
デバッグレジスタの値を見てみます。
0:000> rM20
dr0=012ff000 dr1=012ff004 dr2=30000000
dr3=40000000 dr6=ffff0ff2 dr7=00df0105
図示します。「ba r 4 12FF000」と「ba w 4 12FF004」の設定が、下図の青色と黄色の箇所に格納されています。
なお、コードのブレークポイントは指定のアドレスの命令が実行される「前」に停止するのに対して、データブレークポイントは指定のアドレスのデータが読み書きされた「後」に停止するので注意が必要です。
ハードウェアブレークポイントのデメリット
コードを書き換えることなく処理を中断できたり、データへのアクセスを監視できたりと便利なハードウェアブレークポイントですが、一度に仕掛けられるブレークポイントが 4 個までという制限があります。監視対象のアドレスを格納するデバッグアドレスレジスタ(DR0, DR1, DR2, DR3)が 4 個しかないためです。
次のように 5 個以上のハードウェアブレークポイントを設定することはできます。
0:000> ba r 4 10000000
0:000> ba r 4 20000000
0:000> ba r 4 30000000
0:000> ba r 4 40000000
0:000> ba r 4 50000000
0:000> bl
0 e 10000000 r 4 0001 (0001) 0:****
1 e 20000000 r 4 0001 (0001) 0:****
2 e 30000000 r 4 0001 (0001) 0:****
3 e 40000000 r 4 0001 (0001) 0:****
4 e 50000000 r 4 0001 (0001) 0:****
しかしこれはデバッガに情報が登録されたに過ぎません。
いざプログラムを実行すると、情報がデバッグレジスタに入りきらず「Too many data breakpoints」というエラーメッセージが表示されます。
0:000> g =af8000
Too many data breakpoints for thread 0 ★ 設定数過多でエラー
bp4 at 50000000 failed
Visual C++ の IDE から 5 個目のデータブレークポイントを作成しようとしたときも同様に、「ブレークポイントを設定できません。データブレークポイントの最大数は既に設定されています。」というエラーメッセージが表示されます。
おわりに
ソフトウェアブレークポイントとハードウェアブレークポイントの違いを見てきました。
まとめると次のようになります。
ソフトウェア ブレークポイント | ハードウェア ブレークポイント | |
---|---|---|
最大使用可能数 | 上限なし | 4 |
コードの一時的な書き換え | あり | なし |
コードの実行時に停止 | 可 | 可 |
データのリード・ライト時に停止 | 不可 | 可 |
データのライト時に停止 | 不可 | 可 |
コードを止めるには数に制限のないソフトウェアブレークポイントを使い、データのアクセスを監視したい場合は貴重なハードウェアブレークポイントを使う、というのが一般的な使い方になるかと思います。
参考文献
[1] Intel「Intel(R) 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4」, 2023
[2] インテルジャパン「386DX マイクロプロセッサ プログラマーズ・リファレンス・マニュアル(第 2 版)」, 1991