Skip to content

Commit

Permalink
Add mini post about gbtracer
Browse files Browse the repository at this point in the history
  • Loading branch information
tekknolagi committed Nov 21, 2024
1 parent 09dffd5 commit b3447de
Showing 1 changed file with 170 additions and 0 deletions.
170 changes: 170 additions & 0 deletions _posts/2024-11-21-gbtracer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
title: A multi-emulator Gameboy tracer
layout: post
---

I've been developing Gameboy emulators off and on for some years. They're all
broken in one way or another. Part of my debugging process is to run a test
ROM like the blargg suite. That gives me some positive signal---if the tests
pass, I've done something right---but it doesn't help me narrow down a bug if
they fail.

For this, I've added a `dump()` function to my emulators that logs the state in
some well-known format and then done a side-by-side comparison against a
known-correct log. That's all fine and good but:

* I don't always want this thing dumping state (to stdout)
* If I use someone else's logs, I have to go in and change the log format and
recompile

I figured this would be a good opportunity to write sidecar program,
`gbtracer`. This program `dlopen`s an emulator and then runs it, logging the
state to a tempfile. How does it know the state? The emulator calls some
well-known functions:

```diff
diff --git a/main.cpp b/main.cpp
index 92774d7..4c2c420 100644
--- a/main.cpp
+++ b/main.cpp
@@ -8,6 +8,22 @@
#include "fenster.h"
#include "rom.h"

+extern "C" {
+void gbtracer_open(){}
+void gbtracer_set_h(uint8_t){}
+void gbtracer_set_l(uint8_t){}
+void gbtracer_set_a(uint8_t){}
+void gbtracer_set_b(uint8_t){}
+void gbtracer_set_c(uint8_t){}
+void gbtracer_set_d(uint8_t){}
+void gbtracer_set_e(uint8_t){}
+void gbtracer_set_f(uint8_t){}
+void gbtracer_set_sp(uint16_t){}
+void gbtracer_set_pc(uint16_t){}
+void gbtracer_set_memory(uint8_t*, size_t){}
+void gbtracer_close(){}
+}
+
typedef uint8_t byte;
typedef uint16_t word;
typedef int16_t sword;
@@ -54,6 +70,7 @@ class CPU {
DCHECK(rom_bin_len == 0x100, "ROM too big");
std::memcpy(memory, rom_bin, rom_bin_len);
set_lcd_status(OAM);
+ gbtracer_set_memory(memory, sizeof(memory));
}

void loadCart(const byte* cart, size_t size) {
@@ -628,6 +645,16 @@ class CPU {
void tick() {
word t_before = t_cycles;
byte opcode = imm8();
+ gbtracer_set_a(regs[A]);
+ gbtracer_set_b(regs[B]);
+ gbtracer_set_c(regs[C]);
+ gbtracer_set_d(regs[D]);
+ gbtracer_set_e(regs[E]);
+ gbtracer_set_f(regs[F]);
+ gbtracer_set_h(regs[H]);
+ gbtracer_set_l(regs[L]);
+ gbtracer_set_sp(sp);
+ gbtracer_set_pc(pc);
execute(opcode);
m_cycles++;
timer();
@@ -1142,6 +1169,7 @@ inline std::vector<uint8_t> read_vector_from_disk(std::string file_path) {
}

int main(int argc, char **argv) {
+ gbtracer_open();
CPU cpu;
if (argc == 2) {
const char* filename = argv[1];
@@ -1184,4 +1212,5 @@ int main(int argc, char **argv) {
before_ms = fenster_time();
}
}
+ gbtracer_close();
}
```

These functions are empty in the emulator but have definitions in the tracer.
This feels like it takes advantage of something broken or undefined, so if you
know more about this, please let me know.

```c
#define WRITER(type, name) \
void gbtracer_set_##name(type v) { gbtracer_##name = v; }
WRITER(uint8_t, h);
WRITER(uint8_t, l);
WRITER(uint8_t, a);
WRITER(uint8_t, b);
WRITER(uint8_t, c);
WRITER(uint8_t, d);
WRITER(uint8_t, e);
WRITER(uint8_t, f);
WRITER(uint16_t, sp);
#undef WRITER
```
And when the emulator calls `gbtracer_set_pc`, the tracer writes to a tempfile:
```c
static void flush() {
uint16_t pc = gbtracer_pc;
char flags[4] = "----";
if (gbtracer_f & 0x80) {
flags[0] = 'Z';
}
if (gbtracer_f & 0x40) {
flags[1] = 'N';
}
if (gbtracer_f & 0x20) {
flags[2] = 'H';
}
if (gbtracer_f & 0x10) {
flags[3] = 'C';
}
fprintf(logfile_fp,
"A: %02X B: %02X C: %02X D: %02X E: %02X F: %s H: %02X L: %02X SP: "
"%04X PC: %04X PCMEM: %02X,%02X,%02X,%02X\n",
gbtracer_a, gbtracer_b, gbtracer_c, gbtracer_d, gbtracer_e, flags,
gbtracer_h, gbtracer_l, gbtracer_sp, pc, gbtracer_memory[pc + 0],
gbtracer_memory[pc + 1], gbtracer_memory[pc + 2],
gbtracer_memory[pc + 3]);
}
```

This would be a good cut point where the output format could be made
configurable without recompiling the emulator.

This is what it looks like in action:

```console
$ ./gbtracer ./main.so 01-special.gb
01-special


Passed
gbtracer: /tmp/gbtracer.log.ysINd0
$ head /tmp/gbtracer.log.ysINd0
A: 00 B: 00 C: 00 D: 00 E: 00 F: ---- H: 00 L: 00 SP: 0000 PC: 0000 PCMEM: 31,FE,FF,AF
A: 00 B: 00 C: 00 D: 00 E: 00 F: ---- H: 00 L: 00 SP: FFFE PC: 0001 PCMEM: FE,FF,AF,21
A: 00 B: 00 C: 00 D: 00 E: 00 F: Z--- H: 00 L: 00 SP: FFFE PC: 0004 PCMEM: 21,FF,9F,32
A: 00 B: 00 C: 00 D: 00 E: 00 F: Z--- H: 9F L: FF SP: FFFE PC: 0005 PCMEM: FF,9F,32,CB
A: 00 B: 00 C: 00 D: 00 E: 00 F: Z--- H: 9F L: FE SP: FFFE PC: 0008 PCMEM: CB,7C,20,FB
A: 00 B: 00 C: 00 D: 00 E: 00 F: --H- H: 9F L: FE SP: FFFE PC: 0009 PCMEM: 7C,20,FB,21
A: 00 B: 00 C: 00 D: 00 E: 00 F: --H- H: 9F L: FE SP: FFFE PC: 000B PCMEM: FB,21,26,FF
A: 00 B: 00 C: 00 D: 00 E: 00 F: --H- H: 9F L: FD SP: FFFE PC: 0008 PCMEM: CB,7C,20,FB
A: 00 B: 00 C: 00 D: 00 E: 00 F: --H- H: 9F L: FD SP: FFFE PC: 0009 PCMEM: 7C,20,FB,21
A: 00 B: 00 C: 00 D: 00 E: 00 F: --H- H: 9F L: FD SP: FFFE PC: 000B PCMEM: FB,21,26,FF
$
```

The catch is that you have to make a second build of your emulator as a shared
object with `-fpie -fPIC` flags.

See the [full code](https://gist.github.com/tekknolagi/73e75306d6a04887af0f631c0e919991).

0 comments on commit b3447de

Please sign in to comment.