Compare commits

..

9 Commits

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

View File

@@ -1,54 +1,44 @@
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;
}
const pkgs = struct {
const Pkg = std.build.Pkg;
pub const gdbstub: Pkg = .{
.name = "gdbstub",
.source = .{ .path = path("src/lib.zig") },
.dependencies = &[_]Pkg{network},
};
pub fn getModule(b: *std.Build) *std.build.Module {
// https://github.com/MasterQ32/zig-network
pub const network: Pkg = .{
.name = "network",
.source = .{ .path = path("lib/zig-network/network.zig") },
};
};
const network = b.createModule(.{ .source_file = .{ .path = path("lib/zig-network/network.zig") } });
pub fn link(exe: *std.build.LibExeObjStep) void {
exe.addPackage(pkgs.gdbstub);
return b.createModule(.{
.source_file = .{ .path = path("src/lib.zig") },
.dependencies = &.{.{ .name = "network", .module = network }},
});
}
pub fn build(b: *std.build.Builder) void {
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
_ = target;
const mode = b.standardReleaseOptions();
// const optimize = b.standardOptimizeOption(.{});
// -- library --
const lib = b.addStaticLibrary("gdbstub", "src/lib.zig");
lib.addPackage(pkgs.network);
// -- Library --
lib.setBuildMode(mode);
lib.install();
const lib_test = b.addTest(.{
.root_source_file = .{ .path = "src/lib.zig" },
.target = target,
});
const lib_tests = b.addTest("src/lib.zig");
lib_tests.setBuildMode(mode);
const test_step = b.step("test", "Run Library Tests");
test_step.dependOn(&lib_test.step);
const test_step = b.step("lib-test", "Run Library Tests");
test_step.dependOn(&lib_tests.step);
// -- Executable --
// // -- Executable --
// const exe = b.addExecutable("gdbserver", "src/main.zig");
// const exe = b.addExecutable(.{
// .name = "gdbserver",
// .root_source_file = .{ .path = "src/main.zig" },
// .target = target,
// .optimize = optimize,
// });
// link(exe);
// exe.setTarget(target);
// exe.setBuildMode(mode);
// exe.install();
// const run_cmd = exe.run();

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const Emulator = @import("lib.zig").Emulator;
const State = @import("State.zig");
const target = @import("Server.zig").target;
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]) {
// 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();
@@ -76,8 +74,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 +85,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,40 +92,124 @@ 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...", .{});
return .{ .static = "OK" };
},
'H' => return .{ .static = "" },
'v' => {
if (substr(self.contents[1..], "MustReplyEmpty")) {
return .{ .static = "" };
if (!substr(self.contents[1..], "MustReplyEmpty")) {
log.warn("Unimplemented: {s}", .{self.contents});
}
log.warn("Unimplemented: {s}", .{self.contents});
return .{ .static = "" };
},
'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+";
const format = "PacketSize={x:};swbreak+;hwbreak+;qXfer:features:read+;qXfer:memory-map:read+";
// TODO: Anything else?
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(); // 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);
@@ -173,8 +253,8 @@ pub fn parse(self: *Self, allocator: Allocator, emu: Emulator) !String {
_ = 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);

View File

@@ -4,6 +4,7 @@ const Packet = @import("Packet.zig");
const Socket = network.Socket;
const Allocator = std.mem.Allocator;
const Atomic = std.atomic.Atomic;
const Emulator = @import("lib.zig").Emulator;
const Self = @This();
@@ -94,16 +95,21 @@ const Action = union(enum) {
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;
while (true) {
const len = try self.client.receive(&buf);
if (len == 0) break;
if (quit.load(.Monotonic)) break;
const action = try self.parse(allocator, buf[0..len]);
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 {
@@ -129,7 +135,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.emu) catch return .nack;
defer string.deinit(allocator);
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
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,7 +28,7 @@ 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);
@@ -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 {
@@ -75,7 +102,45 @@ 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),
}
}
};