Read/Write Memory/Register
This is perhaps the easiest functionality to do in our debugger, because they are merely variables in our virtual machine. All we need to do is to dig a hole in our interfaces to allow access to them.
Interface
struct context
{
int r1;
int r2;
int r3;
int r4;
int sp;
int ip;
};
class debugger
{
public:
debugger(virtual_machine_debugging_interface* virtual_machine_debugging_interface);
// Low level debugging
context get_context();
void set_context(context c);
int read_memory(int address);
void write_memory(int address, int content);
};
class virtual_machine
{
public:
debugger* debug(instruction_sequence instructions, int entry_point);
};
class virtual_machine_debugging_interface
{
public:
virtual context get_context() = 0;
virtual void set_context(context c) = 0;
virtual int read_memory(int address) = 0;
virtual void write_memory(int address, int content) = 0;
};
Implementation
An important note, the debug()
method on the virtual machine merely creates a debugger object and load the program, it does not start execute the code yet.
In some sense, the interface is almost the answer to the problem, we will skip the implementation part for this chapter.
Practical notes
Despite the simplicity, this is what actually happen when one develop a debugger. These functionalities are expected from the operating system. Think about it, there is really no other way round to implement this without having operating system support because the operating system is supposed to protect one process from interacting with another. From the perspective on an operating system, it isn't all that hard to implement these either since most likely these are known data structures in the kernel (e.g. thread context / page tables / ...), but we will not dive into those implementations.