Compare commits

...

8 Commits

Author SHA1 Message Date
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
6 changed files with 332 additions and 64 deletions

View File

@@ -1,54 +1,49 @@
const std = @import("std"); const std = @import("std");
const CompileStep = std.Build.CompileStep;
fn path(comptime suffix: []const u8) []const u8 { fn path(comptime suffix: []const u8) []const u8 {
if (suffix[0] == '/') @compileError("expected a relative path"); if (suffix[0] == '/') @compileError("expected a relative path");
return comptime (std.fs.path.dirname(@src().file) orelse ".") ++ std.fs.path.sep_str ++ suffix; return comptime (std.fs.path.dirname(@src().file) orelse ".") ++ std.fs.path.sep_str ++ suffix;
} }
const pkgs = struct { pub fn link(exe: *CompileStep) void {
const Pkg = std.build.Pkg; // create zig-network module
const b = exe.builder;
pub const gdbstub: Pkg = .{
.name = "gdbstub",
.source = .{ .path = path("src/lib.zig") },
.dependencies = &[_]Pkg{network},
};
// https://github.com/MasterQ32/zig-network // https://github.com/MasterQ32/zig-network
pub const network: Pkg = .{ const network = b.createModule(.{ .source_file = .{ .path = path("lib/zig-network/network.zig") } });
.name = "network",
.source = .{ .path = path("lib/zig-network/network.zig") },
};
};
pub fn link(exe: *std.build.LibExeObjStep) void { const gdbstub = b.createModule(.{
exe.addPackage(pkgs.gdbstub); .source_file = .{ .path = path("src/lib.zig") },
.dependencies = &.{.{ .name = "network", .module = network }},
});
exe.addModule("gdbstub", gdbstub);
} }
pub fn build(b: *std.build.Builder) void { pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
_ = target; // const optimize = b.standardOptimizeOption(.{});
const mode = b.standardReleaseOptions();
// -- library -- // -- Library --
const lib = b.addStaticLibrary("gdbstub", "src/lib.zig");
lib.addPackage(pkgs.network);
lib.setBuildMode(mode); const lib_test = b.addTest(.{
lib.install(); .root_source_file = .{ .path = "src/lib.zig" },
.target = target,
});
const lib_tests = b.addTest("src/lib.zig"); const test_step = b.step("test", "Run Library Tests");
lib_tests.setBuildMode(mode); test_step.dependOn(&lib_test.step);
const test_step = b.step("lib-test", "Run Library Tests"); // -- Executable --
test_step.dependOn(&lib_tests.step);
// // -- Executable -- // const exe = b.addExecutable(.{
// const exe = b.addExecutable("gdbserver", "src/main.zig"); // .name = "gdbserver",
// .root_source_file = .{ .path = "src/main.zig" },
// .target = target,
// .optimize = optimize,
// });
// link(exe); // link(exe);
// exe.setTarget(target);
// exe.setBuildMode(mode);
// exe.install(); // exe.install();
// const run_cmd = exe.run(); // const run_cmd = exe.run();

View File

@@ -2,6 +2,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 target = @import("Server.zig").target; const target = @import("Server.zig").target;
const memory_map = @import("Server.zig").memory_map; const memory_map = @import("Server.zig").memory_map;
@@ -45,13 +46,10 @@ const String = union(enum) {
} }
}; };
pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String { pub fn parse(self: *Self, allocator: Allocator, emu: *Emulator) !String {
switch (self.contents[0]) { switch (self.contents[0]) {
// Required // Required
'?' => { '?' => return .{ .static = "T05" }, // FIXME: which errno?
const ret = try std.fmt.allocPrint(allocator, "S{x:0>2}", .{@enumToInt(Signal.Int)});
return .{ .alloc = ret };
},
'g' => { 'g' => {
const r = emu.registers(); const r = emu.registers();
const cpsr = emu.cpsr(); const cpsr = emu.cpsr();
@@ -76,8 +74,8 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
'G' => @panic("TODO: Register Write"), 'G' => @panic("TODO: Register Write"),
'm' => { 'm' => {
var tokens = std.mem.tokenize(u8, self.contents[1..], ","); var tokens = std.mem.tokenize(u8, self.contents[1..], ",");
const addr_str = tokens.next() orelse return .{ .static = "E9999" }; // EUNKNOWN const addr_str = tokens.next() orelse return error.InvalidPacket;
const length_str = tokens.next() orelse return .{ .static = "E9999" }; // EUNKNOWN const length_str = tokens.next() orelse return error.InvalidPacket;
const addr = try std.fmt.parseInt(u32, addr_str, 16); const addr = try std.fmt.parseInt(u32, addr_str, 16);
const len = try std.fmt.parseInt(u32, length_str, 16); const len = try std.fmt.parseInt(u32, length_str, 16);
@@ -87,9 +85,6 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
{ {
var i: u32 = 0; var i: u32 = 0;
while (i < len) : (i += 1) { 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 // 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 }); _ = std.fmt.bufPrintIntToSlice(ret[i * 2 ..][0..2], emu.read(addr + i), 16, .lower, .{ .fill = '0', .width = 2 });
} }
@@ -97,40 +92,124 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
return .{ .alloc = ret }; return .{ .alloc = ret };
}, },
'M' => @panic("TODO: Memory Write"), 'M' => {
'c' => @panic("TODO: Continue"), 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' => { 's' => {
// var tokens = std.mem.tokenize(u8, self.contents[1..], " "); // var tokens = std.mem.tokenize(u8, self.contents[1..], " ");
// const addr = if (tokens.next()) |s| try std.fmt.parseInt(u32, s, 16) else null; // const addr = if (tokens.next()) |s| try std.fmt.parseInt(u32, s, 16) else null;
emu.step(); switch (emu.step()) {
.SingleStep => return .{ .static = "T05" },
const ret = try std.fmt.allocPrint(allocator, "S{x:0>2}", .{@enumToInt(Signal.Trap)}); .Trap => |r| switch (r) {
return .{ .alloc = ret }; .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' => { 'D' => {
log.info("Disconnecting...", .{}); log.info("Disconnecting...", .{});
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});
} }
log.warn("Unimplemented: {s}", .{self.contents});
return .{ .static = "" }; return .{ .static = "" };
}, },
'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" };
if (substr(self.contents[1..], "fThreadInfo")) return .{ .static = "m1" }; if (substr(self.contents[1..], "fThreadInfo")) return .{ .static = "m1" };
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..], "Supported")) {
const format = "PacketSize={x:};qXfer:features:read+;qXfer:memory-map:read+"; const format = "PacketSize={x:};swbreak+;hwbreak+;qXfer:features:read+;qXfer:memory-map:read+";
// TODO: Anything else? // TODO: Anything else?
const ret = try std.fmt.allocPrint(allocator, format, .{Self.max_len}); const ret = try std.fmt.allocPrint(allocator, format, .{Self.max_len});
@@ -142,9 +221,10 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
_ = tokens.next(); // Xfer _ = tokens.next(); // Xfer
_ = tokens.next(); // features _ = tokens.next(); // features
_ = tokens.next(); // read _ = tokens.next(); // read
const annex = tokens.next() orelse return .{ .static = "E9999" };
const offset_str = tokens.next() orelse return .{ .static = "E99999" }; const annex = tokens.next() orelse return error.InvalidPacket;
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;
if (std.mem.eql(u8, annex, "target.xml")) { if (std.mem.eql(u8, annex, "target.xml")) {
const offset = try std.fmt.parseInt(usize, offset_str, 16); const offset = try std.fmt.parseInt(usize, offset_str, 16);
@@ -173,8 +253,8 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
_ = tokens.next(); // Xfer _ = tokens.next(); // Xfer
_ = tokens.next(); // memory-map _ = tokens.next(); // memory-map
_ = tokens.next(); // read _ = tokens.next(); // read
const offset_str = tokens.next() orelse return .{ .static = "E9999" }; const offset_str = tokens.next() orelse return error.InvalidPacket;
const length_str = tokens.next() orelse return .{ .static = "E9999" }; const length_str = tokens.next() orelse return error.InvalidPacket;
const offset = try std.fmt.parseInt(usize, offset_str, 16); const offset = try std.fmt.parseInt(usize, offset_str, 16);
const length = try std.fmt.parseInt(usize, length_str, 16); const length = try std.fmt.parseInt(usize, length_str, 16);

View File

@@ -4,6 +4,7 @@ const Packet = @import("Packet.zig");
const Socket = network.Socket; const Socket = network.Socket;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Atomic = std.atomic.Atomic;
const Emulator = @import("lib.zig").Emulator; const Emulator = @import("lib.zig").Emulator;
const Self = @This(); const Self = @This();
@@ -94,16 +95,21 @@ const Action = union(enum) {
nack, nack,
}; };
pub fn run(self: *Self, allocator: Allocator) !void { pub fn run(self: *Self, allocator: Allocator, quit: *Atomic(bool)) !void {
var buf: [Packet.max_len]u8 = undefined; var buf: [Packet.max_len]u8 = undefined;
while (true) { while (true) {
const len = try self.client.receive(&buf); const len = try self.client.receive(&buf);
if (len == 0) break; if (len == 0) break;
if (quit.load(.Monotonic)) 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, action);
} }
// Just in case its the gdbstub that exited first,
// attempt to signal to the GUI that it should also exit
quit.store(true, .Monotonic);
} }
fn parse(self: *Self, allocator: Allocator, input: []const u8) !Action { fn parse(self: *Self, allocator: Allocator, input: []const u8) !Action {
@@ -129,7 +135,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.emu) catch return .nack;
defer string.deinit(allocator); defer string.deinit(allocator);
const reply = string.inner(); const reply = string.inner();

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 /// Re-export of the server interface
pub const Server = @import("Server.zig"); pub const Server = @import("Server.zig");
const State = @import("State.zig");
/// Interface for interacting between GDB and a GBA emu /// Interface for interacting between GDB and a GBA emu
pub const Emulator = struct { pub const Emulator = struct {
const Self = @This(); const Self = @This();
const Signal = union(enum) {
const Kind = enum { HwBkpt, SwBkpt };
Trap: Kind,
SingleStep: void,
};
state: State,
ptr: *anyopaque, ptr: *anyopaque,
readFn: *const fn (*anyopaque, u32) u8, readFn: *const fn (*anyopaque, u32) u8,
@@ -15,7 +28,7 @@ pub const Emulator = struct {
stepFn: *const fn (*anyopaque) void, 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 = @TypeOf(ptr);
const ptr_info = @typeInfo(Ptr); const ptr_info = @typeInfo(Ptr);
@@ -56,7 +69,21 @@ pub const Emulator = struct {
} }
}; };
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 { pub inline fn read(self: Self, addr: u32) u8 {
@@ -75,7 +102,45 @@ pub const Emulator = struct {
return self.cpsrFn(self.ptr); 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); 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),
}
} }
}; };