デバッグを効率的に進めるためには、ウォッチポイントの実装が不可欠だ。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
ちゃんと実装できているようだ!