Compare commits

...

17 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
11 changed files with 341 additions and 171 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
zig-out/ zig-out/
zig-cache/ 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,55 +1,35 @@
const std = @import("std"); const std = @import("std");
const CompileStep = std.Build.CompileStep;
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;
}
pub fn link(exe: *CompileStep) void {
// create zig-network module
const b = exe.builder;
// https://github.com/MasterQ32/zig-network
const network = b.createModule(.{ .source_file = .{ .path = path("lib/zig-network/network.zig") } });
const gdbstub = b.createModule(.{
.source_file = .{ .path = path("src/lib.zig") },
.dependencies = &.{.{ .name = "network", .module = network }},
});
exe.addModule("gdbstub", gdbstub);
}
// 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 { 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 target = b.standardTargetOptions(.{});
// const optimize = b.standardOptimizeOption(.{});
// -- Library -- // 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(.{});
const lib_test = b.addTest(.{ _ = b.addModule("zba-gdbstub", .{ .root_source_file = b.path("src/lib.zig") });
.root_source_file = .{ .path = "src/lib.zig" },
// 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, .target = target,
.optimize = optimize,
}); });
const test_step = b.step("test", "Run Library Tests"); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
test_step.dependOn(&lib_test.step);
// -- Executable -- // 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
// const exe = b.addExecutable(.{ // running the unit tests.
// .name = "gdbserver", const test_step = b.step("test", "Run unit tests");
// .root_source_file = .{ .path = "src/main.zig" }, test_step.dependOn(&run_lib_unit_tests.step);
// .target = target,
// .optimize = optimize,
// });
// link(exe);
// 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);
} }

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

