From 136c7a94cb0195b1b24fdc561235f700b9cef7e1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 1 Nov 2025 00:07:18 +0000 Subject: [PATCH 01/23] Add `GDBHostCommand.ParsingRule` type for better parsing --- .../GDBRemoteProtocol/GDBHostCommand.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 4bf24467..e25750d8 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -45,6 +45,8 @@ package struct GDBHostCommand: Equatable { case resumeThreads case `continue` case kill + case insertSoftwareBreakpoint + case removeSoftwareBreakpoint case generalRegisters @@ -108,6 +110,45 @@ package struct GDBHostCommand: Equatable { /// Arguments supplied with a host command. package let arguments: String + struct ParsingRule { + let kind: Kind + let prefix: String + var separator: String? = nil + } + + static let parsingRules: [ParsingRule] = [ + .init( + kind: .readMemoryBinaryData, + prefix: "x", + ), + .init( + kind: .readMemory, + prefix: "m", + ), + .init( + kind: .insertSoftwareBreakpoint, + prefix: "Z0", + separator: ",", + ), + .init( + kind: .removeSoftwareBreakpoint, + prefix: "z0", + separator: ",", + ), + .init( + kind: .registerInfo, + prefix: "qRegisterInfo", + ), + .init( + kind: .threadStopInfo, + prefix: "qThreadStopInfo", + ), + .init( + kind: .resumeThreads, + prefix: "vCont;" + ) + ] + /// Initialize a host command from raw strings sent from a host. /// - Parameters: /// - kindString: raw ``String`` that denotes kind of the command. From 20a21019a419cb3f358dcf470aac459438b61368 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 3 Nov 2025 12:54:38 +0000 Subject: [PATCH 02/23] Enable breakpoint commands --- .../GDBRemoteProtocol/GDBHostCommand.swift | 42 ++++--------------- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 24 +++++++++++ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index e25750d8..c4bc0525 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -146,7 +146,7 @@ package struct GDBHostCommand: Equatable { .init( kind: .resumeThreads, prefix: "vCont;" - ) + ), ] /// Initialize a host command from raw strings sent from a host. @@ -154,41 +154,15 @@ package struct GDBHostCommand: Equatable { /// - 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" - - 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 + for rule in Self.parsingRules { + if kindString.starts(with: rule.prefix) { + self.kind = rule.kind + self.arguments = String(kindString.dropFirst(rule.prefix.count)) + arguments + return } - 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 - } - 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) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 6cacef1e..6332e665 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -49,6 +49,7 @@ case hostCommandNotImplemented(GDBHostCommand.Kind) case exitCodeUnknown([Value]) case killRequestReceived + case unknownHexEncodedArguments(String) } private let wasmBinary: ByteBuffer @@ -91,6 +92,20 @@ return buffer.hexDump(format: .compact) } + private func firstHexArgument(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)] = [ @@ -265,6 +280,15 @@ case .kill: throw Error.killRequestReceived + + case .insertSoftwareBreakpoint: + try self.debugger.disableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) + responseKind = .ok + + case.removeSoftwareBreakpoint: + try self.debugger.enableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) + responseKind = .ok + case .generalRegisters: throw Error.hostCommandNotImplemented(command.kind) } From 10d1c0b9c5e2632fd0f8b7ec8907ca35c4f26025 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 3 Nov 2025 12:55:10 +0000 Subject: [PATCH 03/23] format --- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 6332e665..5a458aff 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -280,12 +280,11 @@ case .kill: throw Error.killRequestReceived - case .insertSoftwareBreakpoint: try self.debugger.disableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) responseKind = .ok - case.removeSoftwareBreakpoint: + case .removeSoftwareBreakpoint: try self.debugger.enableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) responseKind = .ok From bd79763855a3288cdbfde088947a277fb7b29041 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 3 Nov 2025 15:30:21 +0000 Subject: [PATCH 04/23] Fix multiple bugs in breakpoint handling --- Sources/CLI/DebuggerServer.swift | 2 +- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 17 +++++++++++++---- Sources/WasmKit/Execution/Debugger.swift | 7 ++++++- Sources/WasmKit/Execution/Function.swift | 6 ++++-- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 10 +++++++--- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/Sources/CLI/DebuggerServer.swift b/Sources/CLI/DebuggerServer.swift index ccc9c5aa..3f348bb2 100644 --- a/Sources/CLI/DebuggerServer.swift +++ b/Sources/CLI/DebuggerServer.swift @@ -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() diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index c4bc0525..bf8425f4 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -114,6 +114,10 @@ package struct GDBHostCommand: Equatable { let kind: Kind let prefix: String var separator: String? = nil + + /// Whether command arguments us a `:` delimiter, which usually otherwise + /// separates command kind from arguments. + var argumentsContainColonDelimiter = false } static let parsingRules: [ParsingRule] = [ @@ -128,12 +132,10 @@ package struct GDBHostCommand: Equatable { .init( kind: .insertSoftwareBreakpoint, prefix: "Z0", - separator: ",", ), .init( kind: .removeSoftwareBreakpoint, prefix: "z0", - separator: ",", ), .init( kind: .registerInfo, @@ -145,7 +147,8 @@ package struct GDBHostCommand: Equatable { ), .init( kind: .resumeThreads, - prefix: "vCont;" + prefix: "vCont;", + argumentsContainColonDelimiter: true ), ] @@ -157,7 +160,13 @@ package struct GDBHostCommand: Equatable { for rule in Self.parsingRules { if kindString.starts(with: rule.prefix) { self.kind = rule.kind - self.arguments = String(kindString.dropFirst(rule.prefix.count)) + arguments + let prependedArguments = kindString.dropFirst(rule.prefix.count) + + if rule.argumentsContainColonDelimiter { + self.arguments = "\(prependedArguments):\(arguments)" + } else { + self.arguments = prependedArguments + arguments + } return } } diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 2a0620d8..4277492f 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,5 +1,7 @@ #if WasmDebuggingSupport + import struct WASI.WASIExitCode + /// Debugger state owner, driven by a debugger host. This implementation has no knowledge of the exact /// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. package struct Debugger: ~Copyable { @@ -169,7 +171,8 @@ self.state = .entrypointReturned( type.results.enumerated().map { (i, type) in sp[VReg(i)].cast(to: type) - }) + } + ) } } else { let result = try self.execution.executeWasm( @@ -189,6 +192,8 @@ } self.state = .stoppedAtBreakpoint(.init(iseq: breakpoint, wasmPc: wasmPc)) + } catch let error as WASIExitCode { + self.state = .wasiModuleExited(exitCode: error.code) } } diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 93aa8bde..c552d15e 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -216,10 +216,12 @@ extension InternalFunction { function: EntityHandle ) { 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) } } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 5a458aff..1db85c78 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -122,7 +122,11 @@ return .keyValuePairs(result) case .wasiModuleExited(let exitCode): - return .string("W\(self.hexDump(exitCode, endianness: .big))") + if exitCode > UInt8.max { + return .string("W\(self.hexDump(exitCode, endianness: .big))") + } else { + return .string("W\(self.hexDump(UInt8(exitCode), endianness: .big))") + } case .entrypointReturned(let values): guard !values.isEmpty else { @@ -281,11 +285,11 @@ throw Error.killRequestReceived case .insertSoftwareBreakpoint: - try self.debugger.disableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) + try self.debugger.enableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) responseKind = .ok case .removeSoftwareBreakpoint: - try self.debugger.enableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) + try self.debugger.disableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) responseKind = .ok case .generalRegisters: From ad8d11774abefd4bb683ea8ba0ea28048e4515f0 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 3 Nov 2025 17:55:01 +0000 Subject: [PATCH 05/23] Test coverage WIP --- Sources/WasmKit/Execution/Debugger.swift | 41 ++++++++++++++++++------ Tests/WasmKitTests/DebuggerTests.swift | 28 ++++++++++++++++ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 4277492f..1bcbed1e 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -48,6 +48,11 @@ private var pc = Pc.allocate(capacity: 1) + /// Addresses of functions in the original Wasm binary, used 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: [Int] + /// Initializes a new debugger state instance. /// - Parameters: /// - module: Wasm module to instantiate. @@ -61,6 +66,7 @@ throw Error.entrypointFunctionNotFound } + self.functionAddresses = module.functions.map(\.code.originalAddress) self.instance = instance self.module = module self.entrypointFunction = entrypointFunction @@ -81,7 +87,7 @@ /// Finds a Wasm address for the first instruction in a given function. /// - Parameter function: the Wasm function to find the first Wasm instruction address for. /// - Returns: byte offset of the first Wasm instruction of given function in the module it was parsed from. - private func originalAddress(function: Function) throws -> Int { + package func originalAddress(function: Function) throws -> Int { precondition(function.handle.isWasm) switch function.handle.wasm.code { @@ -95,6 +101,22 @@ } } + private func findIseq(forWasmAddress address: Int) throws(Error) -> (iseq: Pc, wasm: Int) { + if let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) { + return (iseq, wasm) + } + // else if let functionIndex = self.functionAddresses.firstIndex(where: { $0 > address }) - 1 { + // let function = self.module.functions[functionIndex] + // try function.handle.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 @@ -106,10 +128,7 @@ 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] = iseq.pointee iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) } @@ -124,9 +143,7 @@ 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 @@ -137,7 +154,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() @@ -174,7 +192,7 @@ } ) } - } else { + case .instantiated: let result = try self.execution.executeWasm( threadingModel: self.threadingModel, function: self.entrypointFunction.handle, @@ -184,6 +202,9 @@ pc: self.pc ) self.state = .entrypointReturned(result) + + case .trapped, .wasiModuleExited, .entrypointReturned: + fatalError("Restarting a WASI module from the debugger is not implemented yet.") } } catch let breakpoint as Execution.Breakpoint { let pc = breakpoint.pc diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index a12cc26e..d8e93df9 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -18,6 +18,24 @@ ) """ + private let multiFunctionWAT = """ + (module + (func (export "_start") (result i32) (local $x i32) + (i32.const 42) + (i32.const 0) + (i32.eqz) + (drop) + (local.set $x) + (local.get $x) + (call $f) + ) + + (func $f (param $a i32) (result i32) + (local.get $a) + ) + ) + """ + @Suite struct DebuggerTests { @Test @@ -48,6 +66,16 @@ } } + @Test + func lazyFunctionsCompilation() throws { + let store = Store(engine: Engine()) + let bytes = try wat2wasm(trivialModuleWAT) + let module = try parseWasm(bytes: bytes) + var debugger = try Debugger(module: module, store: store, imports: [:]) + + #expect(try debugger.originalAddress(function: module.functions[1]) == 42) + } + @Test func binarySearch() throws { #expect([Int]().binarySearch(nextClosestTo: 42) == nil) From 9215fe0588be97af766ffd7ea6f59a2762a34e81 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 4 Nov 2025 13:00:32 +0000 Subject: [PATCH 06/23] Add tests for debugging across function calls --- Sources/WasmKit/Execution/Debugger.swift | 46 +++++++++++-------- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 16 ++++++- Tests/WasmKitTests/DebuggerTests.swift | 15 ++++-- 3 files changed, 54 insertions(+), 23 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 1bcbed1e..a02403a9 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -51,7 +51,7 @@ /// Addresses of functions in the original Wasm binary, used 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: [Int] + private let functionAddresses: [(address: Int, instanceFunctionIndex: Int)] /// Initializes a new debugger state instance. /// - Parameters: @@ -66,8 +66,15 @@ throw Error.entrypointFunctionNotFound } - self.functionAddresses = module.functions.map(\.code.originalAddress) 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.allocate(capacity: limit) @@ -101,19 +108,20 @@ } } - private func findIseq(forWasmAddress address: Int) throws(Error) -> (iseq: Pc, wasm: Int) { + 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) } - // else if let functionIndex = self.functionAddresses.firstIndex(where: { $0 > address }) - 1 { - // let function = self.module.functions[functionIndex] - // try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) - // - // if let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) { - // return (iseq, wasm) - // } - // } - // + throw Error.noInstructionMappingAvailable(address) } @@ -123,14 +131,16 @@ /// 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 + 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 @@ -138,7 +148,7 @@ /// - 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 } @@ -238,10 +248,10 @@ return [] } - var result = Execution.captureBacktrace(sp: breakpoint.iseq.sp, store: self.store).symbols.compactMap { + 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) - } - result.append(breakpoint.wasmPc) + }) return result } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 1db85c78..2408121c 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -285,11 +285,23 @@ throw Error.killRequestReceived case .insertSoftwareBreakpoint: - try self.debugger.enableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) + try self.debugger.enableBreakpoint( + address: Int(self.firstHexArgument( + argumentsString: command.arguments, + separator: ",", + endianness: .big + ) - codeOffset) + ) responseKind = .ok case .removeSoftwareBreakpoint: - try self.debugger.disableBreakpoint(address: self.firstHexArgument(argumentsString: command.arguments, separator: ",", endianness: .big)) + try self.debugger.disableBreakpoint( + address: Int(self.firstHexArgument( + argumentsString: command.arguments, + separator: ",", + endianness: .big + ) - codeOffset) + ) responseKind = .ok case .generalRegisters: diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index d8e93df9..fe012d77 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -53,7 +53,7 @@ #expect(debugger.currentCallStack == [firstExpectedPc]) try debugger.step() - #expect(try debugger.breakpoints.count == 1) + #expect(debugger.breakpoints.count == 1) let secondExpectedPc = try #require(debugger.breakpoints.keys.first) #expect(debugger.currentCallStack == [secondExpectedPc]) @@ -66,14 +66,23 @@ } } + /// Ensures that breakpoints and call stacks work across multiple function calls. @Test func lazyFunctionsCompilation() throws { let store = Store(engine: Engine()) - let bytes = try wat2wasm(trivialModuleWAT) + let bytes = try wat2wasm(multiFunctionWAT) let module = try parseWasm(bytes: bytes) + + #expect(module.functions.count == 2) var debugger = try Debugger(module: module, store: store, imports: [:]) - #expect(try debugger.originalAddress(function: module.functions[1]) == 42) + let breakpointAddress = try debugger.enableBreakpoint( + address: module.functions[1].code.originalAddress + ) + try debugger.run() + + #expect(debugger.currentCallStack.count == 2) + #expect(debugger.currentCallStack.first == breakpointAddress) } @Test From 909178f01503d654e160a3ed7cf2f3c4fd69d6b9 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 4 Nov 2025 13:02:56 +0000 Subject: [PATCH 07/23] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 7 +++--- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 22 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index a02403a9..430c465c 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -249,9 +249,10 @@ } 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) - }) + 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 } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 2408121c..e0d69fe7 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -286,21 +286,23 @@ case .insertSoftwareBreakpoint: try self.debugger.enableBreakpoint( - address: Int(self.firstHexArgument( - argumentsString: command.arguments, - separator: ",", - endianness: .big - ) - codeOffset) + 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) + address: Int( + self.firstHexArgument( + argumentsString: command.arguments, + separator: ",", + endianness: .big + ) - codeOffset) ) responseKind = .ok From 55b10e9a4f4e0407950cc860391febff9ca9e358 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 4 Nov 2025 13:51:35 +0000 Subject: [PATCH 08/23] Remove dependency on WASI from `WasmKit.Debugger` --- Sources/WasmKit/Execution/Debugger.swift | 9 ++------- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 7 ------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 430c465c..33c56f3c 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -1,7 +1,5 @@ #if WasmDebuggingSupport - import struct WASI.WASIExitCode - /// Debugger state owner, driven by a debugger host. This implementation has no knowledge of the exact /// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. package struct Debugger: ~Copyable { @@ -14,7 +12,6 @@ case instantiated case stoppedAtBreakpoint(BreakpointState) case trapped(String) - case wasiModuleExited(exitCode: UInt32) case entrypointReturned([Value]) } @@ -213,8 +210,8 @@ ) self.state = .entrypointReturned(result) - case .trapped, .wasiModuleExited, .entrypointReturned: - fatalError("Restarting a WASI module from the debugger is not implemented yet.") + 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 @@ -223,8 +220,6 @@ } self.state = .stoppedAtBreakpoint(.init(iseq: breakpoint, wasmPc: wasmPc)) - } catch let error as WASIExitCode { - self.state = .wasiModuleExited(exitCode: error.code) } } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index e0d69fe7..8a47ef51 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -121,13 +121,6 @@ result.append(("reason", "trace")) return .keyValuePairs(result) - case .wasiModuleExited(let exitCode): - if exitCode > UInt8.max { - return .string("W\(self.hexDump(exitCode, endianness: .big))") - } else { - return .string("W\(self.hexDump(UInt8(exitCode), endianness: .big))") - } - case .entrypointReturned(let values): guard !values.isEmpty else { return .string("W\(self.hexDump(0 as UInt8, endianness: .big))") From 1820a66af1b2fdb5f774bc074131bba42108c1ee Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 4 Nov 2025 15:06:57 +0000 Subject: [PATCH 09/23] Make `ParsingRules` private, add doc comments --- Sources/GDBRemoteProtocol/GDBHostCommand.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index bf8425f4..017e0f5b 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -110,17 +110,21 @@ package struct GDBHostCommand: Equatable { /// Arguments supplied with a host command. package let arguments: String - struct ParsingRule { + /// 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 - var separator: String? = nil - /// Whether command arguments us a `:` delimiter, which usually otherwise + /// Whether command arguments use a `:` delimiter, which usually otherwise /// separates command kind from arguments. var argumentsContainColonDelimiter = false } - static let parsingRules: [ParsingRule] = [ + private static let parsingRules: [ParsingRule] = [ .init( kind: .readMemoryBinaryData, prefix: "x", From 46a8196c66261ed52a52d3074302ff3771e212c9 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 5 Nov 2025 11:22:52 +0000 Subject: [PATCH 10/23] Add scaffolding for reading Wasm locals --- .../GDBRemoteProtocol/GDBHostCommand.swift | 3 ++ Sources/WasmKit/Execution/Debugger.swift | 16 +++++--- Sources/WasmKit/Execution/Execution.swift | 6 +-- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 39 ++++++++++++++++--- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index 017e0f5b..b2a49745 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -47,6 +47,7 @@ package struct GDBHostCommand: Equatable { case kill case insertSoftwareBreakpoint case removeSoftwareBreakpoint + case wasmLocal case generalRegisters @@ -97,6 +98,8 @@ package struct GDBHostCommand: Equatable { self = .continue case "k": self = .kill + case "qWasmLocal": + self = .wasmLocal default: return nil diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 33c56f3c..be0f5bed 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -4,6 +4,7 @@ /// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. package struct Debugger: ~Copyable { package struct BreakpointState { + let sp: Sp let iseq: Execution.Breakpoint package let wasmPc: Int } @@ -43,7 +44,8 @@ package private(set) var state: State - private var pc = Pc.allocate(capacity: 1) + /// Pc ofthe final instruction that a successful program will execute, initialized with `Instruction.endofExecution` + private let endOfExecution = Pc.allocate(capacity: 1) /// Addresses of functions in the original Wasm binary, used looking up functions when a breakpoint /// is enabled at an arbitrary address if it isn't present in ``InstructionMapping`` yet (i.e. the @@ -78,7 +80,7 @@ self.store = store self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) self.threadingModel = store.engine.configuration.threadingModel - self.pc.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel) + self.endOfExecution.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel) self.state = .instantiated } @@ -206,7 +208,7 @@ type: self.entrypointFunction.type, arguments: [], sp: self.valueStack, - pc: self.pc + pc: self.endOfExecution ) self.state = .entrypointReturned(result) @@ -219,7 +221,7 @@ throw Error.noReverseInstructionMappingAvailable(pc) } - self.state = .stoppedAtBreakpoint(.init(iseq: breakpoint, wasmPc: wasmPc)) + self.state = .stoppedAtBreakpoint(.init(sp: breakpoint.sp, iseq: breakpoint, wasmPc: wasmPc)) } } @@ -237,6 +239,10 @@ try self.run() } + package func getLocal(frameIndex: Int, localIndex: Int) -> Address { + + } + /// Array of addresses in the Wasm binary of executed instructions on the call stack. package var currentCallStack: [Int] { guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { @@ -254,7 +260,7 @@ deinit { self.valueStack.deallocate() - self.pc.deallocate() + self.endOfExecution.deallocate() } } diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index a66dfb92..9fec21a0 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -46,7 +46,7 @@ struct Execution: ~Copyable { struct FrameIterator: IteratorProtocol { struct Element { let pc: Pc - let function: EntityHandle? + let sp: Sp } /// The stack pointer currently traversed. @@ -62,7 +62,7 @@ struct Execution: ~Copyable { return nil } self.sp = sp.previousSP - return Element(pc: pc, function: sp.currentFunction) + return Element(pc: pc, sp: sp) } } @@ -71,7 +71,7 @@ struct Execution: ~Copyable { var symbols: [Backtrace.Symbol] = [] while let frame = frames.next() { - guard let function = frame.function else { + guard let function = frame.sp.currentFunction else { symbols.append(.init(name: nil, address: frame.pc)) continue } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 8a47ef51..233393b8 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -32,7 +32,8 @@ } } - private let codeOffset = UInt64(0x4000_0000_0000_0000) + private let codeOffset = UInt64(0x4000_0000_0000_0000) + private let stackOffset = UInt64(0x8000_0000_0000_0000) package actor WasmKitGDBHandler { enum ResumeThreadsAction: String { @@ -50,6 +51,7 @@ case exitCodeUnknown([Value]) case killRequestReceived case unknownHexEncodedArguments(String) + case unknownWasmLocalArguments(String) } private let wasmBinary: ByteBuffer @@ -229,17 +231,25 @@ let argumentsArray = command.arguments.split(separator: ",") guard argumentsArray.count == 2, - let address = UInt64(hexEncoded: argumentsArray[0]), + let hostAddress = UInt64(hexEncoded: argumentsArray[0]), var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - let binaryOffset = Int(address - codeOffset) + if address > stackOffset { + let stackOffset = address - codeOffset - if binaryOffset + length > wasmBinary.readableBytes { - length = wasmBinary.readableBytes - binaryOffset + fatalError("Stack reads are not implemented in the debugger yet") + } else if address > codeOffset { + let binaryOffset = address - stackOffset + if binaryOffset + length > wasmBinary.readableBytes { + length = wasmBinary.readableBytes - binaryOffset + } + + responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) + } else { + fatalError("Linear memory reads are not implemented in the debugger yet.") } - responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) case .wasmCallStack: let callStack = self.debugger.currentCallStack @@ -299,6 +309,23 @@ ) responseKind = .ok + case .wasmLocal: + let arguments = command.arguments.split(separator: ";") + guard arguments.count == 2, + let frameIndexString = arguments.first, + let frameIndex = UInt32(frameIndexString), + let localIndexString = arguments.last, + let localIndex = UInt32(localIndexString) + else { + throw Error.unknownWasmLocalArguments(command.arguments) + } + + var response = self.allocator.buffer(capacity: 64) + response.writeInteger(frameIndex, endianness: .little) + response.writeInteger(localIndex, endianness: .little) + + responseKind = .hexEncodedBinary(response) + case .generalRegisters: throw Error.hostCommandNotImplemented(command.kind) } From d731564b907539c76334fec85046e01235289fb4 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 5 Nov 2025 11:24:30 +0000 Subject: [PATCH 11/23] Fix formatting --- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 233393b8..889f2356 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -32,7 +32,7 @@ } } - private let codeOffset = UInt64(0x4000_0000_0000_0000) + private let codeOffset = UInt64(0x4000_0000_0000_0000) private let stackOffset = UInt64(0x8000_0000_0000_0000) package actor WasmKitGDBHandler { @@ -250,7 +250,6 @@ fatalError("Linear memory reads are not implemented in the debugger yet.") } - case .wasmCallStack: let callStack = self.debugger.currentCallStack var buffer = self.allocator.buffer(capacity: callStack.count * 8) From bc1eb325dae939a6a6a8564c254b13ca5be2f27c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 11 Nov 2025 12:57:57 +0000 Subject: [PATCH 12/23] Add `CallStack: Sequence`, `Debugger.LocalAddress` types --- Sources/WasmKit/Execution/Debugger.swift | 40 +++++++++++++--- Sources/WasmKit/Execution/Execution.swift | 46 +++++++++++-------- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 20 ++++++-- 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 09559f69..764bb9de 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -21,6 +21,9 @@ case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer) case noInstructionMappingAvailable(Int) case noReverseInstructionMappingAvailable(UnsafeMutablePointer) + case stackFuncIndexOOB(LocalAddress) + case stackLocalIndexOOB(LocalAddress) + case notStoppedAtBreakpoint } private let valueStack: Sp @@ -52,11 +55,6 @@ /// was not compiled yet in lazy compilation mode). private let functionAddresses: [(address: Int, instanceFunctionIndex: Int)] - /// 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. @@ -244,8 +242,38 @@ try self.run() } - package func getLocal(frameIndex: Int, localIndex: Int) -> Address { + package struct LocalAddress { + let frameIndex: UInt32 + let localIndex: UInt32 + + package init(frameIndex: Int, localIndex: Int) { + self.frameIndex = frameIndex + self.localIndex = localIndex + } + } + + + /// Iterates through Wasm call stack to return a local at a given address when + /// debugged module is stopped at a breakpoint. + /// - Parameter address: address of the local to return. + /// - Returns: Raw untyped Wasm value at a given address + package func getLocal(address: LocalAddress) throws(Error) -> UInt64 { + guard case let .stoppedAtBreakpoint(breakpoint) = self.state else { + throw Error.notStoppedAtBreakpoint + } + + var i = 0 + for frame in Execution.CallStack(sp: breakpoint.sp) { + guard address.frameIndex == i else { + i += 1 + continue + } + + // TODO: can we guarantee we have no local index OOB here? + return frame.sp[address.localIndex].storage + } + throw Error.stackFuncIndexOOB(address) } /// Array of addresses in the Wasm binary of executed instructions on the call stack. diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index 9fec21a0..ad51183e 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -42,35 +42,43 @@ struct Execution: ~Copyable { sp.currentInstance.unsafelyUnwrapped } - /// An iterator for the call frames in the VM stack. - struct FrameIterator: IteratorProtocol { - struct Element { - let pc: Pc - let sp: Sp - } + struct CallStack: Sequence { + /// An iterator for the call frames in the VM stack. + struct FrameIterator: IteratorProtocol { + struct Element { + let pc: Pc + let sp: Sp + } - /// The stack pointer currently traversed. - private var sp: Sp? + /// The stack pointer currently traversed. + private var sp: Sp? - init(sp: Sp) { - self.sp = sp - } + init(sp: Sp) { + self.sp = sp + } - mutating func next() -> Element? { - guard let sp = self.sp, let pc = sp.returnPC else { - // Reached the root frame, whose stack pointer is nil. - return nil + mutating func next() -> Element? { + guard let sp = self.sp, let pc = sp.returnPC else { + // Reached the root frame, whose stack pointer is nil. + return nil + } + self.sp = sp.previousSP + return Element(pc: pc, sp: sp) } - self.sp = sp.previousSP - return Element(pc: pc, sp: sp) + } + + let sp: Sp + + func makeIterator() -> FrameIterator { + FrameIterator(sp: self.sp) } } static func captureBacktrace(sp: Sp, store: Store) -> Backtrace { - var frames = FrameIterator(sp: sp) + let callStack = CallStack(sp: sp) var symbols: [Backtrace.Symbol] = [] - while let frame = frames.next() { + for frame in callStack { guard let function = frame.sp.currentFunction else { symbols.append(.init(name: nil, address: frame.pc)) continue diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 889f2356..65f0dec8 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -32,8 +32,17 @@ } } - private let codeOffset = UInt64(0x4000_0000_0000_0000) + + extension Debugger.LocalAddress { + package init(raw: UInt64) { + let rawAdjusted = raw - localOffset + self.init(frameIndex: UInt32(truncatingIfNeeded: rawAdjusted >> 32), localIndex: UInt32(truncatingIfNeeded: rawAdjusted)) + } + } + + private let codeOffset = UInt64(0x4000_0000_0000_0000) private let stackOffset = UInt64(0x8000_0000_0000_0000) + private let localOffset = UInt64(0xC000_0000_0000_0000) package actor WasmKitGDBHandler { enum ResumeThreadsAction: String { @@ -231,13 +240,14 @@ let argumentsArray = command.arguments.split(separator: ",") guard argumentsArray.count == 2, - let hostAddress = UInt64(hexEncoded: argumentsArray[0]), + let address = UInt64(hexEncoded: argumentsArray[0]), var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - if address > stackOffset { - let stackOffset = address - codeOffset - + if address > localOffset { + let localRaw = address - localOffset + let localAddress = Debugger.LocalAddress(frameIndex: ) + } else if address > stackOffset { fatalError("Stack reads are not implemented in the debugger yet") } else if address > codeOffset { let binaryOffset = address - stackOffset From 3a2eb99d7daac5146de752fa82ecab2ab1e2f03f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 11 Nov 2025 12:58:30 +0000 Subject: [PATCH 13/23] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 3 +-- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 764bb9de..c31fa675 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -252,13 +252,12 @@ } } - /// Iterates through Wasm call stack to return a local at a given address when /// debugged module is stopped at a breakpoint. /// - Parameter address: address of the local to return. /// - Returns: Raw untyped Wasm value at a given address package func getLocal(address: LocalAddress) throws(Error) -> UInt64 { - guard case let .stoppedAtBreakpoint(breakpoint) = self.state else { + guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { throw Error.notStoppedAtBreakpoint } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 65f0dec8..36aaa2f8 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -32,7 +32,6 @@ } } - extension Debugger.LocalAddress { package init(raw: UInt64) { let rawAdjusted = raw - localOffset @@ -40,7 +39,7 @@ } } - private let codeOffset = UInt64(0x4000_0000_0000_0000) + private let codeOffset = UInt64(0x4000_0000_0000_0000) private let stackOffset = UInt64(0x8000_0000_0000_0000) private let localOffset = UInt64(0xC000_0000_0000_0000) @@ -246,7 +245,7 @@ if address > localOffset { let localRaw = address - localOffset - let localAddress = Debugger.LocalAddress(frameIndex: ) + let localAddress = Debugger.LocalAddress(frameIndex:) } else if address > stackOffset { fatalError("Stack reads are not implemented in the debugger yet") } else if address > codeOffset { From 85b2248bb1b827544057f2cc4e286529b693ad52 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 11 Nov 2025 14:14:09 +0000 Subject: [PATCH 14/23] Fix build errors --- Sources/WasmKit/Execution/Debugger.swift | 2 +- Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index c31fa675..095df97f 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -246,7 +246,7 @@ let frameIndex: UInt32 let localIndex: UInt32 - package init(frameIndex: Int, localIndex: Int) { + package init(frameIndex: UInt32, localIndex: UInt32) { self.frameIndex = frameIndex self.localIndex = localIndex } diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 36aaa2f8..c933c115 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -244,12 +244,12 @@ else { throw Error.unknownReadMemoryArguments } if address > localOffset { - let localRaw = address - localOffset - let localAddress = Debugger.LocalAddress(frameIndex:) + let localAddress = Debugger.LocalAddress(raw: address) + fatalError() } else if address > stackOffset { fatalError("Stack reads are not implemented in the debugger yet") } else if address > codeOffset { - let binaryOffset = address - stackOffset + let binaryOffset = Int(address - stackOffset) if binaryOffset + length > wasmBinary.readableBytes { length = wasmBinary.readableBytes - binaryOffset } @@ -332,7 +332,7 @@ response.writeInteger(frameIndex, endianness: .little) response.writeInteger(localIndex, endianness: .little) - responseKind = .hexEncodedBinary(response) + responseKind = .hexEncodedBinary(response.readableBytesView) case .generalRegisters: throw Error.hostCommandNotImplemented(command.kind) From 185328e370aeaa33a8eb78f71e5b217eac82302f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 11 Nov 2025 18:37:28 +0000 Subject: [PATCH 15/23] Expose interpreter stack via GDB RP --- .../GDBRemoteProtocol/GDBHostCommand.swift | 3 + Sources/WasmKit/Execution/Debugger.swift | 36 ++++++++++-- Sources/WasmKit/Execution/Execution.swift | 11 +++- .../WasmKitGDBHandler/MemoryAlignment.swift | 23 ++++++++ .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 58 +++++++++++-------- Tests/WasmKitTests/DebuggerTests.swift | 10 ++++ 6 files changed, 109 insertions(+), 32 deletions(-) create mode 100644 Sources/WasmKitGDBHandler/MemoryAlignment.swift diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index b2a49745..4b28f6ee 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -48,6 +48,7 @@ package struct GDBHostCommand: Equatable { case insertSoftwareBreakpoint case removeSoftwareBreakpoint case wasmLocal + case memoryRegionInfo case generalRegisters @@ -100,6 +101,8 @@ package struct GDBHostCommand: Equatable { self = .kill case "qWasmLocal": self = .wasmLocal + case "qMemoryRegionInfo": + self = .memoryRegionInfo default: return nil diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 095df97f..5a7eb0cd 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -81,7 +81,11 @@ self.entrypointFunction = entrypointFunction self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) self.store = store - self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit)) + self.execution = Execution( + store: StoreRef(store), + stackStart: .init(start: valueStack, count: limit), + stackEnd: valueStack.advanced(by: limit) + ) self.threadingModel = store.engine.configuration.threadingModel self.endOfExecution.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel) self.state = .instantiated @@ -242,7 +246,7 @@ try self.run() } - package struct LocalAddress { + package struct LocalAddress: Equatable { let frameIndex: UInt32 let localIndex: UInt32 @@ -250,13 +254,20 @@ self.frameIndex = frameIndex self.localIndex = localIndex } + + package init?(raw: UInt64, offset: UInt64) { + guard raw >= offset else { return nil } + + let rawAdjusted = raw - offset + self.init(frameIndex: UInt32(truncatingIfNeeded: rawAdjusted >> 32), localIndex: UInt32(truncatingIfNeeded: rawAdjusted)) + } } /// Iterates through Wasm call stack to return a local at a given address when /// debugged module is stopped at a breakpoint. /// - Parameter address: address of the local to return. /// - Returns: Raw untyped Wasm value at a given address - package func getLocal(address: LocalAddress) throws(Error) -> UInt64 { + package func getLocalPointer(address: LocalAddress) throws(Error) -> UnsafeRawPointer { guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { throw Error.notStoppedAtBreakpoint } @@ -268,8 +279,19 @@ continue } - // TODO: can we guarantee we have no local index OOB here? - return frame.sp[address.localIndex].storage + guard let currentFunction = frame.sp.currentFunction else { + throw Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) + } + + let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) + let localsCount = type.parameters.count + currentFunction.numberOfNonParameterLocals + + guard address.localIndex < localsCount else { + throw Error.stackLocalIndexOOB(address) + } + + let result = UnsafeRawPointer(frame.sp.advanced(by: Int(address.localIndex))) + return result } throw Error.stackFuncIndexOOB(address) @@ -290,6 +312,10 @@ return result } + package var stackMemory: UnsafeRawBufferPointer { + UnsafeRawBufferPointer(self.execution.stackStart) + } + deinit { self.valueStack.deallocate() self.endOfExecution.deallocate() diff --git a/Sources/WasmKit/Execution/Execution.swift b/Sources/WasmKit/Execution/Execution.swift index ad51183e..8801ff32 100644 --- a/Sources/WasmKit/Execution/Execution.swift +++ b/Sources/WasmKit/Execution/Execution.swift @@ -7,16 +7,21 @@ import _CWasmKit struct Execution: ~Copyable { /// The reference to the ``Store`` associated with the execution. let store: StoreRef + + /// The start of the VM stack space, used for debugging purposes. + let stackStart: UnsafeBufferPointer + /// The end of the VM stack space. - private var stackEnd: UnsafeMutablePointer + private let stackEnd: UnsafeMutablePointer /// The error trap thrown during execution. /// This property must not be assigned to be non-nil more than once. /// - Note: If the trap is set, it must be released manually. private var trap: (error: UnsafeRawPointer, sp: Sp)? = nil #if WasmDebuggingSupport - package init(store: StoreRef, stackEnd: UnsafeMutablePointer) { + package init(store: StoreRef, stackStart: UnsafeBufferPointer, stackEnd: UnsafeMutablePointer) { self.store = store + self.stackStart = stackStart self.stackEnd = stackEnd } #endif @@ -32,7 +37,7 @@ struct Execution: ~Copyable { defer { valueStack.deallocate() } - var context = Execution(store: store, stackEnd: valueStack.advanced(by: limit)) + var context = Execution(store: store, stackStart: .init(start: valueStack, count: limit), stackEnd: valueStack.advanced(by: limit)) return try body(&context, valueStack) } diff --git a/Sources/WasmKitGDBHandler/MemoryAlignment.swift b/Sources/WasmKitGDBHandler/MemoryAlignment.swift new file mode 100644 index 00000000..2346531d --- /dev/null +++ b/Sources/WasmKitGDBHandler/MemoryAlignment.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftNIO open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftNIO project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +extension Int { + mutating func roundUpToAlignment(for: Type.Type) { + // Alignment is always positive, we can use unchecked subtraction here. + let alignmentGuide = MemoryLayout.alignment &- 1 + + // But we can't use unchecked addition. + self = (self + alignmentGuide) & (~alignmentGuide) + } +} diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index c933c115..af071950 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -32,16 +32,7 @@ } } - extension Debugger.LocalAddress { - package init(raw: UInt64) { - let rawAdjusted = raw - localOffset - self.init(frameIndex: UInt32(truncatingIfNeeded: rawAdjusted >> 32), localIndex: UInt32(truncatingIfNeeded: rawAdjusted)) - } - } - private let codeOffset = UInt64(0x4000_0000_0000_0000) - private let stackOffset = UInt64(0x8000_0000_0000_0000) - private let localOffset = UInt64(0xC000_0000_0000_0000) package actor WasmKitGDBHandler { enum ResumeThreadsAction: String { @@ -67,6 +58,7 @@ private let logger: Logger private let allocator: ByteBufferAllocator private var debugger: Debugger + private let stackOffsetInProtocolSpace: UInt64 package init( moduleFilePath: FilePath, @@ -94,6 +86,11 @@ guard case .stoppedAtBreakpoint = self.debugger.state else { throw Error.stoppingAtEntrypointFailed } + + var stackOffset = Int(codeOffset) + wasmBinary.readableBytes + // Untyped raw Wasm values in VM's stack are stored as `UInt64`. + stackOffset.roundUpToAlignment(for: UInt64.self) + self.stackOffsetInProtocolSpace = UInt64(stackOffset) } private func hexDump(_ value: I, endianness: Endianness) -> String { @@ -239,22 +236,27 @@ let argumentsArray = command.arguments.split(separator: ",") guard argumentsArray.count == 2, - let address = UInt64(hexEncoded: argumentsArray[0]), + let addressInProtocolSpace = UInt64(hexEncoded: argumentsArray[0]), var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - if address > localOffset { - let localAddress = Debugger.LocalAddress(raw: address) - fatalError() - } else if address > stackOffset { - fatalError("Stack reads are not implemented in the debugger yet") - } else if address > codeOffset { - let binaryOffset = Int(address - stackOffset) - if binaryOffset + length > wasmBinary.readableBytes { - length = wasmBinary.readableBytes - binaryOffset + if addressInProtocolSpace >= self.stackOffsetInProtocolSpace { + let stackAddress = Int(addressInProtocolSpace - self.stackOffsetInProtocolSpace) + if stackAddress + length > self.debugger.stackMemory.count { + length = self.debugger.stackMemory.count - stackAddress } - responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) + responseKind = .hexEncodedBinary( + ByteBuffer( + bytes: self.debugger.stackMemory[stackAddress..<(stackAddress + length)] + ).readableBytesView) + } else if addressInProtocolSpace >= codeOffset { + let codeAddress = Int(addressInProtocolSpace - codeOffset) + if codeAddress + length > wasmBinary.readableBytes { + length = wasmBinary.readableBytes - codeAddress + } + + responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[codeAddress..<(codeAddress + length)]) } else { fatalError("Linear memory reads are not implemented in the debugger yet.") } @@ -328,11 +330,19 @@ throw Error.unknownWasmLocalArguments(command.arguments) } - var response = self.allocator.buffer(capacity: 64) - response.writeInteger(frameIndex, endianness: .little) - response.writeInteger(localIndex, endianness: .little) + let localAddress = Debugger.LocalAddress(frameIndex: frameIndex, localIndex: localIndex) + let localPointer = try self.debugger.getLocalPointer(address: localAddress) + let responseAddress = self.stackOffsetInProtocolSpace + UInt64(localPointer - self.debugger.stackMemory.baseAddress!) + + responseKind = .hexEncodedBinary( + ByteBuffer( + integer: responseAddress, + endianness: .little + ).readableBytesView + ) - responseKind = .hexEncodedBinary(response.readableBytesView) + case .memoryRegionInfo: + responseKind = .empty case .generalRegisters: throw Error.hostCommandNotImplemented(command.kind) diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index fe012d77..7490bf41 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -99,6 +99,16 @@ #expect([106, 110, 111].binarySearch(nextClosestTo: 107) == 110) #expect([106, 110, 111, 113, 119, 120, 122, 128, 136].binarySearch(nextClosestTo: 121) == 122) } + + @Test + func localAddress() { + let localOffset = UInt64(0xC000_0000_0000_0000) + + #expect(Debugger.LocalAddress(raw: 0, offset: localOffset) == nil) + #expect(Debugger.LocalAddress(raw: 0xABCDEF, offset: localOffset) == nil) + #expect(Debugger.LocalAddress(raw: 0xC000_0000_0000_ABCD, offset: localOffset) == Debugger.LocalAddress(frameIndex: 0, localIndex: 0xABCD)) + #expect(Debugger.LocalAddress(raw: 0xC000_0000_0000_ABCD, offset: localOffset) == Debugger.LocalAddress(frameIndex: 0, localIndex: 0xABCD)) + } } #endif From 132dd94346bfc3817a5edb2fc488a169df244804 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 13 Nov 2025 12:59:24 +0000 Subject: [PATCH 16/23] Add scaffolding for debugging packed stack frames --- Sources/WasmKit/Execution/Debugger.swift | 63 +++++++++++++++++++++--- Tests/WasmKitTests/DebuggerTests.swift | 3 ++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 5a7eb0cd..dc07c9c5 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -4,7 +4,6 @@ /// debugger protocol, which allows any protocol implementation or direct API users to be layered on top if needed. package struct Debugger: ~Copyable { package struct BreakpointState { - let sp: Sp let iseq: Execution.Breakpoint package let wasmPc: Int } @@ -21,8 +20,8 @@ case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer) case noInstructionMappingAvailable(Int) case noReverseInstructionMappingAvailable(UnsafeMutablePointer) - case stackFuncIndexOOB(LocalAddress) - case stackLocalIndexOOB(LocalAddress) + case stackFrameIndexOOB(UInt32) + case stackLocalIndexOOB(UInt32) case notStoppedAtBreakpoint } @@ -55,6 +54,8 @@ /// was not compiled yet in lazy compilation mode). private let functionAddresses: [(address: Int, instanceFunctionIndex: Int)] + private var stackFrameBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8) + /// Initializes a new debugger state instance. /// - Parameters: /// - module: Wasm module to instantiate. @@ -228,7 +229,7 @@ throw Error.noReverseInstructionMappingAvailable(pc) } - self.state = .stoppedAtBreakpoint(.init(sp: breakpoint.sp, iseq: breakpoint, wasmPc: wasmPc)) + self.state = .stoppedAtBreakpoint(.init(iseq: breakpoint, wasmPc: wasmPc)) } } @@ -263,6 +264,40 @@ } } + package func packedStackFrame(frameIndex: UInt32, reader: (RawSpan) -> ()) throws(Error) { + guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { + throw Error.notStoppedAtBreakpoint + } + + var i = 0 + for frame in Execution.CallStack(sp: breakpoint.iseq.sp) { + guard frameIndex == i else { + i += 1 + continue + } + + guard let currentFunction = frame.sp.currentFunction else { + throw Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) + } + + let code = switch currentFunction.code { + case .compiled: + // FIXME: we should be able examine frames of uncompiled functions + fatalError() + case .debuggable(let code, _), .uncompiled(let code): + code + } + + // Wasm function arguments are also addressed as locals. + let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) + let localsCount = type.parameters.count + currentFunction.numberOfNonParameterLocals + let localTypes = code.locals + + reader(self.stackFrameBuffer.bytes) + } + throw Error.stackFrameIndexOOB(frameIndex) + } + /// Iterates through Wasm call stack to return a local at a given address when /// debugged module is stopped at a breakpoint. /// - Parameter address: address of the local to return. @@ -273,7 +308,7 @@ } var i = 0 - for frame in Execution.CallStack(sp: breakpoint.sp) { + for frame in Execution.CallStack(sp: breakpoint.iseq.sp) { guard address.frameIndex == i else { i += 1 continue @@ -283,18 +318,29 @@ throw Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) } + // Wasm function arguments are also addressed as locals. let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) let localsCount = type.parameters.count + currentFunction.numberOfNonParameterLocals guard address.localIndex < localsCount else { - throw Error.stackLocalIndexOOB(address) + throw Error.stackLocalIndexOOB(address.localIndex) } - let result = UnsafeRawPointer(frame.sp.advanced(by: Int(address.localIndex))) + // If locals that aren't function arguments are addressed, those can be found by index directly. + let result = if address.localIndex > type.parameters.count { + UnsafeRawPointer(frame.sp + Int(address.localIndex)) + } else { + // Otherwise we need function arguments, and those are stored in the frame header. + // See ``FrameHeaderLayout`` comments, we need to skip 3 bytes for: + // 1. Saved instance + // 2. Saved Pc. + // 3. Saved Sp + UnsafeRawPointer(frame.sp - FrameHeaderLayout.numberOfSavingSlots - type.parameters.count + Int(address.localIndex)) + } return result } - throw Error.stackFuncIndexOOB(address) + throw Error.stackFrameIndexOOB(address.frameIndex) } /// Array of addresses in the Wasm binary of executed instructions on the call stack. @@ -319,6 +365,7 @@ deinit { self.valueStack.deallocate() self.endOfExecution.deallocate() + self.stackFrameBuffer.deallocate() } } diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index 7490bf41..b8930360 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -83,6 +83,9 @@ #expect(debugger.currentCallStack.count == 2) #expect(debugger.currentCallStack.first == breakpointAddress) + + let localPointer = try debugger.getLocalPointer(address: .init(frameIndex: 0, localIndex: 0)) + #expect(localPointer.load(as: UInt64.self) == 42) } @Test From 7be106a040c892a4647f5b03df07037d83348eb5 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 13 Nov 2025 15:15:25 +0000 Subject: [PATCH 17/23] Get `maxStackHeight` in `packedStackFrame` --- Sources/WasmKit/Execution/Debugger.swift | 35 ++++++++++++------------ 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index dc07c9c5..264d6cc4 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -264,7 +264,7 @@ } } - package func packedStackFrame(frameIndex: UInt32, reader: (RawSpan) -> ()) throws(Error) { + package func packedStackFrame(frameIndex: UInt32, reader: (RawSpan) -> ()) throws { guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { throw Error.notStoppedAtBreakpoint } @@ -280,18 +280,18 @@ throw Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) } - let code = switch currentFunction.code { - case .compiled: - // FIXME: we should be able examine frames of uncompiled functions + try currentFunction.ensureCompiled(store: StoreRef(self.store)) + + guard case .debuggable(let wasm, let iseq) = currentFunction.code else { fatalError() - case .debuggable(let code, _), .uncompiled(let code): - code } + // Wasm function arguments are also addressed as locals. let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) let localsCount = type.parameters.count + currentFunction.numberOfNonParameterLocals - let localTypes = code.locals + let localTypes = wasm.locals + iseq.maxStackHeight reader(self.stackFrameBuffer.bytes) } @@ -327,16 +327,17 @@ } // If locals that aren't function arguments are addressed, those can be found by index directly. - let result = if address.localIndex > type.parameters.count { - UnsafeRawPointer(frame.sp + Int(address.localIndex)) - } else { - // Otherwise we need function arguments, and those are stored in the frame header. - // See ``FrameHeaderLayout`` comments, we need to skip 3 bytes for: - // 1. Saved instance - // 2. Saved Pc. - // 3. Saved Sp - UnsafeRawPointer(frame.sp - FrameHeaderLayout.numberOfSavingSlots - type.parameters.count + Int(address.localIndex)) - } + let result = + if address.localIndex > type.parameters.count { + UnsafeRawPointer(frame.sp + Int(address.localIndex)) + } else { + // Otherwise we need function arguments, and those are stored in the frame header. + // See ``FrameHeaderLayout`` comments, we need to skip 3 bytes for: + // 1. Saved instance + // 2. Saved Pc. + // 3. Saved Sp + UnsafeRawPointer(frame.sp - FrameHeaderLayout.numberOfSavingSlots - type.parameters.count + Int(address.localIndex)) + } return result } From 6d2207c56f49088821e04ae9f4d018cf925e5bed Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 13 Nov 2025 17:43:05 +0000 Subject: [PATCH 18/23] Move packed stack frame code to separate `DebuggerStackFrame` type --- Sources/WasmKit/Execution/Debugger.swift | 28 +------- .../Execution/DebuggerStackFrame.swift | 71 +++++++++++++++++++ Sources/WasmTypes/WasmTypes.swift | 11 +++ 3 files changed, 83 insertions(+), 27 deletions(-) create mode 100644 Sources/WasmKit/Execution/DebuggerStackFrame.swift diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 264d6cc4..bfcd2f33 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -54,7 +54,7 @@ /// was not compiled yet in lazy compilation mode). private let functionAddresses: [(address: Int, instanceFunctionIndex: Int)] - private var stackFrameBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8) + private var stackFrame = DebuggerStackFrame() /// Initializes a new debugger state instance. /// - Parameters: @@ -269,32 +269,7 @@ throw Error.notStoppedAtBreakpoint } - var i = 0 - for frame in Execution.CallStack(sp: breakpoint.iseq.sp) { - guard frameIndex == i else { - i += 1 - continue - } - - guard let currentFunction = frame.sp.currentFunction else { - throw Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) - } - - try currentFunction.ensureCompiled(store: StoreRef(self.store)) - guard case .debuggable(let wasm, let iseq) = currentFunction.code else { - fatalError() - } - - - // Wasm function arguments are also addressed as locals. - let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) - let localsCount = type.parameters.count + currentFunction.numberOfNonParameterLocals - let localTypes = wasm.locals - iseq.maxStackHeight - - reader(self.stackFrameBuffer.bytes) - } throw Error.stackFrameIndexOOB(frameIndex) } @@ -366,7 +341,6 @@ deinit { self.valueStack.deallocate() self.endOfExecution.deallocate() - self.stackFrameBuffer.deallocate() } } diff --git a/Sources/WasmKit/Execution/DebuggerStackFrame.swift b/Sources/WasmKit/Execution/DebuggerStackFrame.swift new file mode 100644 index 00000000..28906b6c --- /dev/null +++ b/Sources/WasmKit/Execution/DebuggerStackFrame.swift @@ -0,0 +1,71 @@ +#if WasmDebuggingSupport + +struct DebuggerStackFrame: ~Copyable { + var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8) + + init() { + buffer.initializeMemory(as: Int.self, repeating: 0) + } + + mutating func withFrames(sp: Sp, frameIndex: Int, store: Store, writer: (borrowing OutputRawSpan) -> ()) throws { + self.buffer.initializeMemory(as: Int.self, repeating: 0) + + var i = 0 + for frame in Execution.CallStack(sp: sp) { + guard frameIndex == i else { + i += 1 + continue + } + + guard let currentFunction = frame.sp.currentFunction else { + throw Debugger.Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) + } + + try currentFunction.ensureCompiled(store: StoreRef(store)) + + guard case .debuggable(let wasm, let iseq) = currentFunction.code else { + fatalError() + } + + // Wasm function arguments are also addressed as locals. + let functionType = store.engine.funcTypeInterner.resolve(currentFunction.type) + + let pessimisticByteCount = functionType.parameters.count * 8 + wasm.locals.count * 8 + iseq.maxStackHeight * 8 + + if pessimisticByteCount > self.buffer.count { + let newBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: pessimisticByteCount, alignment: 8) + newBuffer.copyBytes(from: self.buffer) + self.buffer.deallocate() + self.buffer = newBuffer + } + + var span = OutputRawSpan(buffer: self.buffer, initializedCount: 0) + + for (i, type) in functionType.parameters.enumerated() { + switch type { + case .i32, .f32: + span.append(frame.sp[i32: i], as: UInt32.self) + case .i64, .f64: + span.append(frame.sp[i64: i], as: UInt64.self) + case .v128: + fatalError("SIMD is not yet supported in the Wasm debugger") + case .ref: + fatalError("References are not yet supported in the wasm debugger") + } + } + + _ = span.finalize(for: self.buffer) + + let localsCount = functionType.parameters.count + currentFunction.numberOfNonParameterLocals + let localTypes = wasm.locals + iseq.maxStackHeight + + } + } + + deinit { + buffer.deallocate() + } +} + +#endif diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index daa1ab61..1c07fcb0 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -35,6 +35,17 @@ public enum ValueType: Equatable, Hashable, Sendable { case v128 /// Reference value type. case ref(ReferenceType) + +#if WasmDebuggingSupport + package var byteSize: Int { + switch self { + case .i32, .f32: 4 + case .i64, .f64: 8 + case .v128: 16 + case .ref: fatalError("bitwise copy of references not implemented yet") + } + } +#endif } /// Runtime representation of a WebAssembly function reference. From 1bb32b56ae04dda2033abbcb656a635da24618e6 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Thu, 13 Nov 2025 17:43:19 +0000 Subject: [PATCH 19/23] Fix formatting --- Sources/WasmKit/Execution/Debugger.swift | 3 +- .../Execution/DebuggerStackFrame.swift | 100 +++++++++--------- 2 files changed, 51 insertions(+), 52 deletions(-) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index bfcd2f33..942ec0cf 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -264,12 +264,11 @@ } } - package func packedStackFrame(frameIndex: UInt32, reader: (RawSpan) -> ()) throws { + package func packedStackFrame(frameIndex: UInt32, reader: (RawSpan) -> Void) throws { guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { throw Error.notStoppedAtBreakpoint } - throw Error.stackFrameIndexOOB(frameIndex) } diff --git a/Sources/WasmKit/Execution/DebuggerStackFrame.swift b/Sources/WasmKit/Execution/DebuggerStackFrame.swift index 28906b6c..ebf3ae9a 100644 --- a/Sources/WasmKit/Execution/DebuggerStackFrame.swift +++ b/Sources/WasmKit/Execution/DebuggerStackFrame.swift @@ -1,71 +1,71 @@ #if WasmDebuggingSupport -struct DebuggerStackFrame: ~Copyable { - var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8) + struct DebuggerStackFrame: ~Copyable { + var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8) - init() { - buffer.initializeMemory(as: Int.self, repeating: 0) - } + init() { + buffer.initializeMemory(as: Int.self, repeating: 0) + } - mutating func withFrames(sp: Sp, frameIndex: Int, store: Store, writer: (borrowing OutputRawSpan) -> ()) throws { - self.buffer.initializeMemory(as: Int.self, repeating: 0) + mutating func withFrames(sp: Sp, frameIndex: Int, store: Store, writer: (borrowing OutputRawSpan) -> Void) throws { + self.buffer.initializeMemory(as: Int.self, repeating: 0) - var i = 0 - for frame in Execution.CallStack(sp: sp) { - guard frameIndex == i else { - i += 1 - continue - } + var i = 0 + for frame in Execution.CallStack(sp: sp) { + guard frameIndex == i else { + i += 1 + continue + } - guard let currentFunction = frame.sp.currentFunction else { - throw Debugger.Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) - } + guard let currentFunction = frame.sp.currentFunction else { + throw Debugger.Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) + } - try currentFunction.ensureCompiled(store: StoreRef(store)) + try currentFunction.ensureCompiled(store: StoreRef(store)) - guard case .debuggable(let wasm, let iseq) = currentFunction.code else { - fatalError() - } + guard case .debuggable(let wasm, let iseq) = currentFunction.code else { + fatalError() + } - // Wasm function arguments are also addressed as locals. - let functionType = store.engine.funcTypeInterner.resolve(currentFunction.type) + // Wasm function arguments are also addressed as locals. + let functionType = store.engine.funcTypeInterner.resolve(currentFunction.type) - let pessimisticByteCount = functionType.parameters.count * 8 + wasm.locals.count * 8 + iseq.maxStackHeight * 8 + let pessimisticByteCount = functionType.parameters.count * 8 + wasm.locals.count * 8 + iseq.maxStackHeight * 8 - if pessimisticByteCount > self.buffer.count { - let newBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: pessimisticByteCount, alignment: 8) - newBuffer.copyBytes(from: self.buffer) - self.buffer.deallocate() - self.buffer = newBuffer - } + if pessimisticByteCount > self.buffer.count { + let newBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: pessimisticByteCount, alignment: 8) + newBuffer.copyBytes(from: self.buffer) + self.buffer.deallocate() + self.buffer = newBuffer + } - var span = OutputRawSpan(buffer: self.buffer, initializedCount: 0) - - for (i, type) in functionType.parameters.enumerated() { - switch type { - case .i32, .f32: - span.append(frame.sp[i32: i], as: UInt32.self) - case .i64, .f64: - span.append(frame.sp[i64: i], as: UInt64.self) - case .v128: - fatalError("SIMD is not yet supported in the Wasm debugger") - case .ref: - fatalError("References are not yet supported in the wasm debugger") + var span = OutputRawSpan(buffer: self.buffer, initializedCount: 0) + + for (i, type) in functionType.parameters.enumerated() { + switch type { + case .i32, .f32: + span.append(frame.sp[i32: i], as: UInt32.self) + case .i64, .f64: + span.append(frame.sp[i64: i], as: UInt64.self) + case .v128: + fatalError("SIMD is not yet supported in the Wasm debugger") + case .ref: + fatalError("References are not yet supported in the wasm debugger") + } } - } - _ = span.finalize(for: self.buffer) + _ = span.finalize(for: self.buffer) - let localsCount = functionType.parameters.count + currentFunction.numberOfNonParameterLocals - let localTypes = wasm.locals - iseq.maxStackHeight + let localsCount = functionType.parameters.count + currentFunction.numberOfNonParameterLocals + let localTypes = wasm.locals + iseq.maxStackHeight + } } - } - deinit { - buffer.deallocate() + deinit { + buffer.deallocate() + } } -} #endif From 6f2cb82b976257f4123a34f1c38024ff6277476e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 14 Nov 2025 17:33:44 +0000 Subject: [PATCH 20/23] Write out the stack frame, propagate layout back to the debugger --- Examples/wasm/ctest.wasm | Bin 0 -> 27904 bytes Sources/WasmKit/Execution/Debugger.swift | 67 +----------------- .../Execution/DebuggerStackFrame.swift | 64 ++++++++++++----- .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 33 +++++++-- Sources/WasmTypes/WasmTypes.swift | 18 ++--- 5 files changed, 83 insertions(+), 99 deletions(-) create mode 100755 Examples/wasm/ctest.wasm diff --git a/Examples/wasm/ctest.wasm b/Examples/wasm/ctest.wasm new file mode 100755 index 0000000000000000000000000000000000000000..e85a417e00441d82f3834832b27544a00275e73c GIT binary patch literal 27904 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 H>GA&n<6J^w literal 0 HcmV?d00001 diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 942ec0cf..2a507b13 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -247,75 +247,12 @@ try self.run() } - package struct LocalAddress: Equatable { - let frameIndex: UInt32 - let localIndex: UInt32 - - package init(frameIndex: UInt32, localIndex: UInt32) { - self.frameIndex = frameIndex - self.localIndex = localIndex - } - - package init?(raw: UInt64, offset: UInt64) { - guard raw >= offset else { return nil } - - let rawAdjusted = raw - offset - self.init(frameIndex: UInt32(truncatingIfNeeded: rawAdjusted >> 32), localIndex: UInt32(truncatingIfNeeded: rawAdjusted)) - } - } - - package func packedStackFrame(frameIndex: UInt32, reader: (RawSpan) -> Void) throws { + package mutating func packedStackFrame(frameIndex: Int, reader: (RawSpan, DebuggerStackFrame.Layout) -> T) throws -> T { guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { throw Error.notStoppedAtBreakpoint } - throw Error.stackFrameIndexOOB(frameIndex) - } - - /// Iterates through Wasm call stack to return a local at a given address when - /// debugged module is stopped at a breakpoint. - /// - Parameter address: address of the local to return. - /// - Returns: Raw untyped Wasm value at a given address - package func getLocalPointer(address: LocalAddress) throws(Error) -> UnsafeRawPointer { - guard case .stoppedAtBreakpoint(let breakpoint) = self.state else { - throw Error.notStoppedAtBreakpoint - } - - var i = 0 - for frame in Execution.CallStack(sp: breakpoint.iseq.sp) { - guard address.frameIndex == i else { - i += 1 - continue - } - - guard let currentFunction = frame.sp.currentFunction else { - throw Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp) - } - - // Wasm function arguments are also addressed as locals. - let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) - let localsCount = type.parameters.count + currentFunction.numberOfNonParameterLocals - - guard address.localIndex < localsCount else { - throw Error.stackLocalIndexOOB(address.localIndex) - } - - // If locals that aren't function arguments are addressed, those can be found by index directly. - let result = - if address.localIndex > type.parameters.count { - UnsafeRawPointer(frame.sp + Int(address.localIndex)) - } else { - // Otherwise we need function arguments, and those are stored in the frame header. - // See ``FrameHeaderLayout`` comments, we need to skip 3 bytes for: - // 1. Saved instance - // 2. Saved Pc. - // 3. Saved Sp - UnsafeRawPointer(frame.sp - FrameHeaderLayout.numberOfSavingSlots - type.parameters.count + Int(address.localIndex)) - } - return result - } - - throw Error.stackFrameIndexOOB(address.frameIndex) + return try self.stackFrame.withFrames(sp: breakpoint.iseq.sp, frameIndex: frameIndex, store: self.store, reader: reader) } /// Array of addresses in the Wasm binary of executed instructions on the call stack. diff --git a/Sources/WasmKit/Execution/DebuggerStackFrame.swift b/Sources/WasmKit/Execution/DebuggerStackFrame.swift index ebf3ae9a..b7d7c29f 100644 --- a/Sources/WasmKit/Execution/DebuggerStackFrame.swift +++ b/Sources/WasmKit/Execution/DebuggerStackFrame.swift @@ -1,13 +1,17 @@ #if WasmDebuggingSupport - struct DebuggerStackFrame: ~Copyable { - var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8) + package struct DebuggerStackFrame: ~Copyable { + private var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8) + + package struct Layout { + package fileprivate(set) var localOffsets = [Int]() + } init() { buffer.initializeMemory(as: Int.self, repeating: 0) } - mutating func withFrames(sp: Sp, frameIndex: Int, store: Store, writer: (borrowing OutputRawSpan) -> Void) throws { + mutating func withFrames(sp: Sp, frameIndex: Int, store: Store, reader: (borrowing RawSpan, Layout) -> T) throws -> T { self.buffer.initializeMemory(as: Int.self, repeating: 0) var i = 0 @@ -30,37 +34,40 @@ // Wasm function arguments are also addressed as locals. let functionType = store.engine.funcTypeInterner.resolve(currentFunction.type) - let pessimisticByteCount = functionType.parameters.count * 8 + wasm.locals.count * 8 + iseq.maxStackHeight * 8 + let stackSlotByteCount = MemoryLayout.size + + let pessimisticByteCount = functionType.parameters.count * stackSlotByteCount + wasm.locals.count * stackSlotByteCount + iseq.maxStackHeight * stackSlotByteCount if pessimisticByteCount > self.buffer.count { - let newBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: pessimisticByteCount, alignment: 8) + let newBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: pessimisticByteCount, alignment: MemoryLayout.alignment) newBuffer.copyBytes(from: self.buffer) self.buffer.deallocate() self.buffer = newBuffer } var span = OutputRawSpan(buffer: self.buffer, initializedCount: 0) + var layout = Layout() for (i, type) in functionType.parameters.enumerated() { - switch type { - case .i32, .f32: - span.append(frame.sp[i32: i], as: UInt32.self) - case .i64, .f64: - span.append(frame.sp[i64: i], as: UInt64.self) - case .v128: - fatalError("SIMD is not yet supported in the Wasm debugger") - case .ref: - fatalError("References are not yet supported in the wasm debugger") - } + // See ``FrameHeaderLayout`` documentation for offset calculation details. + type.append(to: &span, frame, offset: i - max(functionType.parameters.count, functionType.results.count)) + layout.localOffsets.append(span.byteCount) } - _ = span.finalize(for: self.buffer) + for (i, type) in wasm.locals.enumerated() { + type.append(to: &span, frame, offset: i) + layout.localOffsets.append(span.byteCount) + } + + // FIXME: copy over actual stack values + span.append(repeating: 0, count: iseq.maxStackHeight, as: UInt64.self) - let localsCount = functionType.parameters.count + currentFunction.numberOfNonParameterLocals - let localTypes = wasm.locals - iseq.maxStackHeight + _ = span.finalize(for: self.buffer) + return reader(self.buffer.bytes, layout) } + + throw Debugger.Error.stackFrameIndexOOB(frameIndex) } deinit { @@ -68,4 +75,23 @@ } } + extension ValueType { + fileprivate func append( + to span: inout OutputRawSpan, + _ frame: Execution.CallStack.FrameIterator.Element, + offset: Int + ) { + switch self { + case .i32, .f32: + span.append(frame.sp[i32: offset], as: UInt32.self) + case .i64, .f64: + span.append(frame.sp[i64: offset], as: UInt64.self) + case .v128: + fatalError("SIMD is not yet supported in the Wasm debugger") + case .ref: + fatalError("References are not yet supported in the wasm debugger") + } + } + } + #endif diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index af071950..17edeada 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -60,6 +60,9 @@ private var debugger: Debugger private let stackOffsetInProtocolSpace: UInt64 + /// Mapping from frame index to a buffer with packed representation of a frame with its layout. + private var stackFrames = [Int: (ByteBuffer, DebuggerStackFrame.Layout)]() + package init( moduleFilePath: FilePath, engineConfiguration: EngineConfiguration, @@ -280,6 +283,9 @@ throw Error.unknownThreadAction(threadActionString) } + // Stack frames become invalid after running or stepping. + self.stackFrames = [:] + switch threadAction { case .step: try self.debugger.step() @@ -290,6 +296,9 @@ responseKind = try self.currentThreadStopInfo case .continue: + // Stack frames become invalid after running or stepping. + self.stackFrames = [:] + try self.debugger.run() responseKind = try self.currentThreadStopInfo @@ -323,19 +332,31 @@ let arguments = command.arguments.split(separator: ";") guard arguments.count == 2, let frameIndexString = arguments.first, - let frameIndex = UInt32(frameIndexString), + let frameIndex = Int(frameIndexString), let localIndexString = arguments.last, - let localIndex = UInt32(localIndexString) + let localIndex = Int(localIndexString) else { throw Error.unknownWasmLocalArguments(command.arguments) } - let localAddress = Debugger.LocalAddress(frameIndex: frameIndex, localIndex: localIndex) - let localPointer = try self.debugger.getLocalPointer(address: localAddress) - let responseAddress = self.stackOffsetInProtocolSpace + UInt64(localPointer - self.debugger.stackMemory.baseAddress!) + let (buffer, layout) = try self.debugger.packedStackFrame(frameIndex: frameIndex) { span, layout in + var buffer = self.allocator.buffer(capacity: span.byteCount) + // Working around availability limitations until https://github.com/apple/swift-nio/pull/3447 is merged. + _ = span.withUnsafeBytes { + buffer.writeBytes($0) + } + return (buffer, layout) + } + + self.stackFrames[frameIndex] = (buffer, layout) + // FIXME: adjust the address so that frame indices are accounted for + let responseAddress = self.stackOffsetInProtocolSpace + UInt64(layout.localOffsets[localIndex]) + // let localPointer = try self.debugger.getLocalPointer(address: localAddress) + // print("localPointer is \(localPointer)") + // let responseAddress = self.stackOffsetInProtocolSpace + UInt64(localPointer - self.debugger.stackMemory.baseAddress!) responseKind = .hexEncodedBinary( - ByteBuffer( + self.allocator.buffer( integer: responseAddress, endianness: .little ).readableBytesView diff --git a/Sources/WasmTypes/WasmTypes.swift b/Sources/WasmTypes/WasmTypes.swift index 1c07fcb0..e8cf7533 100644 --- a/Sources/WasmTypes/WasmTypes.swift +++ b/Sources/WasmTypes/WasmTypes.swift @@ -36,16 +36,16 @@ public enum ValueType: Equatable, Hashable, Sendable { /// Reference value type. case ref(ReferenceType) -#if WasmDebuggingSupport - package var byteSize: Int { - switch self { - case .i32, .f32: 4 - case .i64, .f64: 8 - case .v128: 16 - case .ref: fatalError("bitwise copy of references not implemented yet") + #if WasmDebuggingSupport + package var byteSize: Int { + switch self { + case .i32, .f32: 4 + case .i64, .f64: 8 + case .v128: 16 + case .ref: fatalError("bitwise copy of references not implemented yet") + } } - } -#endif + #endif } /// Runtime representation of a WebAssembly function reference. From 3fecaf94fafdc2210fcf0a6750924c0662341d50 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 14 Nov 2025 17:34:15 +0000 Subject: [PATCH 21/23] Move debugger code from Swift 6.1 to 6.2 for `Span` availability --- Package@swift-6.1.swift => Package@swift-6.2.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename Package@swift-6.1.swift => Package@swift-6.2.swift (98%) diff --git a/Package@swift-6.1.swift b/Package@swift-6.2.swift similarity index 98% rename from Package@swift-6.1.swift rename to Package@swift-6.2.swift index 0370943e..0397de29 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.2.swift @@ -1,4 +1,4 @@ -// swift-tools-version:6.1 +// swift-tools-version:6.2 import PackageDescription @@ -32,7 +32,7 @@ let package = Package( .library(name: "_CabiShims", targets: ["_CabiShims"]), ], traits: [ - .default(enabledTraits: []), + .default(enabledTraits: ["WasmDebuggingSupport"]), "WasmDebuggingSupport", ], targets: [ @@ -139,7 +139,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.1"), .package(url: "https://github.com/apple/swift-system", from: "1.5.0"), - .package(url: "https://github.com/apple/swift-nio", from: "2.86.2"), + .package(url: "https://github.com/apple/swift-nio", from: "2.89.0"), .package(url: "https://github.com/apple/swift-log", from: "1.6.4"), ] } else { From 894b716580ed190275ca8620c3875239001c1a00 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 18 Nov 2025 16:55:29 +0000 Subject: [PATCH 22/23] Create separate `DebuggerMemoryCache` type --- Package@swift-6.2.swift | 2 +- Sources/WasmKit/Execution/Debugger.swift | 2 +- .../DebuggerMemoryCache.swift | 81 +++++++++++++++++++ .../WasmKitGDBHandler/WasmKitGDBHandler.swift | 69 ++++------------ 4 files changed, 99 insertions(+), 55 deletions(-) create mode 100644 Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift diff --git a/Package@swift-6.2.swift b/Package@swift-6.2.swift index 0397de29..16df0ceb 100644 --- a/Package@swift-6.2.swift +++ b/Package@swift-6.2.swift @@ -139,7 +139,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil { package.dependencies += [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.1"), .package(url: "https://github.com/apple/swift-system", from: "1.5.0"), - .package(url: "https://github.com/apple/swift-nio", from: "2.89.0"), + .package(url: "https://github.com/apple/swift-nio", from: "2.90.0"), .package(url: "https://github.com/apple/swift-log", from: "1.6.4"), ] } else { diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 2a507b13..3a59c907 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -20,7 +20,7 @@ case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer) case noInstructionMappingAvailable(Int) case noReverseInstructionMappingAvailable(UnsafeMutablePointer) - case stackFrameIndexOOB(UInt32) + case stackFrameIndexOOB(Int) case stackLocalIndexOOB(UInt32) case notStoppedAtBreakpoint } diff --git a/Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift b/Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift new file mode 100644 index 00000000..0e09054f --- /dev/null +++ b/Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift @@ -0,0 +1,81 @@ +#if WasmDebuggingSupport + + import NIOCore + import WasmKit + + let debuggerCodeOffset = UInt64(0x4000_0000_0000_0000) + + struct DebuggerMemoryCache: ~Copyable { + private let allocator: ByteBufferAllocator + private let stackOffsetInProtocolSpace: UInt64 + + private let wasmBinary: ByteBuffer + + /// Mapping from frame index to a buffer with packed representation of a frame with its layout. + private var stackFrames = [Int: (ByteBuffer, DebuggerStackFrame.Layout)]() + + init(allocator: ByteBufferAllocator, wasmBinary: ByteBuffer) { + self.allocator = allocator + + var stackOffset = Int(debuggerCodeOffset) + wasmBinary.readableBytes + // Untyped raw Wasm values in VM's stack are stored as `UInt64`. + stackOffset.roundUpToAlignment(for: UInt64.self) + + self.stackOffsetInProtocolSpace = UInt64(stackOffset) + self.wasmBinary = wasmBinary + } + + func getAddressOfLocal(debugger: inout Debugger, frameIndex: Int, localIndex: Int) throws -> UInt64 { + let (buffer, layout) = try debugger.packedStackFrame(frameIndex: frameIndex) { span, layout in + var buffer = self.allocator.buffer(capacity: span.byteCount) + buffer.writeBytes(span) + return (buffer, layout) + } + + self.stackFrames[frameIndex] = (buffer, layout) + // FIXME: adjust the address so that frame indices are accounted for + let responseAddress = self.stackOffsetInProtocolSpace + UInt64(layout.localOffsets[localIndex]) + // let localPointer = try self.debugger.getLocalPointer(address: localAddress) + // print("localPointer is \(localPointer)") + // let responseAddress = self.stackOffsetInProtocolSpace + UInt64(localPointer - self.debugger.stackMemory.baseAddress!) + + return responseAddress + + } + + func readMemory(debugger: borrowing Debugger, + addressInProtocolSpace: UInt64, + length: Int + ) -> ByteBufferView { + var length = length + + if addressInProtocolSpace >= self.stackOffsetInProtocolSpace { + print("stackMemory") + let stackAddress = Int(addressInProtocolSpace - self.stackOffsetInProtocolSpace) + print("stackAddress is \(stackAddress)") + if stackAddress + length > debugger.stackMemory.count { + length = debugger.stackMemory.count - stackAddress + } + + return ByteBuffer( + bytes: debugger.stackMemory[stackAddress..<(stackAddress + length)] + ).readableBytesView + } else if addressInProtocolSpace >= debuggerCodeOffset { + print("wasmBinary") + let codeAddress = Int(addressInProtocolSpace - debuggerCodeOffset) + if codeAddress + length > wasmBinary.readableBytes { + length = wasmBinary.readableBytes - codeAddress + } + + return wasmBinary.readableBytesView[codeAddress..<(codeAddress + length)] + } else { + fatalError("Linear memory reads are not implemented in the debugger yet.") + } + } + + mutating func invalidate() { + self.stackFrames = [:] + } + } + +#endif diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 17edeada..9f30fde8 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -32,8 +32,6 @@ } } - private let codeOffset = UInt64(0x4000_0000_0000_0000) - package actor WasmKitGDBHandler { enum ResumeThreadsAction: String { case step = "s" @@ -53,15 +51,12 @@ case unknownWasmLocalArguments(String) } - private let wasmBinary: ByteBuffer private let moduleFilePath: FilePath private let logger: Logger private let allocator: ByteBufferAllocator private var debugger: Debugger - private let stackOffsetInProtocolSpace: UInt64 - /// Mapping from frame index to a buffer with packed representation of a frame with its layout. - private var stackFrames = [Int: (ByteBuffer, DebuggerStackFrame.Layout)]() + private var memoryCache: DebuggerMemoryCache package init( moduleFilePath: FilePath, @@ -72,7 +67,7 @@ self.logger = logger self.allocator = allocator - self.wasmBinary = try await FileSystem.shared.withFileHandle(forReadingAt: moduleFilePath) { + let wasmBinary = try await FileSystem.shared.withFileHandle(forReadingAt: moduleFilePath) { try await $0.readToEnd(maximumSizeAllowed: .unlimited) } @@ -83,17 +78,14 @@ let wasi = try WASIBridgeToHost() wasi.link(to: &imports, store: store) - self.debugger = try Debugger(module: parseWasm(bytes: .init(buffer: self.wasmBinary)), store: store, imports: imports) + self.debugger = try Debugger(module: parseWasm(bytes: .init(buffer: wasmBinary)), store: store, imports: imports) try self.debugger.stopAtEntrypoint() try self.debugger.run() guard case .stoppedAtBreakpoint = self.debugger.state else { throw Error.stoppingAtEntrypointFailed } - var stackOffset = Int(codeOffset) + wasmBinary.readableBytes - // Untyped raw Wasm values in VM's stack are stored as `UInt64`. - stackOffset.roundUpToAlignment(for: UInt64.self) - self.stackOffsetInProtocolSpace = UInt64(stackOffset) + self.memoryCache = DebuggerMemoryCache(allocator: allocator, wasmBinary: wasmBinary) } private func hexDump(_ value: I, endianness: Endianness) -> String { @@ -243,26 +235,9 @@ var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - if addressInProtocolSpace >= self.stackOffsetInProtocolSpace { - let stackAddress = Int(addressInProtocolSpace - self.stackOffsetInProtocolSpace) - if stackAddress + length > self.debugger.stackMemory.count { - length = self.debugger.stackMemory.count - stackAddress - } - - responseKind = .hexEncodedBinary( - ByteBuffer( - bytes: self.debugger.stackMemory[stackAddress..<(stackAddress + length)] - ).readableBytesView) - } else if addressInProtocolSpace >= codeOffset { - let codeAddress = Int(addressInProtocolSpace - codeOffset) - if codeAddress + length > wasmBinary.readableBytes { - length = wasmBinary.readableBytes - codeAddress - } - - responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[codeAddress..<(codeAddress + length)]) - } else { - fatalError("Linear memory reads are not implemented in the debugger yet.") - } + responseKind = .hexEncodedBinary( + self.memoryCache.readMemory(addressInProtocolSpace: addressInProtocolSpace, length: length) + ) case .wasmCallStack: let callStack = self.debugger.currentCallStack @@ -284,7 +259,7 @@ } // Stack frames become invalid after running or stepping. - self.stackFrames = [:] + self.memoryCache.invalidate() switch threadAction { case .step: @@ -297,7 +272,7 @@ case .continue: // Stack frames become invalid after running or stepping. - self.stackFrames = [:] + self.memoryCache.invalidate() try self.debugger.run() @@ -313,7 +288,7 @@ argumentsString: command.arguments, separator: ",", endianness: .big - ) - codeOffset) + ) - debuggerCodeOffset) ) responseKind = .ok @@ -324,7 +299,7 @@ argumentsString: command.arguments, separator: ",", endianness: .big - ) - codeOffset) + ) - debuggerCodeOffset) ) responseKind = .ok @@ -339,25 +314,13 @@ throw Error.unknownWasmLocalArguments(command.arguments) } - let (buffer, layout) = try self.debugger.packedStackFrame(frameIndex: frameIndex) { span, layout in - var buffer = self.allocator.buffer(capacity: span.byteCount) - // Working around availability limitations until https://github.com/apple/swift-nio/pull/3447 is merged. - _ = span.withUnsafeBytes { - buffer.writeBytes($0) - } - return (buffer, layout) - } - - self.stackFrames[frameIndex] = (buffer, layout) - // FIXME: adjust the address so that frame indices are accounted for - let responseAddress = self.stackOffsetInProtocolSpace + UInt64(layout.localOffsets[localIndex]) - // let localPointer = try self.debugger.getLocalPointer(address: localAddress) - // print("localPointer is \(localPointer)") - // let responseAddress = self.stackOffsetInProtocolSpace + UInt64(localPointer - self.debugger.stackMemory.baseAddress!) - responseKind = .hexEncodedBinary( self.allocator.buffer( - integer: responseAddress, + integer: try self.memoryCache.getAddressOfLocal( + debugger: &self.debugger, + frameIndex: frameIndex, + localIndex: localIndex + ), endianness: .little ).readableBytesView ) From 6b0cb81c6f96c73f4b0453c0d3be1e4bb9612ec1 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 18 Nov 2025 16:55:45 +0000 Subject: [PATCH 23/23] Apply formatter --- .../DebuggerMemoryCache.swift | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift b/Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift index 0e09054f..321aecdc 100644 --- a/Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift +++ b/Sources/WasmKitGDBHandler/DebuggerMemoryCache.swift @@ -26,24 +26,25 @@ } func getAddressOfLocal(debugger: inout Debugger, frameIndex: Int, localIndex: Int) throws -> UInt64 { - let (buffer, layout) = try debugger.packedStackFrame(frameIndex: frameIndex) { span, layout in - var buffer = self.allocator.buffer(capacity: span.byteCount) - buffer.writeBytes(span) - return (buffer, layout) - } + let (buffer, layout) = try debugger.packedStackFrame(frameIndex: frameIndex) { span, layout in + var buffer = self.allocator.buffer(capacity: span.byteCount) + buffer.writeBytes(span) + return (buffer, layout) + } - self.stackFrames[frameIndex] = (buffer, layout) - // FIXME: adjust the address so that frame indices are accounted for - let responseAddress = self.stackOffsetInProtocolSpace + UInt64(layout.localOffsets[localIndex]) - // let localPointer = try self.debugger.getLocalPointer(address: localAddress) - // print("localPointer is \(localPointer)") - // let responseAddress = self.stackOffsetInProtocolSpace + UInt64(localPointer - self.debugger.stackMemory.baseAddress!) + self.stackFrames[frameIndex] = (buffer, layout) + // FIXME: adjust the address so that frame indices are accounted for + let responseAddress = self.stackOffsetInProtocolSpace + UInt64(layout.localOffsets[localIndex]) + // let localPointer = try self.debugger.getLocalPointer(address: localAddress) + // print("localPointer is \(localPointer)") + // let responseAddress = self.stackOffsetInProtocolSpace + UInt64(localPointer - self.debugger.stackMemory.baseAddress!) - return responseAddress + return responseAddress } - func readMemory(debugger: borrowing Debugger, + func readMemory( + debugger: borrowing Debugger, addressInProtocolSpace: UInt64, length: Int ) -> ByteBufferView { @@ -58,8 +59,8 @@ } return ByteBuffer( - bytes: debugger.stackMemory[stackAddress..<(stackAddress + length)] - ).readableBytesView + bytes: debugger.stackMemory[stackAddress..<(stackAddress + length)] + ).readableBytesView } else if addressInProtocolSpace >= debuggerCodeOffset { print("wasmBinary") let codeAddress = Int(addressInProtocolSpace - debuggerCodeOffset)