Skip to content
2 changes: 1 addition & 1 deletion Sources/CLI/DebuggerServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
// isn't taking down the entire server. In our case we need to be able to shut down the server on
// debugger client's request, so let's wrap the discarding task group with a throwing task group
// for cancellation.
try await withThrowingTaskGroup { cancellableGroup in
await withThrowingTaskGroup { cancellableGroup in
// Use `AsyncStream` for sending a signal out of the discarding group.
let (shutDownStream, shutDownContinuation) = AsyncStream<()>.makeStream()

Expand Down
92 changes: 60 additions & 32 deletions Sources/GDBRemoteProtocol/GDBHostCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ package struct GDBHostCommand: Equatable {
case resumeThreads
case `continue`
case kill
case insertSoftwareBreakpoint
case removeSoftwareBreakpoint

case generalRegisters

Expand Down Expand Up @@ -108,46 +110,72 @@ package struct GDBHostCommand: Equatable {
/// Arguments supplied with a host command.
package let arguments: String

/// Helper type for representing parsing prefixes in host commands.
private struct ParsingRule {
/// Kind of the host command parsed by this rul.
let kind: Kind

/// String prefix required for the raw string to match for the rule
/// to yield a parsed command.
let prefix: String

/// Whether command arguments use a `:` delimiter, which usually otherwise
/// separates command kind from arguments.
var argumentsContainColonDelimiter = false
}

private static let parsingRules: [ParsingRule] = [
.init(
kind: .readMemoryBinaryData,
prefix: "x",
),
.init(
kind: .readMemory,
prefix: "m",
),
.init(
kind: .insertSoftwareBreakpoint,
prefix: "Z0",
),
.init(
kind: .removeSoftwareBreakpoint,
prefix: "z0",
),
.init(
kind: .registerInfo,
prefix: "qRegisterInfo",
),
.init(
kind: .threadStopInfo,
prefix: "qThreadStopInfo",
),
.init(
kind: .resumeThreads,
prefix: "vCont;",
argumentsContainColonDelimiter: true
),
]

/// Initialize a host command from raw strings sent from a host.
/// - Parameters:
/// - kindString: raw ``String`` that denotes kind of the command.
/// - arguments: raw arguments that immediately follow kind of the command.
package init(kindString: String, arguments: String) throws(GDBHostCommandDecoder.Error) {
let registerInfoPrefix = "qRegisterInfo"
let threadStopInfoPrefix = "qThreadStopInfo"
let resumeThreadsPrefix = "vCont"
for rule in Self.parsingRules {
if kindString.starts(with: rule.prefix) {
self.kind = rule.kind
let prependedArguments = kindString.dropFirst(rule.prefix.count)

if kindString.starts(with: "x") {
self.kind = .readMemoryBinaryData
self.arguments = String(kindString.dropFirst())
return
} else if kindString.starts(with: "m") {
self.kind = .readMemory
self.arguments = String(kindString.dropFirst())
return
} else if kindString.starts(with: registerInfoPrefix) {
self.kind = .registerInfo

guard arguments.isEmpty else {
throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue
}
self.arguments = String(kindString.dropFirst(registerInfoPrefix.count))
return
} else if kindString.starts(with: threadStopInfoPrefix) {
self.kind = .threadStopInfo

guard arguments.isEmpty else {
throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue
if rule.argumentsContainColonDelimiter {
self.arguments = "\(prependedArguments):\(arguments)"
} else {
self.arguments = prependedArguments + arguments
}
return
}
self.arguments = String(kindString.dropFirst(registerInfoPrefix.count))
return
} else if kindString != "vCont?" && kindString.starts(with: resumeThreadsPrefix) {
self.kind = .resumeThreads
}

// Strip the prefix and a semicolon ';' delimiter, append arguments back with the original delimiter.
self.arguments = String(kindString.dropFirst(resumeThreadsPrefix.count + 1)) + ":" + arguments
return
} else if let kind = Kind(rawValue: kindString) {
if let kind = Kind(rawValue: kindString) {
self.kind = kind
} else {
throw GDBHostCommandDecoder.Error.unknownCommand(kind: kindString, arguments: arguments)
Expand Down
68 changes: 50 additions & 18 deletions Sources/WasmKit/Execution/Debugger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
case instantiated
case stoppedAtBreakpoint(BreakpointState)
case trapped(String)
case wasiModuleExited(exitCode: UInt32)
case entrypointReturned([Value])
}

Expand Down Expand Up @@ -46,6 +45,11 @@

private var pc = Pc.allocate(capacity: 1)

/// Addresses of functions in the original Wasm binary, used for looking up functions when a breakpoint
/// is enabled at an arbitrary address if it isn't present in ``InstructionMapping`` yet (i.e. the
/// was not compiled yet in lazy compilation mode).
private let functionAddresses: [(address: Int, instanceFunctionIndex: Int)]

/// Initializes a new debugger state instance.
/// - Parameters:
/// - module: Wasm module to instantiate.
Expand All @@ -60,6 +64,14 @@
}

self.instance = instance
self.functionAddresses = instance.handle.functions.enumerated().filter { $0.element.isWasm }.lazy.map {
switch $0.element.wasm.code {
case .uncompiled(let wasm), .debuggable(let wasm, _):
return (address: wasm.originalAddress, instanceFunctionIndex: $0.offset)
case .compiled:
fatalError()
}
}
self.module = module
self.entrypointFunction = entrypointFunction
self.valueStack = UnsafeMutablePointer<StackSlot>.allocate(capacity: limit)
Expand Down Expand Up @@ -93,38 +105,52 @@
}
}

private func findIseq(forWasmAddress address: Int) throws -> (iseq: Pc, wasm: Int) {
if let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) {
return (iseq, wasm)
}

let followingIndex = self.functionAddresses.firstIndex(where: { $0.address > address }) ?? self.functionAddresses.endIndex
let functionIndex = self.functionAddresses[followingIndex - 1].instanceFunctionIndex
let function = instance.handle.functions[functionIndex]
try function.wasm.ensureCompiled(store: StoreRef(self.store))

if let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) {
return (iseq, wasm)
}

throw Error.noInstructionMappingAvailable(address)
}

/// Enables a breakpoint at a given Wasm address.
/// - Parameter address: byte offset of the Wasm instruction that will be replaced with a breakpoint. If no
/// direct internal bytecode matching instruction is found, the next closest internal bytecode instruction
/// is replaced with a breakpoint. The original instruction to be restored is preserved in debugger state
/// represented by `self`.
/// See also ``Debugger/disableBreakpoint(address:)``.
package mutating func enableBreakpoint(address: Int) throws(Error) {
@discardableResult
package mutating func enableBreakpoint(address: Int) throws -> Int {
guard self.breakpoints[address] == nil else {
return
}

guard let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else {
throw Error.noInstructionMappingAvailable(address)
return address
}

let (iseq, wasm) = try self.findIseq(forWasmAddress: address)
self.breakpoints[wasm] = iseq.pointee
iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel)
return wasm
}

/// Disables a breakpoint at a given Wasm address. If no breakpoint at a given address was previously set with
/// `self.enableBreakpoint(address:), this function immediately returns.
/// - Parameter address: byte offset of the Wasm instruction that was replaced with a breakpoint. The original
/// instruction is restored from debugger state and replaces the breakpoint instruction.
/// See also ``Debugger/enableBreakpoint(address:)``.
package mutating func disableBreakpoint(address: Int) throws(Error) {
package mutating func disableBreakpoint(address: Int) throws {
guard let oldCodeSlot = self.breakpoints[address] else {
return
}

guard let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else {
throw Error.noInstructionMappingAvailable(address)
}
let (iseq, wasm) = try self.findIseq(forWasmAddress: address)

self.breakpoints[wasm] = nil
iseq.pointee = oldCodeSlot
Expand All @@ -135,7 +161,8 @@
/// executed. If the module is not stopped at a breakpoint, this function returns immediately.
package mutating func run() throws {
do {
if case .stoppedAtBreakpoint(let breakpoint) = self.state {
switch self.state {
case .stoppedAtBreakpoint(let breakpoint):
// Remove the breakpoint before resuming
try self.disableBreakpoint(address: breakpoint.wasmPc)
self.execution.resetError()
Expand Down Expand Up @@ -169,9 +196,10 @@
self.state = .entrypointReturned(
type.results.enumerated().map { (i, type) in
sp[VReg(i)].cast(to: type)
})
}
)
}
} else {
case .instantiated:
let result = try self.execution.executeWasm(
threadingModel: self.threadingModel,
function: self.entrypointFunction.handle,
Expand All @@ -181,6 +209,9 @@
pc: self.pc
)
self.state = .entrypointReturned(result)

case .trapped, .entrypointReturned:
fatalError("Restarting a Wasm module from the debugger is not implemented yet.")
}
} catch let breakpoint as Execution.Breakpoint {
let pc = breakpoint.pc
Expand Down Expand Up @@ -212,10 +243,11 @@
return []
}

var result = Execution.captureBacktrace(sp: breakpoint.iseq.sp, store: self.store).symbols.compactMap {
return self.instance.handle.instructionMapping.findWasm(forIseqAddress: $0.address)
}
result.append(breakpoint.wasmPc)
var result = [breakpoint.wasmPc]
result.append(
contentsOf: Execution.captureBacktrace(sp: breakpoint.iseq.sp, store: self.store).symbols.compactMap {
return self.instance.handle.instructionMapping.findWasm(forIseqAddress: $0.address)
})

return result
}
Expand Down
6 changes: 4 additions & 2 deletions Sources/WasmKit/Execution/Function.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,12 @@ extension InternalFunction {
function: EntityHandle<WasmFunctionEntity>
) {
let entity = self.wasm
guard case .compiled(let iseq) = entity.code else {
switch entity.code {
case .compiled(let iseq), .debuggable(_, let iseq):
return (iseq, entity.numberOfNonParameterLocals, entity)
case .uncompiled:
preconditionFailure()
}
return (iseq, entity.numberOfNonParameterLocals, entity)
}
}

Expand Down
40 changes: 37 additions & 3 deletions Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
case hostCommandNotImplemented(GDBHostCommand.Kind)
case exitCodeUnknown([Value])
case killRequestReceived
case unknownHexEncodedArguments(String)
}

private let wasmBinary: ByteBuffer
Expand Down Expand Up @@ -91,6 +92,20 @@
return buffer.hexDump(format: .compact)
}