@@ -3,9 +3,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Emulator = @import("lib.zig").Emulator; const Emulator = @import("lib.zig").Emulator;
const State = @import("State.zig"); const State = @import("State.zig");
const Server = @import("Server.zig");
const target = @import("Server.zig").target;
const memory_map = @import("Server.zig").memory_map;
const Self = @This(); const Self = @This();
const log = std.log.scoped(.Packet); const log = std.log.scoped(.Packet);
@@ -46,7 +44,7 @@ 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]) { switch (self.contents[0]) {
// Required // Required
'?' => return .{ .static = "T05" }, // FIXME: which errno? '?' => return .{ .static = "T05" }, // FIXME: which errno?
@@ -61,7 +59,7 @@ pub fn parse(self: *Self, allocator: Allocator, emu: *Emulator) !String {
var i: u32 = 0; var i: u32 = 0;
while (i < r.len + 1) : (i += 1) { while (i < r.len + 1) : (i += 1) {
var 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 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 // 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 // GDB also expects the bytes to be in the opposite order for whatever reason
@@ -191,16 +189,25 @@ pub fn parse(self: *Self, allocator: Allocator, emu: *Emulator) !String {
// TODO: Figure out the difference between 'M' and 'X' // TODO: Figure out the difference between 'M' and 'X'
'D' => { 'D' => {
log.info("Disconnecting...", .{}); log.info("Disconnecting...", .{});
state.should_quit = true;
return .{ .static = "OK" }; return .{ .static = "OK" };
}, },
'H' => return .{ .static = "" }, 'H' => return .{ .static = "" },
'v' => { 'v' => {
if (!substr(self.contents[1..], "MustReplyEmpty")) { if (substr(self.contents[1..], "MustReplyEmpty")) return .{ .static = "" };
log.warn("Unimplemented: {s}", .{self.contents});
if (substr(self.contents[1..], "Cont")) {
switch (self.contents[5]) {
'?' => return .{ .static = "" }, // TODO: Implement vCont
else => {},
}
} }
log.warn("Unimplemented: {s}", .{self.contents});
return .{ .static = "" }; return .{ .static = "" };
}, },
'T' => return .{ .static = "OK " }, // We assume single threaded here
'q' => { 'q' => {
if (self.contents[1] == 'C' and self.contents.len == 2) return .{ .static = "QC1" }; if (self.contents[1] == 'C' and self.contents.len == 2) return .{ .static = "QC1" };
@@ -208,11 +215,19 @@ pub fn parse(self: *Self, allocator: Allocator, emu: *Emulator) !String {
if (substr(self.contents[1..], "sThreadInfo")) return .{ .static = "l" }; 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..], "Attached")) return .{ .static = "1" }; // Tell GDB we're attached to a process
if (substr(self.contents[1..], "Supported")) { if (substr(self.contents[1..], "ThreadExtraInfo")) {
const format = "PacketSize={x:};swbreak+;hwbreak+;qXfer:features:read+;qXfer:memory-map:read+"; const extra_info = "FIXME: what is even expected here?";
// TODO: Anything else? 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 }; return .{ .alloc = ret };
} }
@@ -233,11 +248,11 @@ pub fn parse(self: *Self, allocator: Allocator, emu: *Emulator) !String {
// + 2 to account for the "m " in the response // + 2 to account for the "m " in the response
// subtract offset so that the allocated buffer isn't // subtract offset so that the allocated buffer isn't
// larger than it needs to be TODO: Test this? // 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); const ret = try allocator.alloc(u8, len);
ret[0] = if (ret.len < length) 'l' else 'm'; 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 }; return .{ .alloc = ret };
} else { } else {
@@ -249,6 +264,8 @@ pub fn parse(self: *Self, allocator: Allocator, emu: *Emulator) !String {
} }
if (substr(self.contents[1..], "Xfer:memory-map:read")) { if (substr(self.contents[1..], "Xfer:memory-map:read")) {
const mem_map = state.memmap_xml.?;
var tokens = std.mem.tokenize(u8, self.contents[1..], ":,"); var tokens = std.mem.tokenize(u8, self.contents[1..], ":,");
_ = tokens.next(); // Xfer _ = tokens.next(); // Xfer
_ = tokens.next(); // memory-map _ = tokens.next(); // memory-map
@@ -260,11 +277,11 @@ pub fn parse(self: *Self, allocator: Allocator, emu: *Emulator) !String {
const length = try std.fmt.parseInt(usize, length_str, 16); const length = try std.fmt.parseInt(usize, length_str, 16);
// see above // 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); const ret = try allocator.alloc(u8, len);
ret[0] = if (ret.len < length) 'l' else 'm'; 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 }; return .{ .alloc = ret };
} }
@@ -293,7 +310,7 @@ pub fn checksum(input: []const u8) u8 {
var sum: usize = 0; var sum: usize = 0;
for (input) |char| sum += char; for (input) |char| sum += char;
return @truncate(u8, sum); return @truncate(sum);
} }
fn verify(input: []const u8, chksum: u8) bool { fn verify(input: []const u8, chksum: u8) bool {

View File

@@ -1,87 +1,43 @@
const std = @import("std"); const std = @import("std");
const network = @import("network");
const Packet = @import("Packet.zig"); const Packet = @import("Packet.zig");
const Socket = network.Socket;
const Allocator = std.mem.Allocator;
const Emulator = @import("lib.zig").Emulator; const Emulator = @import("lib.zig").Emulator;
const Allocator = std.mem.Allocator;
const Server = std.net.Server;
const Self = @This(); const Self = @This();
const log = std.log.scoped(.Server); const log = std.log.scoped(.Server);
const port: u16 = 2424; 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? // FIXME: Shouldn't this be a Packet Struct?
pkt_cache: ?[]const u8 = null, pkt_cache: ?[]const u8 = null,
client: Socket, socket: Server,
_socket: Socket, state: State,
emu: Emulator, emu: Emulator,
pub fn init(emulator: Emulator) !Self { pub const State = struct {
try network.init(); should_quit: bool = false,
target_xml: []const u8,
memmap_xml: ?[]const u8,
};
var socket = try Socket.create(.ipv4, .tcp); const Xml = struct { target: []const u8, memory_map: ?[]const u8 };
try socket.bindToPort(port);
try socket.listen();
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(); return .{
log.info("client connected from {}", .{endpoint}); .emu = emulator,
.socket = try localhost.listen(.{}),
return .{ .emu = emulator, ._socket = socket, .client = client }; .state = .{ .target_xml = xml.target, .memmap_xml = xml.memory_map },
};
} }
pub fn deinit(self: *Self, allocator: Allocator) void { pub fn deinit(self: *Self, allocator: Allocator) void {
self.reset(allocator); self.reset(allocator);
self.socket.deinit();
self.client.close();
self._socket.close();
network.deinit();
self.* = undefined; self.* = undefined;
} }
@@ -94,20 +50,30 @@ const Action = union(enum) {
nack, 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; var buf: [Packet.max_len]u8 = undefined;
while (true) { var client = try self.socket.accept();
const len = try self.client.receive(&buf); 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; if (len == 0) break;
const action = try self.parse(allocator, buf[0..len]); 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 { fn parse(self: *Self, allocator: Allocator, input: []const u8) !Action {
// log.debug("-> {s}", .{input}); log.debug("-> {s}", .{input});
return switch (input[0]) { return switch (input[0]) {
'+' => blk: { '+' => blk: {
@@ -129,7 +95,7 @@ fn handlePacket(self: *Self, allocator: Allocator, input: []const u8) !Action {
var packet = Packet.from(allocator, input) catch return .nack; var packet = Packet.from(allocator, input) catch return .nack;
defer packet.deinit(allocator); 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); defer string.deinit(allocator);
const reply = string.inner(); const reply = string.inner();
@@ -140,11 +106,11 @@ fn handlePacket(self: *Self, allocator: Allocator, input: []const u8) !Action {
return .{ .send = response }; 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) { switch (action) {
.send => |pkt| { .send => |pkt| {
_ = try self.client.send(pkt); _ = try client.stream.writeAll(pkt);
// log.debug("<- {s}", .{pkt}); log.debug("<- {s}", .{pkt});
self.reset(allocator); self.reset(allocator);
self.pkt_cache = pkt; 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}); log.warn("received nack, resending: \"{?s}\"", .{self.pkt_cache});
if (self.pkt_cache) |pkt| { if (self.pkt_cache) |pkt| {
_ = try self.client.send(pkt); _ = try client.stream.writeAll(pkt);
// log.debug("<- {s}", .{pkt}); log.debug("<- {s}", .{pkt});
} }
}, },
.ack => { .ack => {
_ = try self.client.send("+"); _ = try client.stream.writeAll("+");
// log.debug("<- +", .{}); log.debug("<- +", .{});
self.reset(allocator); self.reset(allocator);
}, },
.nack => { .nack => {
_ = try self.client.send("-"); _ = try client.stream.writeAll("-");
// log.debug("<- -", .{}); log.debug("<- -", .{});
self.reset(allocator); self.reset(allocator);
}, },

View File

@@ -25,7 +25,7 @@ const SwBkpt = struct {
} }
pub fn deinit(self: *@This()) void { pub fn deinit(self: *@This()) void {
self.deinit(); self.list.deinit();
self.* = undefined; self.* = undefined;
} }
@@ -47,7 +47,7 @@ const SwBkpt = struct {
} }
pub fn remove(self: *@This(), addr: u32) void { pub fn remove(self: *@This(), addr: u32) void {
for (self.list.items) |bkpt, i| { for (self.list.items, 0..) |bkpt, i| {
if (bkpt.addr == addr) { if (bkpt.addr == addr) {
_ = self.list.orderedRemove(i); _ = self.list.orderedRemove(i);
log.debug("Removed Breakpoint at 0x{X:0>8}", .{addr}); log.debug("Removed Breakpoint at 0x{X:0>8}", .{addr});
@@ -73,7 +73,7 @@ const HwBkpt = struct {
} }
pub fn add(self: *@This(), addr: u32, kind: u32) !void { pub fn add(self: *@This(), addr: u32, kind: u32) !void {
for (self.list) |*bkpt_opt| { for (&self.list) |*bkpt_opt| {
if (bkpt_opt.*) |bkpt| { if (bkpt_opt.*) |bkpt| {
if (bkpt.addr == addr) return; // idempotent if (bkpt.addr == addr) return; // idempotent
} else { } else {
@@ -88,7 +88,7 @@ const HwBkpt = struct {
} }
pub fn remove(self: *@This(), addr: u32) void { pub fn remove(self: *@This(), addr: u32) void {
for (self.list) |*bkpt_opt| { for (&self.list) |*bkpt_opt| {
const bkpt = bkpt_opt.* orelse continue; const bkpt = bkpt_opt.* orelse continue;
if (bkpt.addr == addr) { if (bkpt.addr == addr) {

View File

@@ -35,35 +35,33 @@ pub const Emulator = struct {
if (ptr_info != .Pointer) @compileError("ptr must be a pointer"); if (ptr_info != .Pointer) @compileError("ptr must be a pointer");
if (ptr_info.Pointer.size != .One) @compileError("ptr must be a single-item pointer"); if (ptr_info.Pointer.size != .One) @compileError("ptr must be a single-item pointer");
const alignment = ptr_info.Pointer.alignment;
const gen = struct { const gen = struct {
pub fn readImpl(pointer: *anyopaque, addr: u32) u8 { 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 }); return @call(.always_inline, ptr_info.Pointer.child.read, .{ self, addr });
} }
pub fn writeImpl(pointer: *anyopaque, addr: u32, value: u8) void { 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 }); return @call(.always_inline, ptr_info.Pointer.child.write, .{ self, addr, value });
} }
pub fn registersImpl(pointer: *anyopaque) *[16]u32 { 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}); return @call(.always_inline, ptr_info.Pointer.child.registers, .{self});
} }
pub fn cpsrImpl(pointer: *anyopaque) u32 { 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}); return @call(.always_inline, ptr_info.Pointer.child.cpsr, .{self});
} }
pub fn stepImpl(pointer: *anyopaque) void { 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 @call(.always_inline, ptr_info.Pointer.child.step, .{self});
} }
@@ -144,3 +142,7 @@ pub const Emulator = struct {
} }
} }
}; };
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();
}