From 9c114baf99cdb772a706bc5e56588936f9ce4bf4 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 1 Apr 2026 15:10:13 +0200 Subject: [PATCH 01/26] Add a basic gdbstub for lldb remote debugging --- src/main/java/be/ugent/topl/mio/GdbStub.java | 406 ++++++++++++++++++ src/main/kotlin/be/ugent/topl/mio/Main.kt | 11 + .../be/ugent/topl/mio/debugger/Debugger.kt | 10 +- .../be/ugent/topl/mio/woodstate/WOODState.kt | 1 + 4 files changed, 424 insertions(+), 4 deletions(-) create mode 100644 src/main/java/be/ugent/topl/mio/GdbStub.java diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java new file mode 100644 index 0000000..e1793e5 --- /dev/null +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -0,0 +1,406 @@ +package be.ugent.topl.mio; + +import be.ugent.topl.mio.debugger.Debugger; +import be.ugent.topl.mio.sourcemap.SourceMap; +import be.ugent.topl.mio.woodstate.Checkpoint; +import be.ugent.topl.mio.woodstate.Frame; +import be.ugent.topl.mio.woodstate.WOODDumpResponse; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static be.ugent.topl.mio.sourcemap.DwarfSourceMapKt.getDwarfSourcemap; + +public class GdbStub { + private final Debugger debugger; + private final String binaryLocation; + private OutputStream out; + private final SourceMap debugSourceMap; // Remove later, lldb doesn't need this. + + public GdbStub(Debugger debugger, String binaryLocation) { + this.debugger = debugger; + this.binaryLocation = binaryLocation; + debugSourceMap = getDwarfSourcemap(binaryLocation); + + debugger.getBreakpointsListeners().add((pc) -> { + try { + sendPacket(out, "S05"); + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + }); + } + + // Dummy register file (16 x 32-bit) + static int[] regs = new int[16]; + + private static final char[] HEX = "0123456789abcdef".toCharArray(); + private String toHex(byte[] data, int offset, int length) { + StringBuilder sb = new StringBuilder(length * 2); + + for (int i = 0; i < length; i++) { + int b = data[offset + i] & 0xFF; + sb.append(HEX[b >>> 4]); + sb.append(HEX[b & 0x0F]); + } + + return sb.toString(); + } + + private String toHex(long data, int maxLen) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < maxLen; i++) { + long b = (data >> i * 8) & 0xff; + result.append(String.format("%02x", b)); + } + return result.toString(); + } + + private String toHex(long data) { + return toHex(data, 8); + } + + private long toWasmAddr(long addr) { + // We use the upper bits of the address to indicate the type, being part of the object (0) or part of the wasm memory (1). + return addr | 0x1L << 63; + } + + private long getAddrType(long addr) { + return addr >>> 63; + } + + private long stripAddrType(long addr) { + return addr & Long.MAX_VALUE; + } + + private byte[] getCodeSection(String filename) throws IOException { + byte[] wasmBytes = Files.readAllBytes(Path.of(filename)); + int i = 8; // skip header + + while (i < wasmBytes.length) { + int sectionId = wasmBytes[i++] & 0xFF; + + // LEB128 section size + int size = 0; + int shift = 0; + int b; + do { + b = wasmBytes[i++] & 0xFF; + size |= (b & 0x7F) << shift; + shift += 7; + } while ((b & 0x80) != 0); + + if (sectionId == 10) { // code section + return Arrays.copyOfRange(wasmBytes, i, i + size); + } + + i += size; + } + return null; + } + + private String getTriple(String s) { + String hex = s.chars() + .mapToObj(c -> String.format("%02x", c)) + .reduce("", String::concat); + + System.out.println(hex); + return hex; + } + + public WOODDumpResponse getCurrentState() { + return debugger.getCheckpoints().getLast().getSnapshot(); + } + + public void start() throws IOException { + //System.out.println(stripAddrType(0x800000000000fffcL)); // 65532 + debugger.pause(); + + System.out.println(toHex(4508)); + byte[] rawCodeSection = getCodeSection(binaryLocation); + + for (int i = 0; i < regs.length; i++) { + regs[i] = 0x11111111 * (i + 1); + } + + ServerSocket server = new ServerSocket(1234); + System.out.println("Waiting for GDB on port 1234..."); + Socket sock = server.accept(); + System.out.println("GDB connected!"); + + InputStream in = sock.getInputStream(); + out = sock.getOutputStream(); + + while (true) { + String pkt = recvPacket(in, out); + if (pkt == null) { + System.out.println("GDB closed"); + break; + } + System.out.println("<- " + pkt); + + if (pkt.startsWith("m")) { + pkt = pkt.substring(1); + String[] memArgs = pkt.split(","); + long pos = Long.parseUnsignedLong(memArgs[0], 16); + long addrType = getAddrType(pos); + pos = stripAddrType(pos); + long len = Long.parseUnsignedLong(memArgs[1], 16); + log("Read memory " + len + " bytes from " + pos + " with addr type = " + addrType); + + byte[] memory = rawCodeSection; + if (addrType == 1) { + log("Reading from wasm linear memory"); + memory = getCurrentState().getMemory().getBytes(); + } + + // The reply may contain fewer addressable memory units than requested if the server was reading from a trace frame memory and was able to read only part of the region of memory. + if (pos + len >= memory.length) { + System.out.println("Out of bounds for length " + memory.length); + sendPacket(out, "E01"); + continue; + } + + StringBuilder result = new StringBuilder(); + for (int i = 0; i < len; i++) { + // TODO: read from pos + i + // also think about little vs big endian + int index = (int) pos + i; + /*if (index >= memory.length) { + System.out.println("Stop reading, out of bounds"); + break; + }*/ + int b = memory[index]; + result.append(String.format("%02x", b & 0xFF)); + //result.append("00"); + } + sendPacket(out, result.toString()); + + // If memory invalid send E01 + //sendPacket(out, "E01"); + continue; + } + else if (pkt.startsWith("qSupported")) { + sendPacket(out, "qXfer:libraries:read+;vContSupported-;wasm+;"); + continue; + } + else if (pkt.startsWith("qXfer:libraries:read")) { + // https://github.com/v8/v8/blob/main/src/debug/wasm/gdb-server/gdb-remote-util.h#L51 + // For LLDB debugging, an address in a Wasm module code space is represented + // with 64 bits, where the first 32 bits identify the module id: + // +--------------------+--------------------+ + // | module_id | offset | + // +--------------------+--------------------+ + // <----- 32 bit -----> <----- 32 bit -----> + // Offset 0, module id 0 + sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); + continue; + } + // TODO: We can probably remove this: + else if (pkt.startsWith("Hc")) { + sendPacket(out, "OK"); + continue; + } + else if (pkt.startsWith("qWasmLocal:")) { + String[] args = pkt.substring("qWasmLocal:".length()).split(";"); + int frameIdx = Integer.parseInt(args[0]); + int localIdx = Integer.parseInt(args[1]); + log("Reading local " + localIdx + " from frame " + frameIdx); + Frame frame = getCurrentState().getCallstack().get(getCurrentState().getCallstack().size() - frameIdx - 1); + System.out.println(frame); + System.out.println(getCurrentState().getStack()); + int fp = frame.getFp(); + long value = getCurrentState().getStack().get(fp + localIdx).getValue(); + sendPacket(out, toHex(toWasmAddr(value))); // If a pointer on the stack is an address it will be clear it's a wasm memory pointer. + continue; + } + else if (pkt.startsWith("qRegisterInfo0")) { + sendPacket(out, "name:pc;alt-name:pc;bitsize:64;offset:0;encoding:uint;format:hex;set:General Purpose Registers;gcc:16;dwarf:16;generic:pc;"); + continue; + } + else if (pkt.startsWith("p0")) { + sendPacket(out, toHex(getCurrentState().getPc())); + continue; + } + else if (pkt.startsWith("Z")) { + String[] args = pkt.substring(1).split(","); + int type = Integer.parseInt(args[0]); + long addr = Long.parseUnsignedLong(args[1], 16); + int kind = Integer.parseInt(args[2]); + log("Add breakpoint on " + addr); + debugger.addBreakpoint((int) addr); + + try { + for (int i = 0; i < 8; i++) { + log("Breakpoint line " + debugSourceMap.getLineForPc((int) addr + (i-4) * 4) + " " + debugSourceMap.getSourceFileName((int) addr + (i-4) * 4)); + } + } catch(Exception e) {} + + // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. + sendPacket(out, "OK"); + continue; + } + else if (pkt.startsWith("z")) { + // TODO: Fix code duplication + String[] args = pkt.substring(1).split(","); + int type = Integer.parseInt(args[0]); + long addr = Long.parseUnsignedLong(args[1], 16); + int kind = Integer.parseInt(args[2]); + log("Remove breakpoint on " + addr); + debugger.removeBreakpoint((int) addr); + + try { + log("Breakpoint line " + debugSourceMap.getLineForPc((int) addr) + " " + debugSourceMap.getSourceFileName((int) addr)); + } catch(Exception e) {} + + // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. + sendPacket(out, "OK"); + continue; + } + + switch (pkt) { + case "QStartNoAckMode": + sendPacket(out, "OK"); + break; + case "qHostInfo": + //sendPacket(out, "triple:x86_64-pc-linux-gnu;endian:little;ptrsize:8;"); + //sendPacket(out, "cputype:16777228;cpusubtype:3;ostype:darwin;vendor:apple;endian:little;ptrsize:8;hostname:hello;"); + sendPacket(out, "vendor:wamr;ostype:wasi;arch:wasm32;endian:little;ptrsize:4;"); + break; + case "qProcessInfo": + sendPacket(out, "pid:1;parent-pid:1;vendor:wamr;ostype:wasi;arch:wasm32;triple:" + getTriple("wasm32-unknown-unknown-wasm") + ";endian:little;ptrsize:4;"); + //sendPacket(out, "pid:1;parent-pid:1;vendor:wamr;ostype:wasi;arch:wasm32;triple:7761736d33322d77616d722d776173692d7761736d;endian:little;ptrsize:4;"); + break; + case "qGetWorkingDir": + sendPacket(out, "/tmp"); + break; + case "qQueryGDBServer": + sendPacket(out, "PacketSize=4000"); + break; + case "qWasmCallStack:1": // Get the callstack for thread 1. + Checkpoint lastCheckpoint = debugger.getCheckpoints().getLast(); + WOODDumpResponse state = getCurrentState(); + long currentPc = (long) state.getPc(); + String result = toHex(currentPc); + for (int i = 0; i < state.getCallstack().size() - 1; i++) { + result += toHex(0xdead); // TODO: Add other pc's in the callstack + } + log("Callstack size: " + state.getCallstack().size() + " pc = " + currentPc); + sendPacket(out, result); + + try { + log("Current line " + debugSourceMap.getLineForPc(state.getPc()) + " " + debugSourceMap.getSourceFileName(state.getPc())); + } catch(Exception e) {} + + break; + case "qC": // Get thread id + sendPacket(out, "QC 1"); + break; + case "qfThreadInfo": + sendPacket(out, "m 1"); // Active threads start list + break; + case "qsThreadInfo": + sendPacket(out, "l"); // End of list + break; + case "?": + sendPacket(out, "S05"); // SIGTRAP + break; + case "g": + sendPacket(out, encodeRegs()); + break; + case "s": + log("Received step command from lldb"); + debugger.stepInto(); + //sendPacket(out, "T05thread:1;pc:" + toHex(getCurrentState().getPc()) + ";"); + //sendPacket(out, "S05"); + sendPacket(out, "T05thread:1;name:warduino;thread-pcs:" + toHex(getCurrentState().getPc()) + ";00:" + toHex(getCurrentState().getPc()) + ";reason:trace"); + break; + case "c": + // Pretend to run, then stop immediately + debugger.run(); + //sendPacket(out, "S05"); + break; + + default: + System.out.println("Unknown packet: " + pkt); + sendPacket(out, ""); // unsupported + break; + } + } + } + + private void log(String s) { + System.out.print("\u001b[36m"); + System.out.print("[GDBSTUB] "); + System.out.println(s); + System.out.print("\u001b[0m"); + } + + private String recvPacket(InputStream in, OutputStream out) throws IOException { + int c; + + // Wait for '$' + do { + c = in.read(); + if (c == -1) return null; + } while (c != '$'); + + ByteArrayOutputStream payload = new ByteArrayOutputStream(); + + // Read until '#' + while ((c = in.read()) != '#') { + if (c == -1) return null; + payload.write(c); + } + + // Read checksum + int c1 = in.read(); + int c2 = in.read(); + if (c1 == -1 || c2 == -1) return null; + + int received = Integer.parseInt("" + (char)c1 + (char)c2, 16); + byte[] data = payload.toByteArray(); + int computed = checksum(data); + + if (received == computed) { + out.write('+'); // ACK + out.flush(); + return new String(data); + } else { + out.write('-'); // NAK + out.flush(); + return null; + } + } + + private void sendPacket(OutputStream out, String payload) throws IOException { + byte[] data = payload.getBytes(); + int sum = checksum(data); + + String pkt = "$" + payload + "#" + String.format("%02x", sum); + out.write(pkt.getBytes()); + out.flush(); + System.out.println("-> " + pkt); + } + + private int checksum(byte[] data) { + int sum = 0; + for (byte b : data) { + sum = (sum + (b & 0xFF)) & 0xFF; + } + return sum; + } + + private String encodeRegs() { + StringBuilder sb = new StringBuilder(); + for (int r : regs) { + sb.append(String.format("%08x", r)); + } + return sb.toString(); + } +} diff --git a/src/main/kotlin/be/ugent/topl/mio/Main.kt b/src/main/kotlin/be/ugent/topl/mio/Main.kt index eb4b9d5..dbd523c 100644 --- a/src/main/kotlin/be/ugent/topl/mio/Main.kt +++ b/src/main/kotlin/be/ugent/topl/mio/Main.kt @@ -151,6 +151,17 @@ fun main(args: Array) { config.port!! ) } + "gdbstub" -> { + //val wasmFilename = "main.wasm" + val wasmFilename = "test-dbg.wasm" + val debugger = Debugger(ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused")) { + println("Hit breakpoint!") + } + debugger.pause() + debugger.setSnapshotPolicy(Debugger.SnapshotPolicy.Checkpointing()) + val stub = GdbStub(debugger, wasmFilename) + stub.start() + } else -> { println("Invalid option \"${args[0]}\"!") exitProcess(1) diff --git a/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt b/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt index 1983adb..a460615 100644 --- a/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt +++ b/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt @@ -14,8 +14,8 @@ import java.util.* import kotlin.concurrent.thread import kotlin.streams.toList -open class Debugger(private val connection: Connection, start: Boolean = true, private val onHitBreakpoint: (Int) -> Unit = {}) : Closeable, AutoCloseable { - private val requestQueue: Queue = LinkedList() +open class Debugger(private val connection: Connection, start: Boolean = true, onHitBreakpoint: (Int) -> Unit = {}) : Closeable, AutoCloseable { + var breakpointsListeners: MutableList<(Int) -> Unit> = mutableListOf(onHitBreakpoint) var printListener: ((String) -> Unit)? = null private val messageQueue = MessageQueue { for (msg in it) { @@ -39,6 +39,7 @@ open class Debugger(private val connection: Connection, start: Boolean = true, p connection.read(readBuffer) messageQueue.push(String(readBuffer), true) + //println(String(readBuffer)) while (true) { val checkpointMessage = messageQueue.search { val match = Regex("CHECKPOINT (.*)").matchEntire(it.trimEnd('\r')) ?: throw Exception() @@ -97,7 +98,9 @@ open class Debugger(private val connection: Connection, start: Boolean = true, p * incoming messages, so the request would never be completed. */ thread { - onHitBreakpoint(searchAtResult.second) + for (breakpointListener in breakpointsListeners) { + breakpointListener(searchAtResult.second) + } } } } @@ -175,7 +178,6 @@ open class Debugger(private val connection: Connection, start: Boolean = true, p private fun send(code: Int, payload: String = "") { val str = String.format("%02d$payload\n", code) - requestQueue.add(code) print("Sending $str") val write = str.toByteArray() connection.write(write) diff --git a/src/main/kotlin/be/ugent/topl/mio/woodstate/WOODState.kt b/src/main/kotlin/be/ugent/topl/mio/woodstate/WOODState.kt index aa90b6f..e815c64 100644 --- a/src/main/kotlin/be/ugent/topl/mio/woodstate/WOODState.kt +++ b/src/main/kotlin/be/ugent/topl/mio/woodstate/WOODState.kt @@ -156,6 +156,7 @@ data class Checkpoint( val instructions_executed: Int, val fidx_called: Int?, val args: List?, + val returns: List?, val snapshot: WOODDumpResponse ) From 5f9c4e40fe6787588843759b8104ad3cb132f84f Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Wed, 1 Apr 2026 16:41:48 +0200 Subject: [PATCH 02/26] Return fewer bytes if m goes out of bounds + disable reading from type 0 memory to avoid a possible lldb issue The reply may contain fewer addressable memory units than requested if the server was reading from a trace frame memory and was able to read only part of the region of memory. --- src/main/java/be/ugent/topl/mio/GdbStub.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index e1793e5..0b1e79c 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -153,6 +153,13 @@ public void start() throws IOException { long len = Long.parseUnsignedLong(memArgs[1], 16); log("Read memory " + len + " bytes from " + pos + " with addr type = " + addrType); + // TODO: Hack to temporarily prevent lldb from using breakpoints when stepping + // https://github.com/llvm/llvm-project/issues/189960 + if (addrType == 0) { + sendPacket(out, "E01"); + continue; + } + byte[] memory = rawCodeSection; if (addrType == 1) { log("Reading from wasm linear memory"); @@ -160,7 +167,7 @@ public void start() throws IOException { } // The reply may contain fewer addressable memory units than requested if the server was reading from a trace frame memory and was able to read only part of the region of memory. - if (pos + len >= memory.length) { + if (pos >= memory.length) { System.out.println("Out of bounds for length " + memory.length); sendPacket(out, "E01"); continue; @@ -171,10 +178,10 @@ public void start() throws IOException { // TODO: read from pos + i // also think about little vs big endian int index = (int) pos + i; - /*if (index >= memory.length) { + if (index >= memory.length) { System.out.println("Stop reading, out of bounds"); break; - }*/ + } int b = memory[index]; result.append(String.format("%02x", b & 0xFF)); //result.append("00"); @@ -264,9 +271,9 @@ else if (pkt.startsWith("z")) { } switch (pkt) { - case "QStartNoAckMode": + /*case "QStartNoAckMode": sendPacket(out, "OK"); - break; + break;*/ case "qHostInfo": //sendPacket(out, "triple:x86_64-pc-linux-gnu;endian:little;ptrsize:8;"); //sendPacket(out, "cputype:16777228;cpusubtype:3;ostype:darwin;vendor:apple;endian:little;ptrsize:8;hostname:hello;"); From f10e8ec89040752ec5831fa4b1d55a8898fcef1b Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 2 Apr 2026 13:42:23 +0200 Subject: [PATCH 03/26] Also track the offset of the code section + adjustments to toHex to support little endian --- src/main/java/be/ugent/topl/mio/GdbStub.java | 24 ++++++++++++++------ src/main/kotlin/be/ugent/topl/mio/Main.kt | 6 ++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 0b1e79c..5cb16c9 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -52,17 +52,20 @@ private String toHex(byte[] data, int offset, int length) { return sb.toString(); } - private String toHex(long data, int maxLen) { + private String toHex(long data, int maxLen, boolean bigEndian) { StringBuilder result = new StringBuilder(); for (int i = 0; i < maxLen; i++) { long b = (data >> i * 8) & 0xff; + if (!bigEndian) { + b = (data >> (maxLen - i - 1) * 8) & 0xff; + } result.append(String.format("%02x", b)); } return result.toString(); } private String toHex(long data) { - return toHex(data, 8); + return toHex(data, 8, true); } private long toWasmAddr(long addr) { @@ -78,7 +81,9 @@ private long stripAddrType(long addr) { return addr & Long.MAX_VALUE; } - private byte[] getCodeSection(String filename) throws IOException { + record CodeSection(byte[] data, int offset) {} + + private CodeSection getCodeSection(String filename) throws IOException { byte[] wasmBytes = Files.readAllBytes(Path.of(filename)); int i = 8; // skip header @@ -96,7 +101,8 @@ private byte[] getCodeSection(String filename) throws IOException { } while ((b & 0x80) != 0); if (sectionId == 10) { // code section - return Arrays.copyOfRange(wasmBytes, i, i + size); + System.out.println("Found code section at address " + i); + return new CodeSection(Arrays.copyOfRange(wasmBytes, i, i + size), i); } i += size; @@ -122,7 +128,7 @@ public void start() throws IOException { debugger.pause(); System.out.println(toHex(4508)); - byte[] rawCodeSection = getCodeSection(binaryLocation); + CodeSection codeSection = getCodeSection(binaryLocation); for (int i = 0; i < regs.length; i++) { regs[i] = 0x11111111 * (i + 1); @@ -160,7 +166,7 @@ public void start() throws IOException { continue; } - byte[] memory = rawCodeSection; + byte[] memory = codeSection.data; if (addrType == 1) { log("Reading from wasm linear memory"); memory = getCurrentState().getMemory().getBytes(); @@ -205,7 +211,10 @@ else if (pkt.startsWith("qXfer:libraries:read")) { // +--------------------+--------------------+ // <----- 32 bit -----> <----- 32 bit -----> // Offset 0, module id 0 - sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); + // toHex(codeSection.offset, 4) + //"0x00044444" + //sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); + sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); continue; } // TODO: We can probably remove this: @@ -292,6 +301,7 @@ else if (pkt.startsWith("z")) { case "qWasmCallStack:1": // Get the callstack for thread 1. Checkpoint lastCheckpoint = debugger.getCheckpoints().getLast(); WOODDumpResponse state = getCurrentState(); + //long currentPc = (long) state.getPc() - codeSection.offset; long currentPc = (long) state.getPc(); String result = toHex(currentPc); for (int i = 0; i < state.getCallstack().size() - 1; i++) { diff --git a/src/main/kotlin/be/ugent/topl/mio/Main.kt b/src/main/kotlin/be/ugent/topl/mio/Main.kt index dbd523c..7824862 100644 --- a/src/main/kotlin/be/ugent/topl/mio/Main.kt +++ b/src/main/kotlin/be/ugent/topl/mio/Main.kt @@ -153,10 +153,8 @@ fun main(args: Array) { } "gdbstub" -> { //val wasmFilename = "main.wasm" - val wasmFilename = "test-dbg.wasm" - val debugger = Debugger(ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused")) { - println("Hit breakpoint!") - } + val wasmFilename = "tmp/test-dbg.wasm" + val debugger = Debugger(ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused")) debugger.pause() debugger.setSnapshotPolicy(Debugger.SnapshotPolicy.Checkpointing()) val stub = GdbStub(debugger, wasmFilename) From bd3dbe418e18d3ba45f70041b2eebdfb1d58d514 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 2 Apr 2026 14:41:47 +0200 Subject: [PATCH 04/26] Allow reading outside of code section (needs refactor, since the code section is now just the full module) I also am unsure if LLDB actually cares about the section address, seems to not really matter much? --- src/main/java/be/ugent/topl/mio/GdbStub.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 5cb16c9..7876058 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -102,7 +102,8 @@ private CodeSection getCodeSection(String filename) throws IOException { if (sectionId == 10) { // code section System.out.println("Found code section at address " + i); - return new CodeSection(Arrays.copyOfRange(wasmBytes, i, i + size), i); + //return new CodeSection(Arrays.copyOfRange(wasmBytes, i, i + size), i); + return new CodeSection(wasmBytes, i); } i += size; @@ -161,20 +162,23 @@ public void start() throws IOException { // TODO: Hack to temporarily prevent lldb from using breakpoints when stepping // https://github.com/llvm/llvm-project/issues/189960 - if (addrType == 0) { + /*if (addrType == 0) { sendPacket(out, "E01"); continue; - } + }*/ byte[] memory = codeSection.data; if (addrType == 1) { log("Reading from wasm linear memory"); memory = getCurrentState().getMemory().getBytes(); } + else { + //pos -= codeSection.offset; + } // The reply may contain fewer addressable memory units than requested if the server was reading from a trace frame memory and was able to read only part of the region of memory. - if (pos >= memory.length) { - System.out.println("Out of bounds for length " + memory.length); + if (pos >= memory.length || pos < 0) { + System.out.println("Pos " + pos + " out of bounds for length " + memory.length); sendPacket(out, "E01"); continue; } @@ -214,7 +218,8 @@ else if (pkt.startsWith("qXfer:libraries:read")) { // toHex(codeSection.offset, 4) //"0x00044444" //sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); - sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); + sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); + //sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); continue; } // TODO: We can probably remove this: From 84af10e8678b9be22f1a137708fc1117a7c4cc37 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 2 Apr 2026 14:57:36 +0200 Subject: [PATCH 05/26] Seems like we just don't need the code section and the offset --- src/main/java/be/ugent/topl/mio/GdbStub.java | 43 ++------------------ 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 7876058..caa65f2 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -11,7 +11,6 @@ import java.net.Socket; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; import static be.ugent.topl.mio.sourcemap.DwarfSourceMapKt.getDwarfSourcemap; @@ -81,36 +80,6 @@ private long stripAddrType(long addr) { return addr & Long.MAX_VALUE; } - record CodeSection(byte[] data, int offset) {} - - private CodeSection getCodeSection(String filename) throws IOException { - byte[] wasmBytes = Files.readAllBytes(Path.of(filename)); - int i = 8; // skip header - - while (i < wasmBytes.length) { - int sectionId = wasmBytes[i++] & 0xFF; - - // LEB128 section size - int size = 0; - int shift = 0; - int b; - do { - b = wasmBytes[i++] & 0xFF; - size |= (b & 0x7F) << shift; - shift += 7; - } while ((b & 0x80) != 0); - - if (sectionId == 10) { // code section - System.out.println("Found code section at address " + i); - //return new CodeSection(Arrays.copyOfRange(wasmBytes, i, i + size), i); - return new CodeSection(wasmBytes, i); - } - - i += size; - } - return null; - } - private String getTriple(String s) { String hex = s.chars() .mapToObj(c -> String.format("%02x", c)) @@ -125,11 +94,9 @@ public WOODDumpResponse getCurrentState() { } public void start() throws IOException { - //System.out.println(stripAddrType(0x800000000000fffcL)); // 65532 debugger.pause(); - System.out.println(toHex(4508)); - CodeSection codeSection = getCodeSection(binaryLocation); + byte[] wasmData = Files.readAllBytes(Path.of(binaryLocation)); for (int i = 0; i < regs.length; i++) { regs[i] = 0x11111111 * (i + 1); @@ -167,7 +134,7 @@ public void start() throws IOException { continue; }*/ - byte[] memory = codeSection.data; + byte[] memory = wasmData; if (addrType == 1) { log("Reading from wasm linear memory"); memory = getCurrentState().getMemory().getBytes(); @@ -215,11 +182,7 @@ else if (pkt.startsWith("qXfer:libraries:read")) { // +--------------------+--------------------+ // <----- 32 bit -----> <----- 32 bit -----> // Offset 0, module id 0 - // toHex(codeSection.offset, 4) - //"0x00044444" - //sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); - sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); - //sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); + sendPacket(out, String.format("l
", new File(binaryLocation).getAbsolutePath())); continue; } // TODO: We can probably remove this: From 8122405eba565321634aa422b9f65bd4c0ee0391 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 2 Apr 2026 16:55:09 +0200 Subject: [PATCH 06/26] Correctly put return addresses in the callstack --- src/main/java/be/ugent/topl/mio/GdbStub.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index caa65f2..28fa254 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -267,15 +267,15 @@ else if (pkt.startsWith("z")) { sendPacket(out, "PacketSize=4000"); break; case "qWasmCallStack:1": // Get the callstack for thread 1. - Checkpoint lastCheckpoint = debugger.getCheckpoints().getLast(); WOODDumpResponse state = getCurrentState(); - //long currentPc = (long) state.getPc() - codeSection.offset; - long currentPc = (long) state.getPc(); - String result = toHex(currentPc); - for (int i = 0; i < state.getCallstack().size() - 1; i++) { - result += toHex(0xdead); // TODO: Add other pc's in the callstack + String result = toHex(state.getPc()); + for (int i = state.getCallstack().size() - 1; i >= 0; i--) { + // Only functions are real callstack elements: + Frame f = state.getCallstack().get(i); + if (f.getType() == 0) { + result += toHex(f.getRa()); + } } - log("Callstack size: " + state.getCallstack().size() + " pc = " + currentPc); sendPacket(out, result); try { From 37d8cd385563739481e03375a98e2a51fe596fe0 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 3 Apr 2026 16:43:18 +0200 Subject: [PATCH 07/26] Implement D packet Maybe in the future we might want to support keeping the connection open when detaching so you can keep it running and transfer debugging to a different machine. --- src/main/java/be/ugent/topl/mio/GdbStub.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 28fa254..74ba290 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -110,7 +110,9 @@ public void start() throws IOException { InputStream in = sock.getInputStream(); out = sock.getOutputStream(); - while (true) { + boolean attached = true; + + while (attached) { String pkt = recvPacket(in, out); if (pkt == null) { System.out.println("GDB closed"); @@ -246,6 +248,11 @@ else if (pkt.startsWith("z")) { sendPacket(out, "OK"); continue; } + else if (pkt.startsWith("stackInfo")) { + //debugger.stepBack(1, wasmData); + sendPacket(out, getCurrentState().getStack().toString()); + continue; + } switch (pkt) { /*case "QStartNoAckMode": @@ -310,7 +317,12 @@ else if (pkt.startsWith("z")) { debugger.run(); //sendPacket(out, "S05"); break; - + case "D": + attached = false; + log("Detach from target"); + debugger.close(); + sendPacket(out, "OK"); + break; default: System.out.println("Unknown packet: " + pkt); sendPacket(out, ""); // unsupported From 26b0650b0c4c49fc426076a1d6717cbcabe4d94e Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 3 Apr 2026 16:47:16 +0200 Subject: [PATCH 08/26] Return pc register when requesting registers --- src/main/java/be/ugent/topl/mio/GdbStub.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 74ba290..0a1493c 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -2,7 +2,6 @@ import be.ugent.topl.mio.debugger.Debugger; import be.ugent.topl.mio.sourcemap.SourceMap; -import be.ugent.topl.mio.woodstate.Checkpoint; import be.ugent.topl.mio.woodstate.Frame; import be.ugent.topl.mio.woodstate.WOODDumpResponse; @@ -35,9 +34,6 @@ public GdbStub(Debugger debugger, String binaryLocation) { }); } - // Dummy register file (16 x 32-bit) - static int[] regs = new int[16]; - private static final char[] HEX = "0123456789abcdef".toCharArray(); private String toHex(byte[] data, int offset, int length) { StringBuilder sb = new StringBuilder(length * 2); @@ -98,10 +94,6 @@ public void start() throws IOException { byte[] wasmData = Files.readAllBytes(Path.of(binaryLocation)); - for (int i = 0; i < regs.length; i++) { - regs[i] = 0x11111111 * (i + 1); - } - ServerSocket server = new ServerSocket(1234); System.out.println("Waiting for GDB on port 1234..."); Socket sock = server.accept(); @@ -395,9 +387,7 @@ private int checksum(byte[] data) { private String encodeRegs() { StringBuilder sb = new StringBuilder(); - for (int r : regs) { - sb.append(String.format("%08x", r)); - } + sb.append(String.format("%08x", getCurrentState().getPc())); return sb.toString(); } } From 93707b29653e9677ae8ef1f6dc370143a38ebea8 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 3 Apr 2026 17:12:08 +0200 Subject: [PATCH 09/26] Support pausing the execution (0x03) --- src/main/java/be/ugent/topl/mio/GdbStub.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 0a1493c..489e079 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -302,13 +302,17 @@ else if (pkt.startsWith("stackInfo")) { debugger.stepInto(); //sendPacket(out, "T05thread:1;pc:" + toHex(getCurrentState().getPc()) + ";"); //sendPacket(out, "S05"); - sendPacket(out, "T05thread:1;name:warduino;thread-pcs:" + toHex(getCurrentState().getPc()) + ";00:" + toHex(getCurrentState().getPc()) + ";reason:trace"); + sendStopPacket(out, "05"); break; case "c": // Pretend to run, then stop immediately debugger.run(); //sendPacket(out, "S05"); break; + case "pause": + debugger.pause(); + sendStopPacket(out, "02"); + break; case "D": attached = false; log("Detach from target"); @@ -323,6 +327,10 @@ else if (pkt.startsWith("stackInfo")) { } } + private void sendStopPacket(OutputStream out, String signal) throws IOException { + sendPacket(out, "T" + signal + "thread:1;name:warduino;thread-pcs:" + toHex(getCurrentState().getPc()) + ";00:" + toHex(getCurrentState().getPc()) + ";reason:trace"); + } + private void log(String s) { System.out.print("\u001b[36m"); System.out.print("[GDBSTUB] "); @@ -337,7 +345,11 @@ private String recvPacket(InputStream in, OutputStream out) throws IOException { do { c = in.read(); if (c == -1) return null; - } while (c != '$'); + } while (c != '$' && c != 0x03); // 0x03 == pause request + + if (c == 0x03) { + return "pause"; + } ByteArrayOutputStream payload = new ByteArrayOutputStream(); From 958ce3d3eaeeca6faca61794d99bcc03cd344b14 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 3 Apr 2026 21:40:22 +0200 Subject: [PATCH 10/26] Rename gdbstub to gdb-server and user args[1] as filename + minor cleanup --- src/main/java/be/ugent/topl/mio/GdbStub.java | 6 +----- src/main/kotlin/be/ugent/topl/mio/Main.kt | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 489e079..a01ab47 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -300,14 +300,10 @@ else if (pkt.startsWith("stackInfo")) { case "s": log("Received step command from lldb"); debugger.stepInto(); - //sendPacket(out, "T05thread:1;pc:" + toHex(getCurrentState().getPc()) + ";"); - //sendPacket(out, "S05"); sendStopPacket(out, "05"); break; case "c": - // Pretend to run, then stop immediately debugger.run(); - //sendPacket(out, "S05"); break; case "pause": debugger.pause(); @@ -321,7 +317,7 @@ else if (pkt.startsWith("stackInfo")) { break; default: System.out.println("Unknown packet: " + pkt); - sendPacket(out, ""); // unsupported + sendPacket(out, ""); break; } } diff --git a/src/main/kotlin/be/ugent/topl/mio/Main.kt b/src/main/kotlin/be/ugent/topl/mio/Main.kt index 7824862..b3bf99a 100644 --- a/src/main/kotlin/be/ugent/topl/mio/Main.kt +++ b/src/main/kotlin/be/ugent/topl/mio/Main.kt @@ -151,9 +151,9 @@ fun main(args: Array) { config.port!! ) } - "gdbstub" -> { - //val wasmFilename = "main.wasm" - val wasmFilename = "tmp/test-dbg.wasm" + "debug-server" -> { + expectNArguments(args, 2) + val wasmFilename = args[1] val debugger = Debugger(ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused")) debugger.pause() debugger.setSnapshotPolicy(Debugger.SnapshotPolicy.Checkpointing()) From b4db32b1a12df880a747165c11379be4d4f1a523 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 3 Apr 2026 22:21:23 +0200 Subject: [PATCH 11/26] Use checkpoint.fidx_called to know if a primitive was called instead of metadata We already have this data + it's more accurate than the metadata which restores more snapshots than it should sometimes. For example: 0000d2 func[4]
: 0000d3: 23 02 | global.get 2 0000d5: 23 01 | global.get 1 0000d7: 10 00 | call 0 0000d9: 23 03 | global.get 3 0000db: 23 01 | global.get 1 0000dd: 10 00 | call 0 0000df: 23 04 | global.get 4 0000e1: 23 00 | global.get 0 0000e3: 10 00 | call 0 0000e5: 02 40 | block 0000e7: 03 40 | loop 0000e9: 41 01 | i32.const 1 0000eb: 04 40 | if 0000ed: 23 04 | global.get 4 0000ef: 10 01 | call 1 0000f1: 41 c8 01 | i32.const 200 0000f4: 4a | i32.gt_s 0000f5: 04 40 | if 0000f7: 23 02 | global.get 2 0000f9: 41 01 | i32.const 1 0000fb: 10 02 | call 2 0000fd: 23 03 | global.get 3 0000ff: 41 00 | i32.const 0 000101: 10 02 | call 2 000103: 05 | else 000104: 23 02 | global.get 2 000106: 41 00 | i32.const 0 000108: 10 02 | call 2 00010a: 23 03 | global.get 3 00010c: 41 01 | i32.const 1 00010e: 10 02 | call 2 000110: 0b | end 000111: 41 0a | i32.const 10 000113: 10 03 | call 3 000115: 0c 01 | br 1 000117: 0b | end 000118: 0b | end 000119: 00 | unreachable 00011a: 0b | end 00011b: 00 | unreachable 00011c: 0b | end After pc = 0x000101 you have an else at 0x000103, it then jumps to end and 0x000110. This end is not after this primitive call, but it's still restored if we step back in this scenario. This end is after pc = 0x00010e which is a primitive, but we should only restore at 0x000110 in this particular path. So by using the runtime data instead of the ahead of time metadata we actually restore less snapshots so it's more effcient + no need for metadata. --- src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt | 10 +++++----- .../be/ugent/topl/mio/debugger/MultiverseDebugger.kt | 5 ++--- .../kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt | 6 +++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt b/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt index a460615..7f5f6e5 100644 --- a/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt +++ b/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt @@ -223,14 +223,14 @@ open class Debugger(private val connection: Connection, start: Boolean = true, o return checkpoints.size > 1 } - fun stepBackUntil(binaryInfo: WasmInfo, cond: (WOODDumpResponse) -> Boolean) { - stepBack(1, binaryInfo) {} + fun stepBackUntil(cond: (WOODDumpResponse) -> Boolean) { + stepBack() while (!cond(checkpoints.last()!!.snapshot)) { if (!canStepBack()) { System.err.println("WARNING: Can't go back further!") return } - stepBack(1, binaryInfo) + stepBack() } } @@ -263,7 +263,7 @@ open class Debugger(private val connection: Connection, start: Boolean = true, o println("count = ${checkpoints.size}") } - open fun stepBack(n: Int, binaryInfo: WasmInfo, stepDone: () -> Unit = {}) { + open fun stepBack(n: Int = 1, stepDone: () -> Unit = {}) { if (n == 0) { return } @@ -271,7 +271,7 @@ open class Debugger(private val connection: Connection, start: Boolean = true, o val currentState = checkpoints.removeLast() // Remove current state, we don't need to restore this, we are already in this state. val nSnapshots = checkpoints.subList(checkpoints.size - n, checkpoints.size).toList() for (checkpoint in nSnapshots.reversed()) { - if (checkpoint != null && (checkpoint.snapshot.pc in binaryInfo.after_primitive_calls || nSnapshots.first() == checkpoint)) { + if (checkpoint != null && (checkpoint.fidx_called != null || nSnapshots.first() == checkpoint)) { //if (snapshot != null) { println("Snapshot to ${checkpoint.snapshot.pc}") val s = checkpoint.snapshot diff --git a/src/main/kotlin/be/ugent/topl/mio/debugger/MultiverseDebugger.kt b/src/main/kotlin/be/ugent/topl/mio/debugger/MultiverseDebugger.kt index edff384..062f80b 100644 --- a/src/main/kotlin/be/ugent/topl/mio/debugger/MultiverseDebugger.kt +++ b/src/main/kotlin/be/ugent/topl/mio/debugger/MultiverseDebugger.kt @@ -1,7 +1,6 @@ package be.ugent.topl.mio.debugger import WasmBinary -import WasmInfo import be.ugent.topl.mio.concolic.analyse import be.ugent.topl.mio.concolic.processPaths import be.ugent.topl.mio.connections.Connection @@ -202,12 +201,12 @@ class MultiverseDebugger( }*/ } - override fun stepBack(n: Int, binaryInfo: WasmInfo, stepDone: () -> Unit) { + override fun stepBack(n: Int, stepDone: () -> Unit) { var destinationNode = graph.currentNode for (i in 0 ..< n) { destinationNode = destinationNode.parent!! } - super.stepBack(n, binaryInfo, stepDone) + super.stepBack(n, stepDone) graph.currentNode = destinationNode graphUpdated() diff --git a/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt b/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt index 652a51c..b4fc55b 100644 --- a/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt +++ b/src/main/kotlin/be/ugent/topl/mio/ui/InteractiveDebugger.kt @@ -136,7 +136,7 @@ class InteractiveDebugger( stepBackButton.addActionListener { println("Step back") //debugger.stepBack() - debugger.stepBack(1, binaryInfo) {} + debugger.stepBack() updateStepBackButton() updatePcLabel() } @@ -193,7 +193,7 @@ class InteractiveDebugger( } catch(re: RuntimeException) { System.err.println("WARNING: " + re.message) } - debugger.stepBackUntil(binaryInfo) { + debugger.stepBackUntil { try { sourceMapping.getLineForPc(it.pc!!) != startLine } catch(re: RuntimeException) { @@ -541,7 +541,7 @@ class MultiversePanel(private val multiverseDebugger: MultiverseDebugger, config val totalLength = backwardsLength + forwardsLength val backwardPath = graphPanel.selectedPath!!.first.toMutableList() var finishedSteps = 0 - multiverseDebugger.stepBack(backwardPath.size, multiverseDebugger.wasmBinary.metadata) { + multiverseDebugger.stepBack(backwardPath.size) { graphPanel.completedPath.add(backwardPath.removeFirst()) graphPanel.repaint() val remaining = forwardsLength + backwardPath.size From 14458e003cf74775cce21e221c87be85c3ce5f69 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 3 Apr 2026 22:28:29 +0200 Subject: [PATCH 12/26] Support sb (step back) in the gdbstub However, LLDB does not have a stepback command so it can only be used by "process plugin packet send sb" or a plugin that adds such command. --- src/main/java/be/ugent/topl/mio/GdbStub.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index a01ab47..072113b 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -302,6 +302,10 @@ else if (pkt.startsWith("stackInfo")) { debugger.stepInto(); sendStopPacket(out, "05"); break; + case "sb": + debugger.stepBack(1, () -> null); + sendStopPacket(out, "05"); + break; case "c": debugger.run(); break; From 29dcf5cb09a75f80f756e643c27a6f7d7460f65d Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Sat, 4 Apr 2026 14:20:34 +0200 Subject: [PATCH 13/26] Fix qWasmLocal to use only function frames as lldb expects WARDuino also has frames for non-functions, for example blocks in wasm. --- src/main/java/be/ugent/topl/mio/GdbStub.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 072113b..b777ef9 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -10,6 +10,8 @@ import java.net.Socket; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import static be.ugent.topl.mio.sourcemap.DwarfSourceMapKt.getDwarfSourcemap; @@ -189,11 +191,12 @@ else if (pkt.startsWith("qWasmLocal:")) { int frameIdx = Integer.parseInt(args[0]); int localIdx = Integer.parseInt(args[1]); log("Reading local " + localIdx + " from frame " + frameIdx); - Frame frame = getCurrentState().getCallstack().get(getCurrentState().getCallstack().size() - frameIdx - 1); + Frame frame = getCallStack(getCurrentState()).get(frameIdx); + System.out.println(getCallStack(getCurrentState())); System.out.println(frame); System.out.println(getCurrentState().getStack()); int fp = frame.getFp(); - long value = getCurrentState().getStack().get(fp + localIdx).getValue(); + long value = getCurrentState().getStack().get(fp + localIdx + 1).getValue(); sendPacket(out, toHex(toWasmAddr(value))); // If a pointer on the stack is an address it will be clear it's a wasm memory pointer. continue; } @@ -402,4 +405,15 @@ private String encodeRegs() { sb.append(String.format("%08x", getCurrentState().getPc())); return sb.toString(); } + + private List getCallStack(WOODDumpResponse state) { + List callStack = new ArrayList(); + for (int i = state.getCallstack().size() - 1; i >= 0; i--) { + Frame f = state.getCallstack().get(i); + if (f.getType() == 0) { + callStack.add(state.getCallstack().get(i)); + } + } + return callStack; + } } From c653d5c4cd6b866d6f0cafc9d1ed2359752eaa2d Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Sun, 5 Apr 2026 11:10:36 +0200 Subject: [PATCH 14/26] Use --port/-p to start debug server --- src/main/java/be/ugent/topl/mio/GdbStub.java | 6 +++--- src/main/kotlin/be/ugent/topl/mio/Main.kt | 22 ++++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index b777ef9..249c641 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -91,13 +91,13 @@ public WOODDumpResponse getCurrentState() { return debugger.getCheckpoints().getLast().getSnapshot(); } - public void start() throws IOException { + public void start(int port) throws IOException { debugger.pause(); byte[] wasmData = Files.readAllBytes(Path.of(binaryLocation)); - ServerSocket server = new ServerSocket(1234); - System.out.println("Waiting for GDB on port 1234..."); + ServerSocket server = new ServerSocket(port); + System.out.printf("Waiting for GDB on port %d...", port); Socket sock = server.accept(); System.out.println("GDB connected!"); diff --git a/src/main/kotlin/be/ugent/topl/mio/Main.kt b/src/main/kotlin/be/ugent/topl/mio/Main.kt index b3bf99a..40f633b 100644 --- a/src/main/kotlin/be/ugent/topl/mio/Main.kt +++ b/src/main/kotlin/be/ugent/topl/mio/Main.kt @@ -52,6 +52,19 @@ fun main(args: Array) { } expectNArguments(args, 1) val config = DebuggerConfig() + + if (args[0].startsWith("-p=") || args[0].startsWith("--port=")) { + expectNArguments(args, 2) + val wasmFilename = args[1] + val port = args[0].split("=")[1].toInt() + val debugger = Debugger(ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused")) + debugger.pause() + debugger.setSnapshotPolicy(Debugger.SnapshotPolicy.Checkpointing()) + val stub = GdbStub(debugger, wasmFilename) + stub.start(port) + return + } + when (args[0]) { "debug" -> { expectNArguments(args, 2) @@ -151,15 +164,6 @@ fun main(args: Array) { config.port!! ) } - "debug-server" -> { - expectNArguments(args, 2) - val wasmFilename = args[1] - val debugger = Debugger(ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused")) - debugger.pause() - debugger.setSnapshotPolicy(Debugger.SnapshotPolicy.Checkpointing()) - val stub = GdbStub(debugger, wasmFilename) - stub.start() - } else -> { println("Invalid option \"${args[0]}\"!") exitProcess(1) From eab99e051a8cc4d758b7d21fa50163ca4f14ea63 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Sun, 5 Apr 2026 11:50:44 +0200 Subject: [PATCH 15/26] Use logback and slf4j to have proper logging in the GdbStub --- build.gradle.kts | 3 + src/main/java/be/ugent/topl/mio/GdbStub.java | 66 ++++++++------------ 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 34c96e2..81cde81 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,6 +29,9 @@ dependencies { // Needed for AssemblyScript source mapping: //implementation("com.atlassian.sourcemap:sourcemap:2.0.0") implementation("com.google.code.gson:gson:2.11.0") + + // Logging + implementation("ch.qos.logback:logback-classic:1.5.32") } tasks.test { diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 249c641..c262d8b 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -4,6 +4,8 @@ import be.ugent.topl.mio.sourcemap.SourceMap; import be.ugent.topl.mio.woodstate.Frame; import be.ugent.topl.mio.woodstate.WOODDumpResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.*; import java.net.ServerSocket; @@ -19,12 +21,13 @@ public class GdbStub { private final Debugger debugger; private final String binaryLocation; private OutputStream out; + private final Logger logger = LoggerFactory.getLogger(GdbStub.class); private final SourceMap debugSourceMap; // Remove later, lldb doesn't need this. public GdbStub(Debugger debugger, String binaryLocation) { this.debugger = debugger; this.binaryLocation = binaryLocation; - debugSourceMap = getDwarfSourcemap(binaryLocation); + this.debugSourceMap = getDwarfSourcemap(binaryLocation); debugger.getBreakpointsListeners().add((pc) -> { try { @@ -82,8 +85,6 @@ private String getTriple(String s) { String hex = s.chars() .mapToObj(c -> String.format("%02x", c)) .reduce("", String::concat); - - System.out.println(hex); return hex; } @@ -97,9 +98,9 @@ public void start(int port) throws IOException { byte[] wasmData = Files.readAllBytes(Path.of(binaryLocation)); ServerSocket server = new ServerSocket(port); - System.out.printf("Waiting for GDB on port %d...", port); + logger.info("Waiting for LLDB connection on port {}...", port); Socket sock = server.accept(); - System.out.println("GDB connected!"); + logger.info("LLDB connected!"); InputStream in = sock.getInputStream(); out = sock.getOutputStream(); @@ -109,10 +110,10 @@ public void start(int port) throws IOException { while (attached) { String pkt = recvPacket(in, out); if (pkt == null) { - System.out.println("GDB closed"); + logger.info("Connection closed"); break; } - System.out.println("<- " + pkt); + logger.trace("<- {}", pkt); if (pkt.startsWith("m")) { pkt = pkt.substring(1); @@ -121,7 +122,7 @@ public void start(int port) throws IOException { long addrType = getAddrType(pos); pos = stripAddrType(pos); long len = Long.parseUnsignedLong(memArgs[1], 16); - log("Read memory " + len + " bytes from " + pos + " with addr type = " + addrType); + logger.info("Read memory {} bytes from {} with addr type = {}", len, pos, addrType); // TODO: Hack to temporarily prevent lldb from using breakpoints when stepping // https://github.com/llvm/llvm-project/issues/189960 @@ -132,7 +133,7 @@ public void start(int port) throws IOException { byte[] memory = wasmData; if (addrType == 1) { - log("Reading from wasm linear memory"); + logger.info("Reading from wasm linear memory"); memory = getCurrentState().getMemory().getBytes(); } else { @@ -141,7 +142,7 @@ public void start(int port) throws IOException { // The reply may contain fewer addressable memory units than requested if the server was reading from a trace frame memory and was able to read only part of the region of memory. if (pos >= memory.length || pos < 0) { - System.out.println("Pos " + pos + " out of bounds for length " + memory.length); + logger.warn("Pos {} out of bounds for length {}", pos, memory.length); sendPacket(out, "E01"); continue; } @@ -152,17 +153,13 @@ public void start(int port) throws IOException { // also think about little vs big endian int index = (int) pos + i; if (index >= memory.length) { - System.out.println("Stop reading, out of bounds"); + logger.info("Stop reading, out of bounds"); break; } int b = memory[index]; result.append(String.format("%02x", b & 0xFF)); - //result.append("00"); } sendPacket(out, result.toString()); - - // If memory invalid send E01 - //sendPacket(out, "E01"); continue; } else if (pkt.startsWith("qSupported")) { @@ -190,11 +187,11 @@ else if (pkt.startsWith("qWasmLocal:")) { String[] args = pkt.substring("qWasmLocal:".length()).split(";"); int frameIdx = Integer.parseInt(args[0]); int localIdx = Integer.parseInt(args[1]); - log("Reading local " + localIdx + " from frame " + frameIdx); + logger.info("Reading local {} from frame {}", localIdx, frameIdx); Frame frame = getCallStack(getCurrentState()).get(frameIdx); - System.out.println(getCallStack(getCurrentState())); - System.out.println(frame); - System.out.println(getCurrentState().getStack()); + logger.trace("{}", getCallStack(getCurrentState())); + logger.trace("{}", frame); + logger.trace("{}", getCurrentState().getStack()); int fp = frame.getFp(); long value = getCurrentState().getStack().get(fp + localIdx + 1).getValue(); sendPacket(out, toHex(toWasmAddr(value))); // If a pointer on the stack is an address it will be clear it's a wasm memory pointer. @@ -213,14 +210,12 @@ else if (pkt.startsWith("Z")) { int type = Integer.parseInt(args[0]); long addr = Long.parseUnsignedLong(args[1], 16); int kind = Integer.parseInt(args[2]); - log("Add breakpoint on " + addr); + logger.info("Add breakpoint on {}", addr); debugger.addBreakpoint((int) addr); try { - for (int i = 0; i < 8; i++) { - log("Breakpoint line " + debugSourceMap.getLineForPc((int) addr + (i-4) * 4) + " " + debugSourceMap.getSourceFileName((int) addr + (i-4) * 4)); - } - } catch(Exception e) {} + logger.info("Breakpoint line {} {}", debugSourceMap.getLineForPc((int) addr), debugSourceMap.getSourceFileName((int) addr)); + } catch(Exception _) {} // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. sendPacket(out, "OK"); @@ -232,11 +227,11 @@ else if (pkt.startsWith("z")) { int type = Integer.parseInt(args[0]); long addr = Long.parseUnsignedLong(args[1], 16); int kind = Integer.parseInt(args[2]); - log("Remove breakpoint on " + addr); + logger.info("Remove breakpoint on {}", addr); debugger.removeBreakpoint((int) addr); try { - log("Breakpoint line " + debugSourceMap.getLineForPc((int) addr) + " " + debugSourceMap.getSourceFileName((int) addr)); + logger.info("Breakpoint line {} {}", debugSourceMap.getLineForPc((int) addr), debugSourceMap.getSourceFileName((int) addr)); } catch(Exception e) {} // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. @@ -254,8 +249,6 @@ else if (pkt.startsWith("stackInfo")) { sendPacket(out, "OK"); break;*/ case "qHostInfo": - //sendPacket(out, "triple:x86_64-pc-linux-gnu;endian:little;ptrsize:8;"); - //sendPacket(out, "cputype:16777228;cpusubtype:3;ostype:darwin;vendor:apple;endian:little;ptrsize:8;hostname:hello;"); sendPacket(out, "vendor:wamr;ostype:wasi;arch:wasm32;endian:little;ptrsize:4;"); break; case "qProcessInfo": @@ -281,7 +274,7 @@ else if (pkt.startsWith("stackInfo")) { sendPacket(out, result); try { - log("Current line " + debugSourceMap.getLineForPc(state.getPc()) + " " + debugSourceMap.getSourceFileName(state.getPc())); + logger.info("Current line {} {}", debugSourceMap.getLineForPc(state.getPc()), debugSourceMap.getSourceFileName(state.getPc())); } catch(Exception e) {} break; @@ -301,7 +294,7 @@ else if (pkt.startsWith("stackInfo")) { sendPacket(out, encodeRegs()); break; case "s": - log("Received step command from lldb"); + logger.info("Received step command from lldb"); debugger.stepInto(); sendStopPacket(out, "05"); break; @@ -318,12 +311,12 @@ else if (pkt.startsWith("stackInfo")) { break; case "D": attached = false; - log("Detach from target"); + logger.info("Detach from target"); debugger.close(); sendPacket(out, "OK"); break; default: - System.out.println("Unknown packet: " + pkt); + logger.warn("Unknown packet: {}", pkt); sendPacket(out, ""); break; } @@ -334,13 +327,6 @@ private void sendStopPacket(OutputStream out, String signal) throws IOException sendPacket(out, "T" + signal + "thread:1;name:warduino;thread-pcs:" + toHex(getCurrentState().getPc()) + ";00:" + toHex(getCurrentState().getPc()) + ";reason:trace"); } - private void log(String s) { - System.out.print("\u001b[36m"); - System.out.print("[GDBSTUB] "); - System.out.println(s); - System.out.print("\u001b[0m"); - } - private String recvPacket(InputStream in, OutputStream out) throws IOException { int c; @@ -389,7 +375,7 @@ private void sendPacket(OutputStream out, String payload) throws IOException { String pkt = "$" + payload + "#" + String.format("%02x", sum); out.write(pkt.getBytes()); out.flush(); - System.out.println("-> " + pkt); + logger.trace("-> {}", pkt); } private int checksum(byte[] data) { From f2ce4b6cd7550be17366eb3e24e6427bf715331e Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Sun, 5 Apr 2026 13:38:04 +0200 Subject: [PATCH 16/26] Add logback configuration that writes to stdout (DEBUG) + mio.log (TRACE) --- src/main/resources/logback.xml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/resources/logback.xml diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..77f6ff6 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,22 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + DEBUG + + + + + mio.log + + %-4relative [%thread] %-5level %logger{35} - %msg%n + + + + + + + + From a0616bd8279b5ed83b72acef362c07b34e0b628a Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Sun, 5 Apr 2026 13:53:59 +0200 Subject: [PATCH 17/26] Use colors for log level + don't append in file + improved log messages --- src/main/java/be/ugent/topl/mio/GdbStub.java | 12 +++++++----- src/main/resources/logback.xml | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index c262d8b..9189ffe 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -31,6 +31,7 @@ public GdbStub(Debugger debugger, String binaryLocation) { debugger.getBreakpointsListeners().add((pc) -> { try { + logger.info("Stopped at breakpoint {}", pc); sendPacket(out, "S05"); } catch (IOException e) { throw new RuntimeException(e); @@ -214,7 +215,7 @@ else if (pkt.startsWith("Z")) { debugger.addBreakpoint((int) addr); try { - logger.info("Breakpoint line {} {}", debugSourceMap.getLineForPc((int) addr), debugSourceMap.getSourceFileName((int) addr)); + logger.info("Add breakpoint at {}:{}", debugSourceMap.getSourceFileName((int) addr), debugSourceMap.getLineForPc((int) addr)); } catch(Exception _) {} // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. @@ -231,8 +232,8 @@ else if (pkt.startsWith("z")) { debugger.removeBreakpoint((int) addr); try { - logger.info("Breakpoint line {} {}", debugSourceMap.getLineForPc((int) addr), debugSourceMap.getSourceFileName((int) addr)); - } catch(Exception e) {} + logger.info("Remove breakpoint at {}:{}", debugSourceMap.getSourceFileName((int) addr), debugSourceMap.getLineForPc((int) addr)); + } catch(Exception _) {} // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. sendPacket(out, "OK"); @@ -274,8 +275,8 @@ else if (pkt.startsWith("stackInfo")) { sendPacket(out, result); try { - logger.info("Current line {} {}", debugSourceMap.getLineForPc(state.getPc()), debugSourceMap.getSourceFileName(state.getPc())); - } catch(Exception e) {} + logger.info("At {}:{}", debugSourceMap.getSourceFileName(state.getPc()), debugSourceMap.getLineForPc(state.getPc())); + } catch(Exception _) {} break; case "qC": // Get thread id @@ -303,6 +304,7 @@ else if (pkt.startsWith("stackInfo")) { sendStopPacket(out, "05"); break; case "c": + logger.info("Continue execution"); debugger.run(); break; case "pause": diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 77f6ff6..c023bd2 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,7 +1,7 @@ - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %d{HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n DEBUG @@ -10,6 +10,7 @@ mio.log + false %-4relative [%thread] %-5level %logger{35} - %msg%n From 10a1731e56636535b34ed2eed2862d618c11f3b4 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Tue, 7 Apr 2026 11:19:10 +0200 Subject: [PATCH 18/26] Advertise ReverseStep+ to lldb and fix "bs" being "sb" Also print connected hostname --- src/main/java/be/ugent/topl/mio/GdbStub.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 9189ffe..47b0892 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -101,7 +101,7 @@ public void start(int port) throws IOException { ServerSocket server = new ServerSocket(port); logger.info("Waiting for LLDB connection on port {}...", port); Socket sock = server.accept(); - logger.info("LLDB connected!"); + logger.info("LLDB connected! {}", sock.getInetAddress()); InputStream in = sock.getInputStream(); out = sock.getOutputStream(); @@ -164,7 +164,7 @@ public void start(int port) throws IOException { continue; } else if (pkt.startsWith("qSupported")) { - sendPacket(out, "qXfer:libraries:read+;vContSupported-;wasm+;"); + sendPacket(out, "qXfer:libraries:read+;vContSupported-;wasm+;ReverseStep+;"); continue; } else if (pkt.startsWith("qXfer:libraries:read")) { @@ -299,7 +299,7 @@ else if (pkt.startsWith("stackInfo")) { debugger.stepInto(); sendStopPacket(out, "05"); break; - case "sb": + case "bs": debugger.stepBack(1, () -> null); sendStopPacket(out, "05"); break; From 259e5609b1defe539a1159d09efb8900b3a45eba Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Tue, 7 Apr 2026 17:23:06 +0200 Subject: [PATCH 19/26] Allow stepping back by changing the step direction with QSetStepDir This allows lldb plugins to step back in the following way: ```python ci = debugger.GetCommandInterpreter() res = lldb.SBCommandReturnObject() ci.HandleCommand('process plugin packet send "QSetStepDir:1"', res) debugger.HandleCommand("stepi") ci.HandleCommand('process plugin packet send "QSetStepDir:0"', res) ``` Because the plugin uses `stepi` to perform the actual step operation, lldb will correctly update the state and also use the same UI as regular steps because it's a normal step command. --- src/main/java/be/ugent/topl/mio/GdbStub.java | 41 ++++++++++++++++--- .../be/ugent/topl/mio/debugger/Debugger.kt | 4 +- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 47b0892..8104b75 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -22,6 +22,7 @@ public class GdbStub { private final String binaryLocation; private OutputStream out; private final Logger logger = LoggerFactory.getLogger(GdbStub.class); + private boolean stepForward = true; private final SourceMap debugSourceMap; // Remove later, lldb doesn't need this. public GdbStub(Debugger debugger, String binaryLocation) { @@ -164,6 +165,7 @@ public void start(int port) throws IOException { continue; } else if (pkt.startsWith("qSupported")) { + //ReverseContinue+; sendPacket(out, "qXfer:libraries:read+;vContSupported-;wasm+;ReverseStep+;"); continue; } @@ -243,6 +245,20 @@ else if (pkt.startsWith("stackInfo")) { //debugger.stepBack(1, wasmData); sendPacket(out, getCurrentState().getStack().toString()); continue; + } else if (pkt.startsWith("bs")) { + if (pkt.equals("bs")) { + stepBack(1); + } else { + int n = Integer.parseInt(pkt.split(" ")[1]); + logger.info("Step back {} instruction(s)", n); + stepBack(n); + } + continue; + } else if (pkt.startsWith("QSetStepDir:")) { + stepForward = Integer.parseInt(pkt.substring("QSetStepDir:".length())) == 0; + logger.info("Change step direction to {}", stepForward ? "forward" : "backward"); + sendPacket(out, "OK"); + continue; } switch (pkt) { @@ -296,12 +312,15 @@ else if (pkt.startsWith("stackInfo")) { break; case "s": logger.info("Received step command from lldb"); - debugger.stepInto(); - sendStopPacket(out, "05"); - break; - case "bs": - debugger.stepBack(1, () -> null); - sendStopPacket(out, "05"); + if (stepForward) { + logger.info("Step forward"); + debugger.stepInto(); + sendStopPacket(out, "05"); + } + else { + logger.info("Step backward"); + stepBack(1); + } break; case "c": logger.info("Continue execution"); @@ -325,6 +344,16 @@ else if (pkt.startsWith("stackInfo")) { } } + private void stepBack(int n) throws IOException { + if (!debugger.canStepBack(n)) { + sendPacket(out, "E01"); + return; + } + + debugger.stepBack(n, () -> null); + sendStopPacket(out, "05"); + } + private void sendStopPacket(OutputStream out, String signal) throws IOException { sendPacket(out, "T" + signal + "thread:1;name:warduino;thread-pcs:" + toHex(getCurrentState().getPc()) + ";00:" + toHex(getCurrentState().getPc()) + ";reason:trace"); } diff --git a/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt b/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt index 7f5f6e5..f0647fb 100644 --- a/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt +++ b/src/main/kotlin/be/ugent/topl/mio/debugger/Debugger.kt @@ -219,8 +219,8 @@ open class Debugger(private val connection: Connection, start: Boolean = true, o } } - private fun canStepBack(): Boolean { - return checkpoints.size > 1 + fun canStepBack(n: Int = 1): Boolean { + return checkpoints.size > n } fun stepBackUntil(cond: (WOODDumpResponse) -> Boolean) { From 2699a1f12d4041a26f59b6dc3c3b295726c856a3 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Thu, 9 Apr 2026 17:32:41 +0200 Subject: [PATCH 20/26] Report a history boundary error when stepping back too far --- src/main/java/be/ugent/topl/mio/GdbStub.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 8104b75..55d462e 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -346,7 +346,7 @@ else if (pkt.startsWith("stackInfo")) { private void stepBack(int n) throws IOException { if (!debugger.canStepBack(n)) { - sendPacket(out, "E01"); + sendPacket(out, "T" + "05" + "thread:1;name:warduino;thread-pcs:" + toHex(getCurrentState().getPc()) + ";00:" + toHex(getCurrentState().getPc()) + ";replaylog:begin;"); return; } From cb2930d0d894a7c09a7aeaafac153abdac83e97c Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 10 Apr 2026 13:39:58 +0200 Subject: [PATCH 21/26] Add reverse continue TODO --- src/main/java/be/ugent/topl/mio/GdbStub.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 55d462e..587441d 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -326,6 +326,10 @@ else if (pkt.startsWith("stackInfo")) { logger.info("Continue execution"); debugger.run(); break; + /*case "bc": + // TODO: Actual backwards continue in MIO + stepBack(1); + break;*/ case "pause": debugger.pause(); sendStopPacket(out, "02"); From 3618b4e25a470e2464b5004d5aeadcf6b9f9e78e Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Fri, 10 Apr 2026 13:53:45 +0200 Subject: [PATCH 22/26] Keep connection after detach, allowing lldb to re-connect later --- src/main/java/be/ugent/topl/mio/GdbStub.java | 29 +++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 587441d..6e72f83 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -95,21 +95,30 @@ public WOODDumpResponse getCurrentState() { } public void start(int port) throws IOException { + start(port, false); + } + + public void start(int port, boolean closeOnDetach) throws IOException { debugger.pause(); byte[] wasmData = Files.readAllBytes(Path.of(binaryLocation)); ServerSocket server = new ServerSocket(port); - logger.info("Waiting for LLDB connection on port {}...", port); - Socket sock = server.accept(); - logger.info("LLDB connected! {}", sock.getInetAddress()); - - InputStream in = sock.getInputStream(); - out = sock.getOutputStream(); + Socket sock; + InputStream in = null; + boolean attached = false; - boolean attached = true; + do { + if (!attached) { + logger.info("Waiting for LLDB connection on port {}...", port); + sock = server.accept(); + logger.info("LLDB connected! {}", sock.getInetAddress()); + + in = sock.getInputStream(); + out = sock.getOutputStream(); + attached = true; + } - while (attached) { String pkt = recvPacket(in, out); if (pkt == null) { logger.info("Connection closed"); @@ -337,7 +346,6 @@ else if (pkt.startsWith("stackInfo")) { case "D": attached = false; logger.info("Detach from target"); - debugger.close(); sendPacket(out, "OK"); break; default: @@ -345,7 +353,8 @@ else if (pkt.startsWith("stackInfo")) { sendPacket(out, ""); break; } - } + } while(!closeOnDetach || attached); + debugger.close(); } private void stepBack(int n) throws IOException { From b523dd38da45fe4fe8f5d6e016b4ad71158bd2d2 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Sun, 12 Apr 2026 18:40:17 +0200 Subject: [PATCH 23/26] Send stop packet for breakpoints instead of S05 Otherwise, it seems LLDB just thinks an exception occured instead of a nice stop for a breakpoint causing it to not discard a range thread plan. --- src/main/java/be/ugent/topl/mio/GdbStub.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 6e72f83..111cbaa 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -33,7 +33,7 @@ public GdbStub(Debugger debugger, String binaryLocation) { debugger.getBreakpointsListeners().add((pc) -> { try { logger.info("Stopped at breakpoint {}", pc); - sendPacket(out, "S05"); + sendStopPacket(out, "05"); } catch (IOException e) { throw new RuntimeException(e); } From 097b6c230b2e903f2eed8480275f3fe1b401af91 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Mon, 13 Apr 2026 16:33:59 +0200 Subject: [PATCH 24/26] Remove debugSourceMap from GdbStub since we no longer need it --- src/main/java/be/ugent/topl/mio/GdbStub.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 111cbaa..6ebcf56 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -1,7 +1,6 @@ package be.ugent.topl.mio; import be.ugent.topl.mio.debugger.Debugger; -import be.ugent.topl.mio.sourcemap.SourceMap; import be.ugent.topl.mio.woodstate.Frame; import be.ugent.topl.mio.woodstate.WOODDumpResponse; import org.slf4j.Logger; @@ -15,20 +14,16 @@ import java.util.ArrayList; import java.util.List; -import static be.ugent.topl.mio.sourcemap.DwarfSourceMapKt.getDwarfSourcemap; - public class GdbStub { private final Debugger debugger; private final String binaryLocation; private OutputStream out; private final Logger logger = LoggerFactory.getLogger(GdbStub.class); private boolean stepForward = true; - private final SourceMap debugSourceMap; // Remove later, lldb doesn't need this. public GdbStub(Debugger debugger, String binaryLocation) { this.debugger = debugger; this.binaryLocation = binaryLocation; - this.debugSourceMap = getDwarfSourcemap(binaryLocation); debugger.getBreakpointsListeners().add((pc) -> { try { @@ -225,10 +220,6 @@ else if (pkt.startsWith("Z")) { logger.info("Add breakpoint on {}", addr); debugger.addBreakpoint((int) addr); - try { - logger.info("Add breakpoint at {}:{}", debugSourceMap.getSourceFileName((int) addr), debugSourceMap.getLineForPc((int) addr)); - } catch(Exception _) {} - // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. sendPacket(out, "OK"); continue; @@ -242,10 +233,6 @@ else if (pkt.startsWith("z")) { logger.info("Remove breakpoint on {}", addr); debugger.removeBreakpoint((int) addr); - try { - logger.info("Remove breakpoint at {}:{}", debugSourceMap.getSourceFileName((int) addr), debugSourceMap.getLineForPc((int) addr)); - } catch(Exception _) {} - // A remote target shall return an empty string for an unrecognized breakpoint or watchpoint packet type. sendPacket(out, "OK"); continue; @@ -299,10 +286,6 @@ else if (pkt.startsWith("stackInfo")) { } sendPacket(out, result); - try { - logger.info("At {}:{}", debugSourceMap.getSourceFileName(state.getPc()), debugSourceMap.getLineForPc(state.getPc())); - } catch(Exception _) {} - break; case "qC": // Get thread id sendPacket(out, "QC 1"); From 1587cb9d2285053751fe7e5721d1c2a0eb3ae5b8 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Mon, 13 Apr 2026 16:36:12 +0200 Subject: [PATCH 25/26] Use config to decide if we should use the emulator or not in gdbserver mode --- src/main/kotlin/be/ugent/topl/mio/Main.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/be/ugent/topl/mio/Main.kt b/src/main/kotlin/be/ugent/topl/mio/Main.kt index 40f633b..cfb7e4d 100644 --- a/src/main/kotlin/be/ugent/topl/mio/Main.kt +++ b/src/main/kotlin/be/ugent/topl/mio/Main.kt @@ -3,14 +3,14 @@ package be.ugent.topl.mio import be.ugent.topl.mio.connections.ProcessConnection import be.ugent.topl.mio.connections.SerialConnection import be.ugent.topl.mio.debugger.Debugger -import com.formdev.flatlaf.FlatDarkLaf -import com.formdev.flatlaf.FlatIntelliJLaf import be.ugent.topl.mio.sourcemap.AsSourceMapping import be.ugent.topl.mio.sourcemap.compileAndFlash import be.ugent.topl.mio.sourcemap.compileWat import be.ugent.topl.mio.sourcemap.getDwarfSourcemap import be.ugent.topl.mio.ui.InteractiveDebugger import be.ugent.topl.mio.ui.StartScreen +import com.formdev.flatlaf.FlatDarkLaf +import com.formdev.flatlaf.FlatIntelliJLaf import com.formdev.flatlaf.util.SystemInfo import java.io.File import java.io.FileNotFoundException @@ -57,7 +57,8 @@ fun main(args: Array) { expectNArguments(args, 2) val wasmFilename = args[1] val port = args[0].split("=")[1].toInt() - val debugger = Debugger(ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused")) + val connection = if(config.useEmulator) ProcessConnection(config.wdcliPath, wasmFilename, "--no-socket", "--paused") else SerialConnection(config.port!!) + val debugger = Debugger(connection) debugger.pause() debugger.setSnapshotPolicy(Debugger.SnapshotPolicy.Checkpointing()) val stub = GdbStub(debugger, wasmFilename) From dddd669ad4bb34631b0277d80309e9c4d31cd106 Mon Sep 17 00:00:00 2001 From: MaartenS11 Date: Mon, 13 Apr 2026 16:46:58 +0200 Subject: [PATCH 26/26] Minor cleanup to the GdbStub --- src/main/java/be/ugent/topl/mio/GdbStub.java | 34 +++----------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/src/main/java/be/ugent/topl/mio/GdbStub.java b/src/main/java/be/ugent/topl/mio/GdbStub.java index 6ebcf56..ed484fd 100644 --- a/src/main/java/be/ugent/topl/mio/GdbStub.java +++ b/src/main/java/be/ugent/topl/mio/GdbStub.java @@ -36,19 +36,6 @@ public GdbStub(Debugger debugger, String binaryLocation) { }); } - private static final char[] HEX = "0123456789abcdef".toCharArray(); - private String toHex(byte[] data, int offset, int length) { - StringBuilder sb = new StringBuilder(length * 2); - - for (int i = 0; i < length; i++) { - int b = data[offset + i] & 0xFF; - sb.append(HEX[b >>> 4]); - sb.append(HEX[b & 0x0F]); - } - - return sb.toString(); - } - private String toHex(long data, int maxLen, boolean bigEndian) { StringBuilder result = new StringBuilder(); for (int i = 0; i < maxLen; i++) { @@ -79,10 +66,9 @@ private long stripAddrType(long addr) { } private String getTriple(String s) { - String hex = s.chars() + return s.chars() .mapToObj(c -> String.format("%02x", c)) .reduce("", String::concat); - return hex; } public WOODDumpResponse getCurrentState() { @@ -142,9 +128,6 @@ public void start(int port, boolean closeOnDetach) throws IOException { logger.info("Reading from wasm linear memory"); memory = getCurrentState().getMemory().getBytes(); } - else { - //pos -= codeSection.offset; - } // The reply may contain fewer addressable memory units than requested if the server was reading from a trace frame memory and was able to read only part of the region of memory. if (pos >= memory.length || pos < 0) { @@ -266,7 +249,6 @@ else if (pkt.startsWith("stackInfo")) { break; case "qProcessInfo": sendPacket(out, "pid:1;parent-pid:1;vendor:wamr;ostype:wasi;arch:wasm32;triple:" + getTriple("wasm32-unknown-unknown-wasm") + ";endian:little;ptrsize:4;"); - //sendPacket(out, "pid:1;parent-pid:1;vendor:wamr;ostype:wasi;arch:wasm32;triple:7761736d33322d77616d722d776173692d7761736d;endian:little;ptrsize:4;"); break; case "qGetWorkingDir": sendPacket(out, "/tmp"); @@ -276,15 +258,15 @@ else if (pkt.startsWith("stackInfo")) { break; case "qWasmCallStack:1": // Get the callstack for thread 1. WOODDumpResponse state = getCurrentState(); - String result = toHex(state.getPc()); + StringBuilder result = new StringBuilder(toHex(state.getPc())); for (int i = state.getCallstack().size() - 1; i >= 0; i--) { // Only functions are real callstack elements: Frame f = state.getCallstack().get(i); if (f.getType() == 0) { - result += toHex(f.getRa()); + result.append(toHex(f.getRa())); } } - sendPacket(out, result); + sendPacket(out, result.toString()); break; case "qC": // Get thread id @@ -300,7 +282,7 @@ else if (pkt.startsWith("stackInfo")) { sendPacket(out, "S05"); // SIGTRAP break; case "g": - sendPacket(out, encodeRegs()); + sendPacket(out, String.format("%08x", getCurrentState().getPc())); break; case "s": logger.info("Received step command from lldb"); @@ -413,12 +395,6 @@ private int checksum(byte[] data) { return sum; } - private String encodeRegs() { - StringBuilder sb = new StringBuilder(); - sb.append(String.format("%08x", getCurrentState().getPc())); - return sb.toString(); - } - private List getCallStack(WOODDumpResponse state) { List callStack = new ArrayList(); for (int i = state.getCallstack().size() - 1; i >= 0; i--) {