デバッグを効率的に進めるためには、ウォッチポイントの実装が不可欠だ。GDBでは、以下のウォッチポイントが実装されているようだ。
- watch : 特定のメモリアドレスに対して書き込みが発生すると停止する
- rwatch : 特定のメモリアドレスに対して読み込みが発生すると停止する
この機能を実現するために、GDBには以下のコマンドが定義されている。
- Z2xxxxxxxx,size : アドレスxxxxxxxxにサイズsizeの書き込みが発生すると停止する(この際停止する場所は「書き込み後」で良い)。
- Z3xxxxxxxx,size : アドレスxxxxxxxxにサイズsizeの読み込みが発生すると停止する(この際停止する場所は「読み込み後」で良い)。
- Z4xxxxxxxx,size : アドレスxxxxxxxxにサイズsizeの読み込みまたは書き込みが発生する(この際停止する場所は「読み込みまたは書き込み後」で良い)。
まず、これらのコマンドを実現するためには、プログラムアドレスの時と同様に、ブレークポイントを格納するためのキューが必要だろう。
// Set Write Memory BreakPoint if (IsEqualHead (packet_str, "Z2")) { int idx; for (idx = 3; packet_str[idx] != ',' && idx < packet_str.length(); idx++) { break_addr = (break_addr << 4) | Hex (packet_str[idx]); } if (idx >= packet_str.length()) { PutPacket ("E00"); } else { m_env->AddWriteMemBreak (break_addr); PutPacket ("OK"); } } ... std::set<Addr_t> m_queue_read_break; // Read Memory break list std::set<Addr_t> m_queue_write_break; // Write Memory break list
ブレークポイントを設定、解除、参照するためのメソッドを定義する。
void EnvBase::AddReadMemBreak (Addr_t addr) { if (m_queue_read_break.find (addr) == m_queue_read_break.end()) { InfoPrint ("<set_read_watch : %08x>\n", addr); m_queue_read_break.insert (addr); } else { InfoPrint ("<set_read_watch : already inserted %08x>\n", addr); } return; } bool EnvBase::RemoveReadMemBreak (Addr_t addr) { for (auto it = m_queue_read_break.begin(); it != m_queue_read_break.end(); it++) { if ((*it) == addr) { InfoPrint ("<remove_read_watch : %08x>\n", addr); m_queue_read_break.erase (it); return true; } } return false; } bool EnvBase::FindReadMemBreak (Addr_t addr) { if (m_queue_read_break.find (addr) == m_queue_read_break.end()) { return false; } else { return true; } }
メモリアクセスを実行する度に、ブレークポイントに引掛っているかをチェックしている。
MemResult mem_result = m_env->LoadFromBus (mem_addr, Size_Byte, &res);
m_env->CheckSetReadMemBreakPoint (mem_result);
ブレークポイントにヒットすると、メモリアクセスのフラグとして命令実行ステータスを変更し、プログラム実行のループから抜け出し、GDBに応答を返す。 このときに、GDBから応答が返ったときは、当該メモリアクセスの「実行後」であるが、PCは「当該命令」に存在していなければならない。
このため、メモリアクセスのブレークポイントに引っ掛かると、プログラムを進めることはせず、そのまま応答を返すようにする。
if (IsMemBreakPoint ()) { return ExecAfterBreak; } if ((GetTrace()->IsDelayedSlot() == false) && (GetJumped () == false)) { ProceedPC (); // Update PC }
実際に使ってみる
xv6の、cpu->schedulerにメモリアクセスが発生したところで停止させてみる。gdbのスクリプトは以下のようになる。
rwatch cpu->scheduler
GDBインタフェース側の出力
remote Thread <main> In: L?? PC: 0x1fc00000
Breakpoint 1 at 0x80107c64: file swtch.S, line 29.
Hardware read watchpoint 2: cpu->scheduler
...
|21 kinit1(end, P2V(8*1024*1024)); // phys page allocator |
|22 kvmalloc(); // kernel page table |
|23 mpinit(); // collect info about this machine |
>|24 cprintf("\ncpu%d: starting xv6\n\n", cpu->id); |
|25 picinit(); // interrupt controller |
|26 consoleinit(); // I/O devices & their interrupts |
|27 uartinit(); // serial port |
|28 pinit(); // process table |
$ disassemble
0x8010527c <+52>: lui v0,0x8011
=> 0x80105280 <+56>: lw v0,-15040(v0)
0x80105284 <+60>: lbu v0,0(v0)
0x80105288 <+64>: move a1,v0
0x8010528c <+68>: lui v0,0x8011
0x80105290 <+72>: addiu a0,v0,-18008
0x80105294 <+76>: jal 0x801007fc <cprintf>
0x80105298 <+80>: nop
0x8010529c <+84>: jal 0x801055d4 <picinit>
0x801052a0 <+88>: nop
0x801052a4 <+92>: jal 0x8010138c <consoleinit>
0x801052a8 <+96>: nop
ちゃんと実装できているようだ!