Open venkat24 opened 4 years ago
I'll outline some thoughts about the debugger CLI.
One thing I found very useful during manual debugging was setting breakpoints on not just instruction addresses, but also a set number of CPU Cycles of emulator ticks.
Another useful feature is to be able to peek at any section of the code while in the CLI, enabling breakpoints to be set more easily.
This will require converting the memory pointed to into instruction mnemonics, essentially a disassembly process. This is trivial to do for instructions after the given address, but only if we can assume that the given address is the start of a valid instruction.
step
Continue execution by one instruction
run
Continue execution until a breakpoint is hit
exit
Stop execution
bp set <Addr>
and bp remove <Addr>
Break when the given address is hit
bp-cycles set <Cycles>
and bp-cycles remove <Cycles>
Break when the given number of CPU Cycles is hit
bp-ticks set <Ticks>
and bp-ticks remove <Ticks>
Break When the given number of system ticks is hit
peek <Addr>
Show program code and some context at the given location
reg
Display all registers
mem <Addr>
or mem <Addr start> <Addr end>
Read memory from given locations
It might be useful to make the actual debugger command actions agnostic to the user interface, to more easily plug in a GUI later.
We have the main debugger class, which implements the CLI and makes calls to a DebuggerCore object to actually tick the gameboy. The DebuggerCore class encapsulates general debugger behavior like breakpoints and Run/Step.
Here's some CeePlusPlusish Pseudocode for the CLI Debugger
class IDebuggerCore {
// Ticking actions - this modifies the internal gameboy state
// Return value indicates whether current instruction is a breakpoint
virtual bool tick();
// Non-Gameboy-modifying actions
virtual void set_breakpoint(Address breakpoint);
virtual void set_tick_breakpoint(int ticks);
virtual void set_cycle_breakpoint(CpuCycles breakpoint);
virtual void remove_breakpoint(Address breakpoint);
virtual void remove_tick_breakpoint(int ticks);
virtual void remove_cycle_breakpoint(CpuCycles breakpoint);
// Get current gameboy to check state
virtual IGameboy* get_gameboy();
// Get current debugger state
virtual vector<Address> get_breakpoints();
virtual vector<CpuCycles> get_cycle_breakpoints();
virtual vector<int> get_tick_breakpoints();
};
class DebuggerCore : IDebuggerCore {
private:
IGameboy* gameboy;
vector<Address> breakpoints;
vector<CpuCycles> cycle_breakpoints;
vector<int> tick_breakpoints;
public:
// ...
// implement all methods
bool tick() {
gameboy->tick();
return (is_breakpoint(gameboy->cpu->pc);
}
};
class IDebugger {
// Let the frontend (CLI or GUI) do its thing
// Let it tick the debugger core if it needs to and continue execution
// The fellow who owns this IDebugger (like main.cpp) will call tick()
// in a loop
virtual void tick(IDebuggerCore* debugger_core);
};
class CliDebugger : IDebugger {
IDebuggerCore* debugger_core;
// Passing the input and output streams makes automation easy,
// since we can pass a file as the input stream
Debugger(IDebuggerCore* debugger_core, istream input, ostream output);
bool is_breakpoint;
bool is_stepping;
void tick() override {
auto gameboy = debugger_core->get_gameboy();
// ...and read stuff from gameboy
if (is_breakpoint) {
// print breakpoint stuff
// ask the user for input
string user_command_string = get_from_input();
// if it's an action, like run or step, we call tick.
// We use is_stepping to keep track of it we're in stepping
// or running state
if (user_command is action) {
is_stepping = user command is Step?;
} else (user_command is query) {
// This does the actual execution of commands
// Could be a breakpoint op or a query
do_command(user_command);
is_breakpoint = debugger_core->tick();
}
}
else if (is_stepping) {
// Pretty similar to the is_breakpoint case
// Command line appearance might be different
// ...
}
else {
// We're in the running state
// just loop the core, don't stop until we hit a breakpoint
while(debugger_core->tick()) {};
}
}
};
class GuiDebugger : IDebugger {
// TODO
};
/// in main.cpp
main() {
auto gameboy = make_gameboy();
auto debugger_core = make_debugger_core(gameboy);
auto cli_debugger = make_cli_debugger(debugger_core);
for (;;) {
cli_debugger->tick();
}
}
Attn: @anish0x2a
Hey @venkat24, Thanks for such a detailed write up on the design. Seems like we are trying to achieve a GDB style debugger. Works with me. Some questions. First off, why do we put a set point on the CPU cycles, you mentioned it was useful but in what way? Pardon my limited experience with debuggers. Setting breakpoints at particular Addresses seems a natural thing to do, however, I cannot fathom the use of CPU cycles and ticks here.
Coming to the implementation of the CLI interface. Are we going to use std::input
or some other library? Will setting the ROM in debug mode, transfer the entire control to debugger clock? At each breakpoint, the user will be prompted to various stuff with the ROM, like setting or removing breakpoints, etc. Is this what we are trying to do here?
@anish0x2a
So, I was finding it useful to hook up some other open source emulator with logging statements, and have it print the program counter, and then see at which tick the value deviates between the two emulators, to see what might be causing weird behavior. In such a case it was nice to stop at a particular number of ticks, because after that the PC was incorrect.
I guess this is not as necessary when we have instruction breakpoints, and it also seems like something simple to add on later, so it seems alright to skip that at first.
And yes, when the start in debug mode, the debugger module takes control of ticking the CPU. And yeah, you have the right idea. It's a GDB style debugger.
There are some choices to be made for how to design the debugger.
Once thing I considered was to allow for the debugger to have a socket interface, so that any front-end may be used - such as a command line interface or a browser.
The debugger must also be able to observe the internal state of all modules at any point in time, including most obviously the CPU and Memory, but more helpfully the GPU registers and data too - including the VRAM data like the background tilesets and sprites. This of course if and only there's a UI, but being able to inspect this information easily seems useful for the debugger.