private func firstHexArgument<I: FixedWidthInteger>(argumentsString: String, separator: Character, endianness: Endianness) throws -> I {
guard let hexString = argumentsString.split(separator: separator).first else {
throw Error.unknownHexEncodedArguments(argumentsString)
}

var hexBuffer = try self.allocator.buffer(plainHexEncodedBytes: String(hexString))

guard let argument = hexBuffer.readInteger(endianness: endianness, as: I.self) else {
throw Error.unknownHexEncodedArguments(argumentsString)
}

return argument
}

var currentThreadStopInfo: GDBTargetResponse.Kind {
get throws {
var result: [(String, String)] = [
Expand All @@ -106,9 +121,6 @@
result.append(("reason", "trace"))
return .keyValuePairs(result)

case .wasiModuleExited(let exitCode):
return .string("W\(self.hexDump(exitCode, endianness: .big))")

case .entrypointReturned(let values):
guard !values.isEmpty else {
return .string("W\(self.hexDump(0 as UInt8, endianness: .big))")
Expand Down Expand Up @@ -265,6 +277,28 @@
case .kill:
throw Error.killRequestReceived

case .insertSoftwareBreakpoint:
try self.debugger.enableBreakpoint(
address: Int(
self.firstHexArgument(
argumentsString: command.arguments,
separator: ",",
endianness: .big
) - codeOffset)
)
responseKind = .ok

case .removeSoftwareBreakpoint:
try self.debugger.disableBreakpoint(
address: Int(
self.firstHexArgument(
argumentsString: command.arguments,
separator: ",",
endianness: .big
) - codeOffset)
)
responseKind = .ok

case .generalRegisters:
throw Error.hostCommandNotImplemented(command.kind)
}
Expand Down
Loading
Loading