Compare commits

..

23 Commits

Author SHA1 Message Date
9a50607d5f feat: update to Zig v0.13.0 2024-07-03 19:45:52 -05:00
bc47ff4883 feat: implement qThreadExtraInfo
TODO: figure out what this exactly does lol
2024-03-05 23:14:50 -06:00
3670bebbc4 feat: implement 'T' packet 2024-03-05 23:14:32 -06:00
309851ab06 fix: make xml memory map optional
Can make it mandatory once I've figured out the whole NDS memory map thing
2024-03-05 23:13:47 -06:00
7ae72ed5a8 fix: ensure gdb thread respects + communicates w/ should_quit atomic bool 2024-03-05 20:56:57 -06:00
8d2c76e410 chore: attempt to resolve problem with zig pm hash 2024-02-10 15:25:10 -06:00
8c3a166a5d chore: update to 0.12.0-dev.2063+804cee3b9 2024-02-08 19:29:54 -06:00
eb8e5175bd fix: make gdbstub more modular
in prep for supporting zba and turbo with the same codebase
2023-12-27 21:02:07 -06:00
479319e7ca fix: general bug fixes
1) prevent integer overflow if the PC is less than 0x0000_0004
2) gracefully exit when gdb gracefully exits
3) rename server to socket in Server.zig
2023-12-15 02:47:01 -06:00
5947747533 chore: allow use as git submodule 2023-12-15 00:49:02 -06:00
93cd6b1c5b feat: drop use std.net instead of zig-network 2023-12-14 22:40:56 -06:00
e5c1d4d2b7 feat: update dependency for zig v0.11.0 2023-12-14 21:25:12 -06:00
dc159b4aeb chore: update to latest zig 2023-07-11 00:45:11 -05:00
39a4260ffd chore: update to the new build system 2023-06-19 12:09:07 -05:00
215e053b9a chore: replace link() with getModule() 2023-03-19 20:44:36 -05:00
acb59994fc chore: update to latest zig master 2023-02-23 02:43:58 -06:00
6d6a109a08 fix: gracefully exit
fix stack overflow bug in State.deinit
allow for code in another thread to signal shutdown to gdbstub
2023-02-13 20:01:01 -06:00
c1158b547e chore: update to zig master
current zig master has changes to build system
TODO: make use of Zig's package manager
2023-02-07 17:02:33 -06:00
d7b8d7acb1 feat: implement software breakpoints 2023-01-29 07:18:43 -06:00
81ff227ea7 feat: implement memory writes 2023-01-29 07:04:19 -06:00
82bad92fcf fix: refactor how c and s track breakpoints
We move state from Server.zig to Emulator.zig (the interface)
2023-01-26 23:55:31 -06:00
dbf00006e7 fix: return E22 if hwbreak arr full
Also, ensure that the above case is the only time we return an errno
err. All the others are actually problems when parsing an invalid packet
so we should nack instead
2023-01-26 23:14:23 -06:00
59b6b51466 feat: add hardware breakpoints 2023-01-26 20:47:57 -06:00
11 changed files with 630 additions and 198 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
zig-out/
zig-cache/
.zig-cache/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "lib/zig-network"]
path = lib/zig-network
url = https://github.com/MasterQ32/zig-network

View File

@@ -1,60 +1,35 @@
const std = @import("std");
fn path(comptime suffix: []const u8) []const u8 {
if (suffix[0] == '/') @compileError("expected a relative path");
return comptime (std.fs.path.dirname(@src().file) orelse ".") ++ std.fs.path.sep_str ++ suffix;
}
const pkgs = struct {
const Pkg = std.build.Pkg;
pub const gdbstub: Pkg = .{
.name = "gdbstub",
.source = .{ .path = path("src/lib.zig") },
.dependencies = &[_]Pkg{network},
};
// https://github.com/MasterQ32/zig-network
pub const network: Pkg = .{
.name = "network",
.source = .{ .path = path("lib/zig-network/network.zig") },
};
};
pub fn link(exe: *std.build.LibExeObjStep) void {
exe.addPackage(pkgs.gdbstub);
}
pub fn build(b: *std.build.Builder) void {
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
_ = target;
const mode = b.standardReleaseOptions();
// -- library --
const lib = b.addStaticLibrary("gdbstub", "src/lib.zig");
lib.addPackage(pkgs.network);
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
lib.setBuildMode(mode);
lib.install();
_ = b.addModule("zba-gdbstub", .{ .root_source_file = b.path("src/lib.zig") });
const lib_tests = b.addTest("src/lib.zig");
lib_tests.setBuildMode(mode);
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const lib_unit_tests = b.addTest(.{
.root_source_file = b.path("src/lib.zig"),
.target = target,
.optimize = optimize,
});
const test_step = b.step("lib-test", "Run Library Tests");
test_step.dependOn(&lib_tests.step);
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
// // -- Executable --
// const exe = b.addExecutable("gdbserver", "src/main.zig");
// link(exe);
// exe.setTarget(target);
// exe.setBuildMode(mode);
// exe.install();
// const run_cmd = exe.run();
// run_cmd.step.dependOn(b.getInstallStep());
// if (b.args) |args| run_cmd.addArgs(args);
// const run_step = b.step("run", "Run the app");
// run_step.dependOn(&run_cmd.step);
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
}

