Compare commits

..

35 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
4bca44e5f2 chore: update to lastest zig 2023-01-03 16:54:26 -06:00
1bd9964f58 fix: handle ack+packet strings 2022-12-15 20:25:10 -04:00
460fcec370 feat: implement instruction-granular stepping 2022-12-15 19:35:38 -04:00
21565a9726 feat: gracefully handle disconnects 2022-12-15 06:28:09 -04:00
2bc5bdc310 fix: fix errors in memory-map xml 2022-12-15 06:27:41 -04:00
51082186d7 fix: reverse the endianness of cpu registers 2022-12-15 06:27:21 -04:00
8a58251ef6 feat: implement gdb memory map 2022-12-15 05:06:50 -04:00
400e155502 feat: integrate emulator interface
while I figure out the interface with zba, disable the exe example
since it doesn't have an emu to pass to gdbstub
2022-12-15 04:23:32 -04:00
26aad8d1ae feat: reconfigure zba-gdbstub as a library 2022-12-15 04:23:05 -04:00
f1e1efc5e5 feat: workshop emulator interface 2022-12-15 04:22:42 -04:00
a85874d364 feat: get to user input in gdb 2022-12-15 04:21:16 -04:00
8195012573 chore: reorganize code 2022-12-15 04:21:16 -04:00
11 changed files with 710 additions and 219 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,59 +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(.{});
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 caa31ef878

View File

