# Debug Server The file `server.cpp` adds a gdb-server compatible with several GDB versions and IDEs like VScode and CLion.
It is implemented as a standalone server independent of any specific system, and even ares itself.
This allows for easy integration with systems without having to worry about the details of GDB itself.
Managing the server itself, including the underlying TCP connection, is done by ares.
System specific logic is handled via (optional) call-backs that a can be registered,
as well as methods to report events to GDB. The overall design of this server is to be as neutral as possible.
Meaning that things like stopping, stepping and reading memory should not affect the game.
This is done to make sure that games behave the same as if they were running without a debugger, down to the cycle.
## Integration Guide This section describes how to implement the debugger for a system in ares.
It should not be necessary to modify the server itself, or to know much about the GDB protocol.
Simply registering callbacks and reporting events are enough to get the full set of features working.
For a minimal working debugging session, register/memory reads and a way to report the PC are required.
Although implementing as much as possible is recommended to make GDB more stable. Interactions with the server can be split in three categories: - **Hooks:** lets GDB call functions in your ares system (e.g.: memory read) - **Report-functions:** notify GDB about events (e.g.: exceptions) - **Status-functions:** helper to check the GDB status (e.g.: are breakpoints set or not) Hooks can be set via setting the callbacks in `GDB::server.hooks.XXX`.
Report functions are prefixed `GDB::server.reportXXX()`, and status functions a documented here separately.
All hooks/report/status functions can be safely set or called even if the server is not running.
As an example of a fictional system, this is what a memory read could look like: ```cpp GDB::server.hooks.regRead = [](u32 regIdx) { return hex(cpu.readRegister(regIdx), 16, '0'); }; ``` Or the main execution loop: ```cpp while(!endOfFrame && GDB::server.reportPC(cpu.getPC())) { cpu.step(); } ``` For a real reference implementation, you can take a look at the N64 system.
## Hooks ### Memory Read - `read = (u64 address, u32 byteCount) -> string` Reads `byteCount` bytes from `address` and returns them as a hex-string.
Both the hex-encoding / single-byte reads are dictated by the GDB protocol.
It is important to implement this in a neutral way: no exceptions and status changes.
The GDB-client may issue reads from any address at any point while halted.
If not handled properly, this can cause game crashes or different emulation behavior.
If your system emulates cache, make sure to also handle this here.
A read must be able to see the cache, but never cause a flush.
Example response (reading 3 bytes): `A1B200` ### Memory Write - `write = (u64 address, u32 unitSize, u64 value) -> void` Writes `value` of byte-size `unitSize` to `address`.
For example, writing a 32-bit value would issue a call like this: `write(0x80001230, 4, 0x0000000012345678)`.
Contrary to read, this is not required to be neutral, and is allowed to cause exceptions.
If your system emulates cache, make sure to also handle this here.
The write should behave the same as if it was done via a CPU instruction, incl. flushing the cache if needed.
### Normalize Address - `normalizeAddress = (u64 address) -> u64` Normalizes an address into something that makes it comparable.
This is only used for memory-watchpoints, which needs to compare what GDB send to what ares has internally.
If your system has virtual addresses or masks, this should de-virtualize it.
It's OK to not set this function, or to simply return the input untouched.
In case that memory-watchpoint are not working, this is probably the place to fix it.
Example implementation: ```cpp GDB::server.hooks.normalizeAddress = [](u64 address) { return address & 0x0FFF'FFFF; }; ``` ### Register Read - `regRead = (u32 regIdx) -> string` Reads a single register at `regIdx` and returns it as a hex-string.
The size of the hex-string is dictated by the specific architecture.
Same as for memory-read, this must be implemented in a neutral way.
Any invalid register can be returned as zero.
Example response: `00000000000123AB` ### Register Write - `regWrite = (u32 regIdx, u64 regValue) -> bool` Writes the value `regValue` to the register at `regIdx`.
This write is allowed to have side effects.
If the specific register is not writable or doesn't exist, `false` must be returned.
On success, `true` must be returned.
### Register Read (General) - `regReadGeneral = () -> string` Most common way for GDB to read registers, this fetches all registers at once.
The amount and order of registers is dictated by the specific architecture and GDB.
When implementing this, GDB will usually complain if the order/size is incorrect.
Same as for single reads, this must be implemented in a neutral way.
Due to some issues regarding exception handling, you are given the option to return a different PC.
This PC-override can be accessed via `GDB::server.getPcOverride() -> maybe`.
The reasons for that are explained later in `reportSignal()`. Other than that, this can be implemented by looping over `hooks.regRead` and returning a concatenated string.
Example response: `0000000000000000ffffffff8001000000000000000000420000000000000000000000000000000100000`... ### Register Write (General) - `regWriteGeneral = (const string ®Data) -> void` Writes all registers at once, this happens very rarely.
The format of `regData` is the same as the response of `hooks.regReadGeneral`.
Any register that is not writable or doesn't exist can be ignored.
### Emulator Cache - `emuCacheInvalidate = (u64 address) -> void` Should invalidate the emulator's cache at `address`.
This is only necessary if you have a re-compiler or some form of instruction cache.
### Target XML - `targetXML = () -> string` Provides an XML description of the target system.
The XML must not contain any newlines, and should be as short as possible.
If the client has access to an `.elf` file, this will be mostly ignored. Example implementation: ```cpp GDB::server.hooks.targetXML = []() -> string { return "" "mips:4000" ""; }; ``` Documentation: https://sourceware.org/gdb/onlinedocs/gdb/Target-Description-Format.html#Target-Description-Format
## Report-Functions ### Signal `reportSignal(Signal sig, u64 originPC) -> bool` Reports a signal/exception `sig` that occurred at `originPC`.
The architecture specific exception must be mapped to the enum in `Signal`.
As a default, `Signal::TRAP` can be used.
It will return `false` if the exception occurred while the game was already paused.
This can be safely ignored.
Since you may not be able to stop the execution before an exception occurs,
The `originPC` value will be saved until the next time the game is resumed.
An `hooks.regReadGeneral` implementation may use this to temp. return a different PC.
This is done to allow GDB to halt on the causing instruction instead of the exception handler.
If you can halt before an exception occurs, you can ignore this.
### PC `reportPC(u64 pc) -> bool` Sets a new PC, this will internally check for break- and watch-points.
For convenience, it will return `false` if you should halt execution.
If no debugger is running, it will always return `true`.
You must only call this once per step, before the instruction at the given address gets executed.
This also means a return value of `false` should make it halt before the instruction too.
Once halted, it's safe to call this with the same PC each iteration.
If a re-compiler is used, you may not want to call this for every single instruction.
In that case take a look at `hasBreakpoints()` on how to optimize this.
In case you need the information if a halt is required multiple times, use `GDB::server.isHalted()` instead.
### Memory Read `reportMemRead(u64 address, u32 size) -> void` Reports that a memory read occurred at `address` with `size` bytes.
The passed address must be the raw un-normalized address.
This is exclusively used for memory-watchpoints.
No PC override mechanism is provided here, since it's breaks GDB.
### Memory Write `reportMemWrite(u64 address, u32 size) -> void` Exactly the same as `reportMemRead`, but for writes instead.
The new value of that location will be automatically fetched by the client via a memory read,
and is therefore not needed here. ## Status-Functions ### Halted `isHalted() -> bool` Returns if the game should be currently halted or not.
For convenience, the same value gets directly returned from `reportPC`.
### Breakpoints `hasBreakpoints() -> bool` Return `true` if at least one break- or watch-point is set.
If you use a block-based re-compiler, stopping at every instruction may not be possible.
You may use this information to force single-instruction execution in that case.
If it returns false, you can safely resume using the block-based execution again.
### PC Override `getPcOverride() -> maybe` Returns a value if a PC override is active.
As mentioned in `reportSignal()`, this can be used to return a different PC letting GDB halt at the causing instruction.
You can safely call this function multiple times.
Once a single step is taken, or the game is resumed, the override is cleared.
## API Usage This API can also be used without GDB, which allows for more use cases.
For example, you can write automated tooling or custom debugging UIs.
To make access easier, no strict checks are performed.
This means that the handshake protocol is optional, and checksums are not verified. ### TCP TCP connections behave the same way as a GDB session.
The connection is kept open the entire time, and commands are sent sequentially, each waiting for an response before sending the next command. However, it is possible to send commands even if the game is still running, this allows for real-time data access. Keep in minds that the server uses the RDP-commands, which are different from what you would type into a GDB client.
For a list of all commands, see: https://sourceware.org/gdb/onlinedocs/gdb/Packets.html#Packets As an example, reading from memory would look like this: ``` $m8020a504,100#00 ``` This reads 100 bytes from address `0x8020a504`, the `$` and `#` define the message start/end, and the `00` is the checksum (which is not checked). One detail, and security check, is that new connections must send `+` as the first byte in the first payload.
It's also a good idea to send a proper disconnect-command before closing the socket.
Otherwise, the debugger will not accept new connections until a reset or restart occurs.