72
build.zig.zon Normal file
View File

@@ -0,0 +1,72 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = "zba-gdbstub",
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.1.0",
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
//.example = .{
// // When updating this field to a new URL, be sure to delete the corresponding
// // `hash`, otherwise you are communicating that you expect to find the old hash at
// // the new URL.
// .url = "https://example.com/foo.tar.gz",
//
// // This is computed from the file contents of the directory of files that is
// // obtained after fetching `url` and applying the inclusion rules given by
// // `paths`.
// //
// // This field is the source of truth; packages do not come from a `url`; they
// // come from a `hash`. `url` is just one of many possible mirrors for how to
// // obtain a package matching this `hash`.
// //
// // Uses the [multihash](https://multiformats.io/multihash/) format.
// .hash = "...",
//
// // When this is provided, the package is found in a directory relative to the
// // build root. In this case the package's hash is irrelevant and therefore not
// // computed. This field and `url` are mutually exclusive.
// .path = "foo",
// // When this is set to `true`, a package is declared to be lazily
// // fetched. This makes the dependency only get fetched if it is
// // actually used.
// .lazy = false,
//},
},
// Specifies the set of files and directories that are included in this package.
// Only files and directories listed here are included in the `hash` that
// is computed for this package. Only files listed here will remain on disk
// when using the zig package manager. As a rule of thumb, one should list
// files required for compilation plus any license(s).
// Paths are relative to the build root. Use the empty string (`""`) to refer to
// the build root itself.
// A directory listed here means that all files within, recursively, are included.
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

Submodule lib/zig-network deleted from a8c4502538

View File