@@ -1,7 +1,9 @@
const std = @import("std");
const target = @import("Server.zig").target;
const Allocator = std.mem.Allocator;
const Emulator = @import("lib.zig").Emulator;
const State = @import("State.zig");
const Server = @import("Server.zig");
const Self = @This();
const log = std.log.scoped(.Packet);
@@ -16,8 +18,6 @@ pub fn from(allocator: Allocator, str: []const u8) !Self {
const chksum_str = tokens.next() orelse return error.MissingCheckSum;
const chksum = std.fmt.parseInt(u8, chksum_str, 16) catch return error.InvalidChecksum;
// log.info("Contents: {s}", .{contents});
if (!Self.verify(contents, chksum)) return error.ChecksumMismatch;
return .{ .contents = try allocator.dupe(u8, contents) };
@@ -44,34 +44,26 @@ const String = union(enum) {
}
};
pub fn parse(self: *Self, allocator: Allocator) !String {
pub fn parse(self: *Self, allocator: Allocator, state: *Server.State, emu: *Emulator) !String {
switch (self.contents[0]) {
// Required
'?' => {
const ret: Signal = .Trap;
// Deallocated by the caller
return .{ .alloc = try std.fmt.allocPrint(allocator, "T{x:0>2}thread:1;", .{@enumToInt(ret)}) };
},
'?' => return .{ .static = "T05" }, // FIXME: which errno?
'g' => {
// TODO: Actually reference GBA Registers
const r = [_]u32{0xDEAD_BEEF} ** 0x10;
const cpsr: u32 = 0xCAFE_B0BA;
const char_len = 2;
const reg_len = @sizeOf(u32) * char_len; // Every byte is represented by 2 characters
const r = emu.registers();
const cpsr = emu.cpsr();
const reg_len = @sizeOf(u32) * 2; // Every byte is represented by 2 characters
const ret = try allocator.alloc(u8, r.len * reg_len + reg_len); // r0 -> r15 + CPSR
{
var i: usize = 0;
var i: u32 = 0;
while (i < r.len + 1) : (i += 1) {
const reg: u32 = if (i < r.len) r[i] else cpsr;
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
// bufPrintIntToSlice writes to the provided slice, which is all we want from this
// consequentially, we ignore the slice it returns since it just references the slice
// passed as an argument
_ = std.fmt.bufPrintIntToSlice(ret[i * 8 ..][0..8], reg, 16, .lower, .{ .fill = '0', .width = 8 });
// 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
_ = std.fmt.bufPrintIntToSlice(ret[i * 8 ..][0..8], @byteSwap(reg), 16, .lower, .{ .fill = '0', .width = 8 });
}
}
@@ -79,101 +71,221 @@ pub fn parse(self: *Self, allocator: Allocator) !String {
},
'G' => @panic("TODO: Register Write"),
'm' => {
// TODO: Actually reference GBA Memory
log.err("{s}", .{self.contents});
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);
_ = addr;
const ret = try allocator.alloc(u8, len * 2);
{
var i: usize = 0;
var i: u32 = 0;
while (i < len) : (i += 1) {
const value: u8 = 0;
_ = std.fmt.bufPrintIntToSlice(ret[i * 2 ..][0..2], value, 16, .lower, .{ .fill = '0', .width = 2 });
// 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 });
}
}
return .{ .alloc = ret };
},
'M' => @panic("TODO: Memory Write"),
'c' => @panic("TODO: Continue"),
's' => @panic("TODO: Step"),
'M' => {
var tokens = std.mem.tokenize(u8, self.contents[1..], ",:");
// Optional
'H' => {
log.warn("{s}", .{self.contents});
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;
switch (emu.step()) {
.SingleStep => return .{ .static = "T05" },
.Trap => |r| switch (r) {
.HwBkpt => return .{ .static = "T05 hwbreak:;" },
.SwBkpt => return .{ .static = "T05 swbreak:;" },
},
}
},
// 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]) {
'g', 'c' => return .{ .static = "OK" },
else => {
log.warn("Unimplemented: {s}", .{self.contents});
return .{ .static = "" };
'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 };
}
if (substr(self.contents[1..], "Xfer:features:read")) {
var tokens = std.mem.tokenize(u8, self.contents[1..], ":,");
_ = tokens.next(); // qXfer
_ = tokens.next(); // Xfer
_ = tokens.next(); // features
_ = tokens.next(); // read
const annex = tokens.next() orelse return .{ .static = "E00" };
const offset_str = tokens.next() orelse return .{ .static = "E00" };
const length_str = tokens.next() orelse return .{ .static = "E00" };
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")) {
log.info("Providing ARMv4T target description", .{});
const offset = try std.fmt.parseInt(usize, offset_str, 16);
const length = try std.fmt.parseInt(usize, length_str, 16);
// + 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 {
log.err("Unexpected Annex: {s}", .{annex});
return .{ .static = "E00" };
return .{ .static = "E9999" };
}
return .{ .static = "" };
}
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 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, (mem_map.len + 1) - offset);
const ret = try allocator.alloc(u8, len);
ret[0] = if (ret.len < length) 'l' else 'm';
@memcpy(ret[1..], mem_map[offset..]);
return .{ .alloc = ret };
}
log.warn("Unimplemented: {s}", .{self.contents});
return .{ .static = "" };
},
@@ -198,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 {
@@ -206,7 +318,7 @@ fn verify(input: []const u8, chksum: u8) bool {
}
const Signal = enum(u32) {
Hup, // Hangup
Hup = 1, // Hangup
Int, // Interrupt
Quit, // Quit
Ill, // Illegal Instruction

View File

@@ -1,67 +1,43 @@
const std = @import("std");
const network = @import("network");
const Packet = @import("Packet.zig");
const Emulator = @import("lib.zig").Emulator;
const Socket = network.Socket;
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>
;
// FIXME: Shouldn't this be a Packet Struct?
pkt_cache: ?[]const u8 = null,
client: Socket,
_socket: Socket,
socket: Server,
state: State,
pub fn init() !Self {
try network.init();
emu: Emulator,
var socket = try Socket.create(.ipv4, .tcp);
try socket.bindToPort(port);
try socket.listen();
pub const State = struct {
should_quit: bool = false,
target_xml: []const u8,
memmap_xml: ?[]const u8,
};
var client = try socket.accept(); // TODO: This blocks, is this OK?
const Xml = struct { target: []const u8, memory_map: ?[]const u8 };
const endpoint = try client.getLocalEndPoint();
log.info("client connected from {}", .{endpoint});
pub fn init(emulator: Emulator, xml: Xml) !Self {
const localhost = std.net.Address.initIp4(.{ 127, 0, 0, 1 }, port);
return .{ ._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;
}
@@ -74,46 +50,67 @@ 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);
const action = try self.parse(allocator, buf[0..len]);
try self.send(allocator, client, action);
}
}
fn parse(allocator: Allocator, input: []const u8) !Action {
fn parse(self: *Self, allocator: Allocator, input: []const u8) !Action {
log.debug("-> {s}", .{input});
return switch (input[0]) {
'+' => .nothing,
'+' => blk: {
if (input.len == 1) break :blk .nothing;
break :blk switch (input[1]) {
'$' => self.handlePacket(allocator, input[1..]),
else => std.debug.panic("Unknown: {s}", .{input}),
};
},
'-' => .retry,
'$' => blk: {
// Packet
'$' => try self.handlePacket(allocator, input),
'\x03' => .nothing,
else => std.debug.panic("Unknown: {s}", .{input}),
};
}
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) catch return .nack;
var string = packet.parse(allocator, &self.state, &self.emu) catch return .nack;
defer string.deinit(allocator);
const reply = string.inner();
// deallocated by the caller
const response = try std.fmt.allocPrint(allocator, "${s}#{x:0>2}", .{ reply, Packet.checksum(reply) });
const response = try std.fmt.allocPrint(allocator, "+${s}#{x:0>2}", .{ reply, Packet.checksum(reply) });
break :blk .{ .send = response };
},
else => std.debug.panic("Unknown: {s}", .{input}),
};
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("+"); // ACK
_ = try self.client.send(pkt);
_ = try client.stream.writeAll(pkt);
log.debug("<- {s}", .{pkt});
self.reset(allocator);
self.pkt_cache = pkt;
@@ -121,14 +118,21 @@ fn send(self: *Self, allocator: Allocator, action: Action) !void {
.retry => {
log.warn("received nack, resending: \"{?s}\"", .{self.pkt_cache});
if (self.pkt_cache) |pkt| _ = try self.client.send(pkt); // FIXME: is an ack to a nack necessary?
if (self.pkt_cache) |pkt| {
_ = try client.stream.writeAll(pkt);
log.debug("<- {s}", .{pkt});
}
},
.ack => {
_ = try self.client.send("+");
_ = try client.stream.writeAll("+");
log.debug("<- +", .{});
self.reset(allocator);
},
.nack => {
_ = try self.client.send("-");
_ = try client.stream.writeAll("-");
log.debug("<- -", .{});
self.reset(allocator);
},
.nothing => 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,51 +1,69 @@
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,
writeFn: *const fn (*anyopaque, u32, u8) void,
// FIXME: Expensive copy
registersFn: *const fn (*const anyopaque) [16]u32,
cpsrFn: *const fn (*const anyopaque) u32,
registersFn: *const fn (*anyopaque) *[16]u32,
cpsrFn: *const fn (*anyopaque) u32,
pub fn init(ptr: anytype) Self {
stepFn: *const fn (*anyopaque) void,
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(.{ .modifier = .always_inline }, ptr_info.Pointer.child.read, .{ u8, self, addr });
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(.{ .modifier = .always_inline }, ptr_info.Pointer.child.read, .{ u8, self, addr, value });
return @call(.always_inline, ptr_info.Pointer.child.write, .{ self, addr, value });
}
pub fn registersImpl(pointer: *const anyopaque) [16]u32 {
const self = @ptrCast(Ptr, @alignCast(alignment, pointer));
pub fn registersImpl(pointer: *anyopaque) *[16]u32 {
const self: Ptr = @ptrCast(@alignCast(pointer));
return self.r;
return @call(.always_inline, ptr_info.Pointer.child.registers, .{self});
}
pub fn cpsrImpl(pointer: *const anyopaque) u32 {
const self = @ptrCast(Ptr, @alignCast(alignment, pointer));
pub fn cpsrImpl(pointer: *anyopaque) u32 {
const self: Ptr = @ptrCast(@alignCast(pointer));
return self.cpsr.raw;
return @call(.always_inline, ptr_info.Pointer.child.cpsr, .{self});
}
pub fn stepImpl(pointer: *anyopaque) void {
const self: Ptr = @ptrCast(@alignCast(pointer));
return @call(.always_inline, ptr_info.Pointer.child.step, .{self});
}
};
@@ -55,9 +73,17 @@ pub const Emulator = struct {
.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 {
return self.readFn(self.ptr, addr);
}
@@ -66,11 +92,57 @@ pub const Emulator = struct {
self.writeFn(self.ptr, addr, value);
}
pub inline fn registers(self: Self) [16]u32 {
pub inline fn registers(self: Self) *[16]u32 {
return self.registersFn(self.ptr);
}
pub inline fn cpsr(self: Self) u32 {
return self.cpsrFn(self.ptr);
}
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();
}