@@ -2,9 +2,8 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const Emulator = @import("lib.zig").Emulator;
const target = @import("Server.zig").target;
const memory_map = @import("Server.zig").memory_map;
const State = @import("State.zig");
const Server = @import("Server.zig");
const Self = @This();
const log = std.log.scoped(.Packet);
@@ -45,13 +44,10 @@ const String = union(enum) {
}
};
pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
pub fn parse(self: *Self, allocator: Allocator, state: *Server.State, emu: *Emulator) !String {
switch (self.contents[0]) {
// Required
'?' => {
const ret = try std.fmt.allocPrint(allocator, "S{x:0>2}", .{@enumToInt(Signal.Int)});
return .{ .alloc = ret };
},
'?' => return .{ .static = "T05" }, // FIXME: which errno?
'g' => {
const r = emu.registers();
const cpsr = emu.cpsr();
@@ -63,7 +59,7 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
var i: u32 = 0;
while (i < r.len + 1) : (i += 1) {
var reg: u32 = if (i < r.len) r[i] else cpsr;
if (i == 15) reg -= if (cpsr >> 5 & 1 == 1) 4 else 8; // PC is ahead
if (i == 15) reg -|= if (cpsr >> 5 & 1 == 1) 4 else 8; // PC is ahead
// writes the formatted integer to the buffer, returns a slice to the buffer but we ignore that
// GDB also expects the bytes to be in the opposite order for whatever reason
@@ -76,8 +72,8 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
'G' => @panic("TODO: Register Write"),
'm' => {
var tokens = std.mem.tokenize(u8, self.contents[1..], ",");
const addr_str = tokens.next() orelse return .{ .static = "E9999" }; // EUNKNOWN
const length_str = tokens.next() orelse return .{ .static = "E9999" }; // EUNKNOWN
const addr_str = tokens.next() orelse return error.InvalidPacket;
const length_str = tokens.next() orelse return error.InvalidPacket;
const addr = try std.fmt.parseInt(u32, addr_str, 16);
const len = try std.fmt.parseInt(u32, length_str, 16);
@@ -87,9 +83,6 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
{
var i: u32 = 0;
while (i < len) : (i += 1) {
const value = emu.read(addr + i);
log.debug("read 0x{X:0>2} from 0x{X:0>8}", .{ value, addr + i });
// writes the formatted integer to the buffer, returns a slice to the buffer but we ignore that
_ = std.fmt.bufPrintIntToSlice(ret[i * 2 ..][0..2], emu.read(addr + i), 16, .lower, .{ .fill = '0', .width = 2 });
}
@@ -97,43 +90,144 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
return .{ .alloc = ret };
},
'M' => @panic("TODO: Memory Write"),
'c' => @panic("TODO: Continue"),
'M' => {
var tokens = std.mem.tokenize(u8, self.contents[1..], ",:");
const addr_str = tokens.next() orelse return error.InvalidPacket;
const length_str = tokens.next() orelse return error.InvalidPacket;
const bytes = tokens.next() orelse return error.InvalidPacket;
const addr = try std.fmt.parseInt(u32, addr_str, 16);
const len = try std.fmt.parseInt(u32, length_str, 16);
{
var i: u32 = 0;
while (i < len) : (i += 1) {
const str = bytes[2 * i ..][0..2];
const value = try std.fmt.parseInt(u8, str, 16);
emu.write(addr + i, value);
}
}
return .{ .static = "OK" };
},
'c' => {
switch (emu.contd()) {
.SingleStep => unreachable,
.Trap => |r| switch (r) {
.HwBkpt => return .{ .static = "T05 hwbreak:;" },
.SwBkpt => return .{ .static = "T05 swbreak:;" },
},
}
},
's' => {
// var tokens = std.mem.tokenize(u8, self.contents[1..], " ");
// const addr = if (tokens.next()) |s| try std.fmt.parseInt(u32, s, 16) else null;
emu.step();
const ret = try std.fmt.allocPrint(allocator, "S{x:0>2}", .{@enumToInt(Signal.Trap)});
return .{ .alloc = ret };
switch (emu.step()) {
.SingleStep => return .{ .static = "T05" },
.Trap => |r| switch (r) {
.HwBkpt => return .{ .static = "T05 hwbreak:;" },
.SwBkpt => return .{ .static = "T05 swbreak:;" },
},
}
},
// Optional
// Breakpoints
'z' => {
var tokens = std.mem.tokenize(u8, self.contents[2..], ",");
const addr_str = tokens.next() orelse return error.InvalidPacket;
const addr = try std.fmt.parseInt(u32, addr_str, 16);
switch (self.contents[1]) {
'0' => {
emu.removeBkpt(.Software, addr);
return .{ .static = "OK" };
},
'1' => {
emu.removeBkpt(.Hardware, addr);
return .{ .static = "OK" };
},
'2' => return .{ .static = "" }, // TODO: Remove Write Watchpoint
'3' => return .{ .static = "" }, // TODO: Remove Read Watchpoint
'4' => return .{ .static = "" }, // TODO: Remove Access Watchpoint
else => return .{ .static = "" },
}
},
'Z' => {
var tokens = std.mem.tokenize(u8, self.contents[2..], ",");
const addr_str = tokens.next() orelse return error.InvalidPacket;
const kind_str = tokens.next() orelse return error.InvalidPacket;
const addr = try std.fmt.parseInt(u32, addr_str, 16);
const kind = try std.fmt.parseInt(u32, kind_str, 16);
switch (self.contents[1]) {
'0' => {
try emu.addBkpt(.Software, addr, kind);
return .{ .static = "OK" };
},
'1' => {
emu.addBkpt(.Hardware, addr, kind) catch |e| {
switch (e) {
error.OutOfSpace => return .{ .static = "E22" }, // FIXME: which errno?
else => return e,
}
};
return .{ .static = "OK" };
},
'2' => return .{ .static = "" }, // TODO: Insert Write Watchpoint
'3' => return .{ .static = "" }, // TODO: Insert Read Watchpoint
'4' => return .{ .static = "" }, // TODO: Insert Access Watchpoint
else => return .{ .static = "" },
}
},
// TODO: Figure out the difference between 'M' and 'X'
'D' => {
log.info("Disconnecting...", .{});
state.should_quit = true;
return .{ .static = "OK" };
},
'H' => return .{ .static = "" },
'v' => {
if (substr(self.contents[1..], "MustReplyEmpty")) {
return .{ .static = "" };
if (substr(self.contents[1..], "MustReplyEmpty")) return .{ .static = "" };
if (substr(self.contents[1..], "Cont")) {
switch (self.contents[5]) {
'?' => return .{ .static = "" }, // TODO: Implement vCont
else => {},
}
}
log.warn("Unimplemented: {s}", .{self.contents});
return .{ .static = "" };
},
'T' => return .{ .static = "OK " }, // We assume single threaded here
'q' => {
if (self.contents[1] == 'C' and self.contents.len == 2) return .{ .static = "QC1" };
if (substr(self.contents[1..], "fThreadInfo")) return .{ .static = "m1" };
if (substr(self.contents[1..], "sThreadInfo")) return .{ .static = "l" };
if (substr(self.contents[1..], "Attached")) return .{ .static = "1" }; // Tell GDB we're attached to a process
if (substr(self.contents[1..], "Supported")) {
const format = "PacketSize={x:};qXfer:features:read+;qXfer:memory-map:read+";
// TODO: Anything else?
if (substr(self.contents[1..], "ThreadExtraInfo")) {
const extra_info = "FIXME: what is even expected here?";
const ret = try allocator.dupe(u8, &std.fmt.bytesToHex(extra_info, .lower));
const ret = try std.fmt.allocPrint(allocator, format, .{Self.max_len});
return .{ .alloc = ret };
}
if (substr(self.contents[1..], "Supported")) {
const format = "PacketSize={x:};swbreak+;hwbreak+;qXfer:features:read+;{s}";
const mem_map = if (state.memmap_xml == null) "" else "qXfer:memory-map:read+";
// TODO: Anything else?
const ret = try std.fmt.allocPrint(allocator, format, .{ Self.max_len, mem_map });
return .{ .alloc = ret };
}
@@ -142,9 +236,10 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
_ = tokens.next(); // Xfer
_ = tokens.next(); // features
_ = tokens.next(); // read
const annex = tokens.next() orelse return .{ .static = "E9999" };
const offset_str = tokens.next() orelse return .{ .static = "E99999" };
const length_str = tokens.next() orelse return .{ .static = "E9999" };
const annex = tokens.next() orelse return error.InvalidPacket;
const offset_str = tokens.next() orelse return error.InvalidPacket;
const length_str = tokens.next() orelse return error.InvalidPacket;
if (std.mem.eql(u8, annex, "target.xml")) {
const offset = try std.fmt.parseInt(usize, offset_str, 16);
@@ -153,11 +248,11 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
// + 2 to account for the "m " in the response
// subtract offset so that the allocated buffer isn't
// larger than it needs to be TODO: Test this?
const len = @min(length, (target.len + 1) - offset);
const len = @min(length, (state.target_xml.len + 1) - offset);
const ret = try allocator.alloc(u8, len);
ret[0] = if (ret.len < length) 'l' else 'm';
std.mem.copy(u8, ret[1..], target[offset..]);
@memcpy(ret[1..], state.target_xml[offset..]);
return .{ .alloc = ret };
} else {
@@ -169,22 +264,24 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
}
if (substr(self.contents[1..], "Xfer:memory-map:read")) {
const mem_map = state.memmap_xml.?;
var tokens = std.mem.tokenize(u8, self.contents[1..], ":,");
_ = tokens.next(); // Xfer
_ = tokens.next(); // memory-map
_ = tokens.next(); // read
const offset_str = tokens.next() orelse return .{ .static = "E9999" };
const length_str = tokens.next() orelse return .{ .static = "E9999" };
const offset_str = tokens.next() orelse return error.InvalidPacket;
const length_str = tokens.next() orelse return error.InvalidPacket;
const offset = try std.fmt.parseInt(usize, offset_str, 16);
const length = try std.fmt.parseInt(usize, length_str, 16);
// see above
const len = @min(length, (memory_map.len + 1) - offset);
const len = @min(length, (mem_map.len + 1) - offset);
const ret = try allocator.alloc(u8, len);
ret[0] = if (ret.len < length) 'l' else 'm';
std.mem.copy(u8, ret[1..], memory_map[offset..]);
@memcpy(ret[1..], mem_map[offset..]);
return .{ .alloc = ret };
}
@@ -213,7 +310,7 @@ pub fn checksum(input: []const u8) u8 {
var sum: usize = 0;
for (input) |char| sum += char;
return @truncate(u8, sum);
return @truncate(sum);
}
fn verify(input: []const u8, chksum: u8) bool {

View File

@@ -1,87 +1,43 @@
const std = @import("std");
const network = @import("network");
const Packet = @import("Packet.zig");
const Socket = network.Socket;
const Allocator = std.mem.Allocator;
const Emulator = @import("lib.zig").Emulator;
const Allocator = std.mem.Allocator;
const Server = std.net.Server;
const Self = @This();
const log = std.log.scoped(.Server);
const port: u16 = 2424;
pub const target: []const u8 =
\\<target version="1.0">
\\ <architecture>armv4t</architecture>
\\ <feature name="org.gnu.gdb.arm.core">
\\ <reg name="r0" bitsize="32" type="uint32"/>
\\ <reg name="r1" bitsize="32" type="uint32"/>
\\ <reg name="r2" bitsize="32" type="uint32"/>
\\ <reg name="r3" bitsize="32" type="uint32"/>
\\ <reg name="r4" bitsize="32" type="uint32"/>
\\ <reg name="r5" bitsize="32" type="uint32"/>
\\ <reg name="r6" bitsize="32" type="uint32"/>
\\ <reg name="r7" bitsize="32" type="uint32"/>
\\ <reg name="r8" bitsize="32" type="uint32"/>
\\ <reg name="r9" bitsize="32" type="uint32"/>
\\ <reg name="r10" bitsize="32" type="uint32"/>
\\ <reg name="r11" bitsize="32" type="uint32"/>
\\ <reg name="r12" bitsize="32" type="uint32"/>
\\ <reg name="sp" bitsize="32" type="data_ptr"/>
\\ <reg name="lr" bitsize="32"/>
\\ <reg name="pc" bitsize="32" type="code_ptr"/>
\\
\\ <reg name="cpsr" bitsize="32" regnum="25"/>
\\ </feature>
\\</target>
;
// Game Pak SRAM isn't included
// TODO: Can i be more specific here?
pub const memory_map: []const u8 =
\\ <memory-map version="1.0">
\\ <memory type="rom" start="0x00000000" length="0x00004000"/>
\\ <memory type="ram" start="0x02000000" length="0x00040000"/>
\\ <memory type="ram" start="0x03000000" length="0x00008000"/>
\\ <memory type="ram" start="0x04000000" length="0x00000400"/>
\\ <memory type="ram" start="0x05000000" length="0x00000400"/>
\\ <memory type="ram" start="0x06000000" length="0x00018000"/>
\\ <memory type="ram" start="0x07000000" length="0x00000400"/>
\\ <memory type="rom" start="0x08000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0A000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0C000000" length="0x02000000"/>
\\ </memory-map>
;
// FIXME: Shouldn't this be a Packet Struct?
pkt_cache: ?[]const u8 = null,
client: Socket,
_socket: Socket,
socket: Server,
state: State,
emu: Emulator,
pub fn init(emulator: Emulator) !Self {
try network.init();
pub const State = struct {
should_quit: bool = false,
target_xml: []const u8,
memmap_xml: ?[]const u8,
};
var socket = try Socket.create(.ipv4, .tcp);
try socket.bindToPort(port);
try socket.listen();
const Xml = struct { target: []const u8, memory_map: ?[]const u8 };
var client = try socket.accept(); // TODO: This blocks, is this OK?
pub fn init(emulator: Emulator, xml: Xml) !Self {
const localhost = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, port);
const endpoint = try client.getLocalEndPoint();
log.info("client connected from {}", .{endpoint});
return .{ .emu = emulator, ._socket = socket, .client = client };
return .{
.emu = emulator,
.socket = try localhost.listen(.{}),
.state = .{ .target_xml = xml.target, .memmap_xml = xml.memory_map },
};
}
pub fn deinit(self: *Self, allocator: Allocator) void {
self.reset(allocator);
self.client.close();
self._socket.close();
network.deinit();
self.socket.deinit();
self.* = undefined;
}
@@ -94,20 +50,30 @@ const Action = union(enum) {
nack,
};
pub fn run(self: *Self, allocator: Allocator) !void {
pub fn run(self: *Self, allocator: Allocator, should_quit: *std.atomic.Value(bool)) !void {
var buf: [Packet.max_len]u8 = undefined;
while (true) {
const len = try self.client.receive(&buf);
var client = try self.socket.accept();
log.info("client connected from {}", .{client.address});
while (!should_quit.load(.monotonic)) {
if (self.state.should_quit) {
// Just in case its the gdbstub that exited first,
// attempt to signal to the GUI that it should also exit
should_quit.store(true, .monotonic);
break;
}
const len = try client.stream.read(&buf);
if (len == 0) break;
const action = try self.parse(allocator, buf[0..len]);
try self.send(allocator, action);
try self.send(allocator, client, action);
}
}
fn parse(self: *Self, allocator: Allocator, input: []const u8) !Action {
// log.debug("-> {s}", .{input});
log.debug("-> {s}", .{input});
return switch (input[0]) {
'+' => blk: {
@@ -129,7 +95,7 @@ fn handlePacket(self: *Self, allocator: Allocator, input: []const u8) !Action {
var packet = Packet.from(allocator, input) catch return .nack;
defer packet.deinit(allocator);
var string = packet.parse(allocator, self.emu) catch return .nack;
var string = packet.parse(allocator, &self.state, &self.emu) catch return .nack;
defer string.deinit(allocator);
const reply = string.inner();
@@ -140,11 +106,11 @@ fn handlePacket(self: *Self, allocator: Allocator, input: []const u8) !Action {
return .{ .send = response };
}
fn send(self: *Self, allocator: Allocator, action: Action) !void {
fn send(self: *Self, allocator: Allocator, client: Server.Connection, action: Action) !void {
switch (action) {
.send => |pkt| {
_ = try self.client.send(pkt);
// log.debug("<- {s}", .{pkt});
_ = try client.stream.writeAll(pkt);
log.debug("<- {s}", .{pkt});
self.reset(allocator);
self.pkt_cache = pkt;
@@ -153,19 +119,19 @@ fn send(self: *Self, allocator: Allocator, action: Action) !void {
log.warn("received nack, resending: \"{?s}\"", .{self.pkt_cache});
if (self.pkt_cache) |pkt| {
_ = try self.client.send(pkt);
// log.debug("<- {s}", .{pkt});
_ = try client.stream.writeAll(pkt);
log.debug("<- {s}", .{pkt});
}
},
.ack => {
_ = try self.client.send("+");
// log.debug("<- +", .{});
_ = try client.stream.writeAll("+");
log.debug("<- +", .{});
self.reset(allocator);
},
.nack => {
_ = try self.client.send("-");
// log.debug("<- -", .{});
_ = try client.stream.writeAll("-");
log.debug("<- -", .{});
self.reset(allocator);
},

122
src/State.zig Normal file
View File

@@ -0,0 +1,122 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
hw_bkpt: HwBkpt = .{},
sw_bkpt: SwBkpt,
pub fn init(allocator: Allocator) @This() {
return .{ .sw_bkpt = SwBkpt.init(allocator) };
}
pub fn deinit(self: *@This()) void {
self.sw_bkpt.deinit();
self.* = undefined;
}
const SwBkpt = struct {
const log = std.log.scoped(.SwBkpt);
list: std.ArrayList(Bkpt),
pub fn init(allocator: Allocator) @This() {
return .{ .list = ArrayList(Bkpt).init(allocator) };
}
pub fn deinit(self: *@This()) void {
self.list.deinit();
self.* = undefined;
}
pub fn isHit(self: *const @This(), addr: u32) bool {
for (self.list.items) |bkpt| {
if (bkpt.addr == addr) return true;
}
return false;
}
pub fn add(self: *@This(), addr: u32, kind: u32) !void {
for (self.list.items) |bkpt| {
if (bkpt.addr == addr) return; // indempotent
}
try self.list.append(.{ .addr = addr, .kind = try Bkpt.Kind.from(u32, kind) });
log.warn("Added Breakpoint at 0x{X:0>8}", .{addr});
}
pub fn remove(self: *@This(), addr: u32) void {
for (self.list.items, 0..) |bkpt, i| {
if (bkpt.addr == addr) {
_ = self.list.orderedRemove(i);
log.debug("Removed Breakpoint at 0x{X:0>8}", .{addr});
return;
}
}
}
};
const HwBkpt = struct {
const log = std.log.scoped(.HwBkpt);
list: [2]?Bkpt = .{ null, null },
pub fn isHit(self: *const @This(), addr: u32) bool {
for (self.list) |bkpt_opt| {
const bkpt = bkpt_opt orelse continue;
if (bkpt.addr == addr) return true;
}
return false;
}
pub fn add(self: *@This(), addr: u32, kind: u32) !void {
for (&self.list) |*bkpt_opt| {
if (bkpt_opt.*) |bkpt| {
if (bkpt.addr == addr) return; // idempotent
} else {
bkpt_opt.* = .{ .addr = addr, .kind = try Bkpt.Kind.from(u32, kind) };
log.debug("Added Breakpoint at 0x{X:0>8}", .{addr});
return;
}
}
return error.OutOfSpace;
}
pub fn remove(self: *@This(), addr: u32) void {
for (&self.list) |*bkpt_opt| {
const bkpt = bkpt_opt.* orelse continue;
if (bkpt.addr == addr) {
bkpt_opt.* = null;
log.debug("Removed Breakpoint at 0x{X:0>8}", .{addr});
break;
}
}
}
};
const Bkpt = struct {
addr: u32,
kind: Kind,
const Kind = enum(u3) {
Arm = 2,
Thumb = 4,
pub fn from(comptime T: type, num: T) !@This() {
comptime std.debug.assert(@typeInfo(T) == .Int);
return switch (num) {
2 => .Arm,
4 => .Thumb,
else => error.UnknownBkptKind,
};
}
};
};

View File

@@ -1,10 +1,23 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
/// Re-export of the server interface
pub const Server = @import("Server.zig");
const State = @import("State.zig");
/// Interface for interacting between GDB and a GBA emu
pub const Emulator = struct {
const Self = @This();
const Signal = union(enum) {
const Kind = enum { HwBkpt, SwBkpt };
Trap: Kind,
SingleStep: void,
};
state: State,
ptr: *anyopaque,
readFn: *const fn (*anyopaque, u32) u8,
@@ -15,48 +28,60 @@ pub const Emulator = struct {
stepFn: *const fn (*anyopaque) void,
pub fn init(ptr: anytype) Self {
pub fn init(allocator: Allocator, ptr: anytype) Self {
const Ptr = @TypeOf(ptr);
const ptr_info = @typeInfo(Ptr);
if (ptr_info != .Pointer) @compileError("ptr must be a pointer");
if (ptr_info.Pointer.size != .One) @compileError("ptr must be a single-item pointer");
const alignment = ptr_info.Pointer.alignment;
const gen = struct {
pub fn readImpl(pointer: *anyopaque, addr: u32) u8 {
const self = @ptrCast(Ptr, @alignCast(alignment, pointer));
const self: Ptr = @ptrCast(@alignCast(pointer));
return @call(.always_inline, ptr_info.Pointer.child.read, .{ self, addr });
}
pub fn writeImpl(pointer: *anyopaque, addr: u32, value: u8) void {
const self = @ptrCast(Ptr, @alignCast(alignment, pointer));
const self: Ptr = @ptrCast(@alignCast(pointer));
return @call(.always_inline, ptr_info.Pointer.child.write, .{ self, addr, value });
}
pub fn registersImpl(pointer: *anyopaque) *[16]u32 {
const self = @ptrCast(Ptr, @alignCast(alignment, pointer));
const self: Ptr = @ptrCast(@alignCast(pointer));
return @call(.always_inline, ptr_info.Pointer.child.registers, .{self});
}
pub fn cpsrImpl(pointer: *anyopaque) u32 {
const self = @ptrCast(Ptr, @alignCast(alignment, pointer));
const self: Ptr = @ptrCast(@alignCast(pointer));
return @call(.always_inline, ptr_info.Pointer.child.cpsr, .{self});
}
pub fn stepImpl(pointer: *anyopaque) void {
const self = @ptrCast(Ptr, @alignCast(alignment, pointer));
const self: Ptr = @ptrCast(@alignCast(pointer));
return @call(.always_inline, ptr_info.Pointer.child.step, .{self});
}
};
return .{ .ptr = ptr, .readFn = gen.readImpl, .writeFn = gen.writeImpl, .registersFn = gen.registersImpl, .cpsrFn = gen.cpsrImpl, .stepFn = gen.stepImpl };
return .{
.ptr = ptr,
.readFn = gen.readImpl,
.writeFn = gen.writeImpl,
.registersFn = gen.registersImpl,
.cpsrFn = gen.cpsrImpl,
.stepFn = gen.stepImpl,
.state = State.init(allocator),
};
}
pub fn deinit(self: *Self) void {
self.state.deinit();
self.* = undefined;
}
pub inline fn read(self: Self, addr: u32) u8 {
@@ -75,7 +100,49 @@ pub const Emulator = struct {
return self.cpsrFn(self.ptr);
}
pub inline fn step(self: Self) void {
pub inline fn contd(self: *Self) Signal {
while (true) {
const signal = self.step();
switch (signal) {
.SingleStep => {},
.Trap => return signal,
}
}
}
pub inline fn step(self: *Self) Signal {
self.stepFn(self.ptr);
const r = self.registersFn(self.ptr);
const is_thumb = self.cpsrFn(self.ptr) >> 5 & 1 == 1;
const r15 = r[15] -| if (is_thumb) @as(u32, 4) else 8;
if (self.state.sw_bkpt.isHit(r15)) return .{ .Trap = .SwBkpt };
if (self.state.hw_bkpt.isHit(r15)) return .{ .Trap = .HwBkpt };
return .SingleStep;
}
const BkptType = enum { Hardware, Software };
// TODO: Consider properly implementing Software interrupts?
pub fn addBkpt(self: *Self, comptime @"type": BkptType, addr: u32, kind: u32) !void {
switch (@"type") {
.Hardware => try self.state.hw_bkpt.add(addr, kind),
.Software => try self.state.sw_bkpt.add(addr, kind),
}
}
pub fn removeBkpt(self: *Self, comptime @"type": BkptType, addr: u32) void {
switch (@"type") {
.Hardware => self.state.hw_bkpt.remove(addr),
.Software => self.state.sw_bkpt.remove(addr),
}
}
};
test {
_ = @import("test.zig");
}

View File

@@ -1,18 +0,0 @@
const std = @import("std");
const Server = @import("gdbstub").Server;
pub fn main() !void {
const log = std.log.scoped(.Main);
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator();
var server = try Server.init();
defer server.deinit(allocator);
try server.run(allocator);
log.info("Client disconnected", .{});
}

154
src/test.zig Normal file
View File

@@ -0,0 +1,154 @@
const std = @import("std");
const builtin = @import("builtin");
const Emulator = @import("lib.zig").Emulator;
const Server = @import("Server.zig");
const Allocator = std.mem.Allocator;
const BarebonesEmulator = struct {
// I have this ARMv4T and GBA memory map xml lying around so we'll reuse it here
const target: []const u8 =
\\<target version="1.0">
\\ <architecture>armv4t</architecture>
\\ <feature name="org.gnu.gdb.arm.core">
\\ <reg name="r0" bitsize="32" type="uint32"/>
\\ <reg name="r1" bitsize="32" type="uint32"/>
\\ <reg name="r2" bitsize="32" type="uint32"/>
\\ <reg name="r3" bitsize="32" type="uint32"/>
\\ <reg name="r4" bitsize="32" type="uint32"/>
\\ <reg name="r5" bitsize="32" type="uint32"/>
\\ <reg name="r6" bitsize="32" type="uint32"/>
\\ <reg name="r7" bitsize="32" type="uint32"/>
\\ <reg name="r8" bitsize="32" type="uint32"/>
\\ <reg name="r9" bitsize="32" type="uint32"/>
\\ <reg name="r10" bitsize="32" type="uint32"/>
\\ <reg name="r11" bitsize="32" type="uint32"/>
\\ <reg name="r12" bitsize="32" type="uint32"/>
\\ <reg name="sp" bitsize="32" type="data_ptr"/>
\\ <reg name="lr" bitsize="32"/>
\\ <reg name="pc" bitsize="32" type="code_ptr"/>
\\
\\ <reg name="cpsr" bitsize="32" regnum="25"/>
\\ </feature>
\\</target>
;
const memory_map: []const u8 =
\\ <memory-map version="1.0">
\\ <memory type="rom" start="0x00000000" length="0x00004000"/>
\\ <memory type="ram" start="0x02000000" length="0x00040000"/>
\\ <memory type="ram" start="0x03000000" length="0x00008000"/>
\\ <memory type="ram" start="0x04000000" length="0x00000400"/>
\\ <memory type="ram" start="0x05000000" length="0x00000400"/>
\\ <memory type="ram" start="0x06000000" length="0x00018000"/>
\\ <memory type="ram" start="0x07000000" length="0x00000400"/>
\\ <memory type="rom" start="0x08000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0A000000" length="0x02000000"/>
\\ <memory type="rom" start="0x0C000000" length="0x02000000"/>
\\ </memory-map>
;
r: [16]u32 = [_]u32{0} ** 16,
pub fn interface(self: *@This(), allocator: Allocator) Emulator {
return Emulator.init(allocator, self);
}
pub fn read(_: *const @This(), _: u32) u8 {
return 0;
}
pub fn write(_: *@This(), _: u32, _: u8) void {}
pub fn registers(self: *@This()) *[16]u32 {
return &self.r;
}
pub fn cpsr(_: *const @This()) u32 {
return 0;
}
pub fn step(_: *@This()) void {
// execute 1 instruction
}
};
test Server {
// https://github.com/ziglang/zig/blob/225fe6ddbfae016395762850e0cd5c51f9e7751c/lib/std/net/test.zig#L146C1-L156
if (builtin.single_threaded) return error.SkipZigTest;
if (builtin.os.tag == .wasi) return error.SkipZigTest;
if (builtin.os.tag == .windows)
_ = try std.os.windows.WSAStartup(2, 2);
defer if (builtin.os.tag == .windows) std.os.windows.WSACleanup() catch unreachable;
const allocator = std.testing.allocator;
var impl = BarebonesEmulator{};
var iface = impl.interface(allocator);
defer iface.deinit();
const clientFn = struct {
fn inner(address: std.net.Address) !void {
const socket = try std.net.tcpConnectToAddress(address);
defer socket.close();
_ = try socket.writer().writeAll("+");
}
}.inner;
var server = try Server.init(
iface,
.{ .target = BarebonesEmulator.target, .memory_map = BarebonesEmulator.memory_map },
);
defer server.deinit(allocator);
const t = try std.Thread.spawn(.{}, clientFn, .{server.socket.listen_address});
defer t.join();
var should_quit = std.atomic.Value(bool).init(false);
try server.run(std.testing.allocator, &should_quit);
}
test Emulator {
const ExampleImpl = struct {
r: [16]u32 = [_]u32{0} ** 16,
pub fn interface(self: *@This(), allocator: std.mem.Allocator) Emulator {
return Emulator.init(allocator, self);
}
pub fn read(_: *const @This(), _: u32) u8 {
return 0;
}
pub fn write(_: *@This(), _: u32, _: u8) void {}
pub fn registers(self: *@This()) *[16]u32 {
return &self.r;
}
pub fn cpsr(_: *const @This()) u32 {
return 0;
}
pub fn step(_: *@This()) void {
// execute 1 instruction
}
};
var impl = ExampleImpl{};
var emu = Emulator.init(std.testing.allocator, &impl);
_ = emu.read(0x0000_0000);
emu.write(0x0000_0000, 0x00);
_ = emu.registers();
_ = emu.cpsr();
_ = emu.step();
}