diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0a38b3a..4bf26ea2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -137,19 +137,21 @@ jobs: wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" test-args: "--traits WasmDebuggingSupport --enable-code-coverage" build-dev-dashboard: true - - swift: "swiftlang/swift:nightly-main-noble" - development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" - wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" - wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm - wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" - - swift: "swiftlang/swift:nightly-main-noble" - development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" - wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" - wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm - wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" - test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY --build-system swiftbuild" - label: " --build-system swiftbuild" + # Disabled until a toolchain containing https://github.com/swiftlang/swift/commit/b219d4089c922ceb8b700424236ca97f6087a9a1 + # is tagged. + # - swift: "swiftlang/swift:nightly-main-noble" + # development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" + # wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" + # wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm + # wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" + # test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY" +# - swift: "swiftlang/swift:nightly-main-noble" +# development-toolchain-download: "https://download.swift.org/development/ubuntu2404/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a-ubuntu24.04.tar.gz" +# wasi-swift-sdk-download: "https://download.swift.org/development/wasm-sdk/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a/swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm.artifactbundle.tar.gz" +# wasi-swift-sdk-id: swift-DEVELOPMENT-SNAPSHOT-2025-10-02-a_wasm +# wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9" +# test-args: "--traits WasmDebuggingSupport -Xswiftc -DWASMKIT_CI_TOOLCHAIN_NIGHTLY --build-system swiftbuild" +# label: " --build-system swiftbuild" runs-on: ubuntu-24.04 name: "build-linux (${{ matrix.swift }}${{ matrix.label }})" diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index d5b269ef..1a416aaf 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -21,7 +21,7 @@ let package = Package( ], traits: [ .default(enabledTraits: []), - "WasmDebuggingSupport" + "WasmDebuggingSupport", ], targets: [ .executableTarget( @@ -123,7 +123,8 @@ let package = Package( .target(name: "WITExtractor"), .testTarget(name: "WITExtractorTests", dependencies: ["WITExtractor", "WIT"]), - .target(name: "GDBRemoteProtocol", + .target( + name: "GDBRemoteProtocol", dependencies: [ .product(name: "Logging", package: "swift-log"), .product(name: "NIOCore", package: "swift-nio"), diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index cf0276dc..ff8cc6b7 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -38,6 +38,7 @@ package struct GDBHostCommand: Equatable { case readMemoryBinaryData case readMemory case wasmCallStack + case threadStopInfo case generalRegisters @@ -97,6 +98,7 @@ package struct GDBHostCommand: Equatable { /// - 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" if kindString.starts(with: "x") { self.kind = .readMemoryBinaryData @@ -109,6 +111,14 @@ package struct GDBHostCommand: Equatable { } else if kindString.starts(with: registerInfoPrefix) { self.kind = .registerInfo + guard arguments.isEmpty else { + throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue + } + self.arguments = String(kindString.dropFirst(registerInfoPrefix.count)) + return + } else if kindString.starts(with: threadStopInfoPrefix) { + self.kind = .threadStopInfo + guard arguments.isEmpty else { throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue } diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift index 37b76dc3..ca10e6ee 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponseEncoder.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging import NIOCore extension String { @@ -27,7 +28,12 @@ extension String { package class GDBTargetResponseEncoder: MessageToByteEncoder { private var isNoAckModeActive = false - package init() {} + private let logger: Logger + + package init(logger: Logger) { + self.logger = logger + } + package func encode(data: GDBTargetResponse, out: inout ByteBuffer) { if !isNoAckModeActive { out.writeInteger(UInt8(ascii: "+")) @@ -51,8 +57,9 @@ package class GDBTargetResponseEncoder: MessageToByteEncoder { out.writeString(str.appendedChecksum) case .hexEncodedBinary(let binary): - let hexDump = ByteBuffer(bytes: binary).hexDump(format: .compact) - out.writeString(hexDump.appendedChecksum) + let hexDumpResponse = ByteBuffer(bytes: binary).hexDump(format: .compact).appendedChecksum + self.logger.trace("GDBTargetResponseEncoder encoded a response", metadata: ["RawResponse": .string(hexDumpResponse)]) + out.writeString(hexDumpResponse) case .empty: out.writeString("".appendedChecksum) diff --git a/Sources/WAT/Encoder.swift b/Sources/WAT/Encoder.swift index 52b90740..baa28a3f 100644 --- a/Sources/WAT/Encoder.swift +++ b/Sources/WAT/Encoder.swift @@ -158,6 +158,7 @@ extension TableType: WasmEncodable { struct ElementExprCollector: AnyInstructionVisitor { typealias Output = Void + var binaryOffset: Int = 0 var isAllRefFunc: Bool = true var instructions: [Instruction] = [] @@ -443,6 +444,7 @@ extension WatParser.DataSegmentDecl { } struct ExpressionEncoder: BinaryInstructionEncoder { + var binaryOffset: Int = 0 var encoder = Encoder() var hasDataSegmentInstruction: Bool = false diff --git a/Sources/WAT/Parser/WastParser.swift b/Sources/WAT/Parser/WastParser.swift index cfc69fb1..70258283 100644 --- a/Sources/WAT/Parser/WastParser.swift +++ b/Sources/WAT/Parser/WastParser.swift @@ -54,6 +54,7 @@ struct WastParser { } struct ConstExpressionCollector: WastConstInstructionVisitor { + var binaryOffset: Int = 0 let addValue: (Value) -> Void mutating func visitI32Const(value: Int32) throws { addValue(.i32(UInt32(bitPattern: value))) } diff --git a/Sources/WasmKit/CMakeLists.txt b/Sources/WasmKit/CMakeLists.txt index 256aed0c..e77ea72a 100644 --- a/Sources/WasmKit/CMakeLists.txt +++ b/Sources/WasmKit/CMakeLists.txt @@ -11,6 +11,7 @@ add_wasmkit_library(WasmKit Component/CanonicalCall.swift Component/CanonicalOptions.swift Component/ComponentTypes.swift + Execution/DebuggerInstructionMapping.swift Execution/Instructions/Control.swift Execution/Instructions/Instruction.swift Execution/Instructions/Table.swift diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift new file mode 100644 index 00000000..01a22983 --- /dev/null +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -0,0 +1,201 @@ +#if WasmDebuggingSupport + /// User-facing debugger state 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 { + package enum Error: Swift.Error, @unchecked Sendable { + case entrypointFunctionNotFound + case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer) + case noInstructionMappingAvailable(Int) + case noReverseInstructionMappingAvailable(UnsafeMutablePointer) + } + + private let valueStack: Sp + private var execution: Execution + private let store: Store + + /// Parsed in-memory representation of a Wasm module instantiated for debugging. + private let module: Module + + /// Instance of parsed Wasm ``module``. + private let instance: Instance + + /// Reference to the entrypoint function of the currently debugged module, for use in ``stopAtEntrypoint``. + /// Currently assumed to be the WASI command `_start` entrypoint. + private let entrypointFunction: Function + + /// Threading model of the Wasm engine configuration, cached for a potentially hot path. + private let threadingModel: EngineConfiguration.ThreadingModel + + private(set) var breakpoints = [Int: CodeSlot]() + + private var currentBreakpoint: (iseq: Execution.Breakpoint, wasmPc: Int)? + + private var pc = Pc.allocate(capacity: 1) + + /// Initializes a new debugger state instance. + /// - Parameters: + /// - module: Wasm module to instantiate. + /// - store: Store that instantiates the module. + /// - imports: Imports required by `module` for instantiation. + package init(module: Module, store: Store, imports: Imports) throws { + let limit = store.engine.configuration.stackSize / MemoryLayout.stride + let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true) + + guard case .function(let entrypointFunction) = instance.exports["_start"] else { + throw Error.entrypointFunctionNotFound + } + + self.instance = instance + self.module = module + self.entrypointFunction = entrypointFunction + self.valueStack = UnsafeMutablePointer.allocate(capacity: limit) + 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) + } + + /// Sets a breakpoint at the first instruction in the entrypoint function of the module instantiated by + /// this debugger. + package mutating func stopAtEntrypoint() throws { + try self.enableBreakpoint(address: self.originalAddress(function: entrypointFunction)) + } + + /// 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 { + precondition(function.handle.isWasm) + + switch function.handle.wasm.code { + case .debuggable(let wasm, _): + return wasm.originalAddress + case .uncompiled: + try function.handle.wasm.ensureCompiled(store: StoreRef(self.store)) + return try self.originalAddress(function: function) + case .compiled: + fatalError() + } + } + + /// Enables a breakpoint at a given Wasm address. + /// - Parameter address: byte offset of the Wasm instruction that will be replaced with a breakpoint. If no + /// direct internal bytecode matching instruction is found, the next closest internal bytecode instruction + /// is replaced with a breakpoint. The original instruction to be restored is preserved in debugger state + /// represented by `self`. + /// See also ``Debugger/disableBreakpoint(address:)``. + package mutating func enableBreakpoint(address: Int) throws(Error) { + guard self.breakpoints[address] == nil else { + return + } + + guard let (iseq, wasm) = try self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else { + throw Error.noInstructionMappingAvailable(address) + } + + self.breakpoints[wasm] = iseq.pointee + iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel) + } + + /// Disables a breakpoint at a given Wasm address. If no breakpoint at a given address was previously set with + /// `self.enableBreakpoint(address:), this function immediately returns. + /// - Parameter address: byte offset of the Wasm instruction that was replaced with a breakpoint. The original + /// instruction is restored from debugger state and replaces the breakpoint instruction. + /// See also ``Debugger/enableBreakpoint(address:)``. + package mutating func disableBreakpoint(address: Int) throws(Error) { + guard let oldCodeSlot = self.breakpoints[address] else { + return + } + + guard let (iseq, wasm) = try self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else { + throw Error.noInstructionMappingAvailable(address) + } + + self.breakpoints[wasm] = nil + iseq.pointee = oldCodeSlot + } + + /// Resumes the module instantiated by the debugger stopped at a breakpoint. The breakpoint is disabled + /// and execution is resumed until the next breakpoint is triggered or all remaining instructions are + /// executed. If the module is not stopped at a breakpoint, this function returns immediately. + /// - Returns: `[Value]` result of `entrypointFunction` if current instance ran to completion, + /// `nil` if it stopped at a breakpoint. + package mutating func run() throws -> [Value]? { + do { + if let currentBreakpoint { + // Remove the breakpoint before resuming + try self.disableBreakpoint(address: currentBreakpoint.wasmPc) + self.execution.resetError() + + var sp = currentBreakpoint.iseq.sp + var pc = currentBreakpoint.iseq.pc + var md: Md = nil + var ms: Ms = 0 + + guard let currentFunction = sp.currentFunction else { + throw Error.unknownCurrentFunctionForResumedBreakpoint(sp) + } + + Execution.CurrentMemory.mayUpdateCurrentInstance( + instance: currentFunction.instance, + from: self.instance.handle, + md: &md, + ms: &ms + ) + + do { + switch self.threadingModel { + case .direct: + try self.execution.runDirectThreaded(sp: sp, pc: pc, md: md, ms: ms) + case .token: + try self.execution.runTokenThreaded(sp: &sp, pc: &pc, md: &md, ms: &ms) + } + } catch is Execution.EndOfExecution { + } + + let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) + return type.results.enumerated().map { (i, type) in + sp[VReg(i)].cast(to: type) + } + } else { + return try self.execution.executeWasm( + threadingModel: self.threadingModel, + function: self.entrypointFunction.handle, + type: self.entrypointFunction.type, + arguments: [], + sp: self.valueStack, + pc: self.pc + ) + } + } catch let breakpoint as Execution.Breakpoint { + let pc = breakpoint.pc + guard let wasmPc = self.instance.handle.instructionMapping.findWasm(forIseqAddress: pc) else { + throw Error.noReverseInstructionMappingAvailable(pc) + } + + self.currentBreakpoint = (breakpoint, wasmPc) + return nil + } + } + + /// Array of addresses in the Wasm binary of executed instructions on the call stack. + package var currentCallStack: [Int] { + guard let currentBreakpoint else { + return [] + } + + var result = Execution.captureBacktrace(sp: currentBreakpoint.iseq.sp, store: self.store).symbols.compactMap { + return self.instance.handle.instructionMapping.findWasm(forIseqAddress: $0.address) + } + result.append(currentBreakpoint.wasmPc) + + return result + } + + deinit { + self.valueStack.deallocate() + self.pc.deallocate() + } + } + +#endif diff --git a/Sources/WasmKit/Execution/DebuggerInstructionMapping.swift b/Sources/WasmKit/Execution/DebuggerInstructionMapping.swift new file mode 100644 index 00000000..fa076819 --- /dev/null +++ b/Sources/WasmKit/Execution/DebuggerInstructionMapping.swift @@ -0,0 +1,84 @@ +/// Two-way mapping between Wasm and internal iseq bytecode instructions. The implementation of the mapping +/// is private and is empty when `WasmDebuggingSupport` package trait is disabled. +struct DebuggerInstructionMapping { + #if WasmDebuggingSupport + + /// Mapping from iseq Pc to instruction addresses in the original binary. + /// Used for handling current call stack requests issued by a ``Debugger`` instance. + private var iseqToWasm = [Pc: Int]() + + /// Mapping from Wasm instruction addresses in the original binary to iseq instruction addresses. + /// Used for handling breakpoint requests issued by a ``Debugger`` instance. + private var wasmToIseq = [Int: Pc]() + + /// Wasm addresses sorted in ascending order for binary search when of the next closest mapped + /// instruction, when no key is found in `wasmToIseqMapping`. + private var wasmMappings = [Int]() + + mutating func add(wasm: Int, iseq: Pc) { + // Don't override the existing mapping, only store a new pair if there's no mapping for a given key. + if self.iseqToWasm[iseq] == nil { + self.iseqToWasm[iseq] = wasm + } + if self.wasmToIseq[wasm] == nil { + self.wasmToIseq[wasm] = iseq + } + self.wasmMappings.append(wasm) + } + + /// Computes an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. + /// - Parameter address: the Wasm instruction to find a mapping for. + /// - Returns: A tuple with an address of found iseq instruction and the original Wasm instruction or next + /// closest match if no direct match was found. + func findIseq(forWasmAddress address: Int) -> (iseq: Pc, wasm: Int)? { + // Look in the main mapping + if let iseq = self.wasmToIseq[address] { + return (iseq, address) + } + + // If nothing found, find the closest Wasm address using binary search + guard let nextAddress = self.wasmMappings.binarySearch(nextClosestTo: address), + // Look in the main mapping again with the next closest address if binary search produced anything + let iseq = self.wasmToIseq[nextAddress] + else { + return nil + } + + return (iseq, nextAddress) + } + + func findWasm(forIseqAddress pc: Pc) -> Int? { + self.iseqToWasm[pc] + } + #endif +} + +#if WasmDebuggingSupport + extension [Int] { + /// Uses binary search to find an element in `self` that's next closest to a given value. + /// - Parameter value: the array element to search for or to use as a baseline when searching. + /// - Returns: array element `result`, where `result - value` is the smallest possible, while + /// `result > value` also holds. + package func binarySearch(nextClosestTo value: Int) -> Int? { + switch self.count { + case 0: + return nil + default: + var slice = self[0.. 1 { + let middle = (slice.endIndex - slice.startIndex) / 2 + if slice[middle] < value { + // Not found anything in the lower half, assigning higher half to `slice`. + slice = slice[(middle + 1)..) { + self.store = store + self.stackEnd = stackEnd + } + #endif + /// Executes the given closure with a new execution state associated with /// the given ``Store`` instance. static func with( @@ -61,18 +68,15 @@ struct Execution { static func captureBacktrace(sp: Sp, store: Store) -> Backtrace { var frames = FrameIterator(sp: sp) - var symbols: [Backtrace.Symbol?] = [] + var symbols: [Backtrace.Symbol] = [] + while let frame = frames.next() { guard let function = frame.function else { - symbols.append(nil) + symbols.append(.init(name: nil, address: frame.pc)) continue } let symbolName = store.nameRegistry.symbolicate(.wasm(function)) - symbols.append( - Backtrace.Symbol( - name: symbolName - ) - ) + symbols.append(.init(name: symbolName, address: frame.pc)) } return Backtrace(symbols: symbols) } @@ -231,7 +235,7 @@ extension Sp { // MARK: - Special slots /// The current executing function. - fileprivate var currentFunction: EntityHandle? { + var currentFunction: EntityHandle? { get { return EntityHandle(bitPattern: UInt(self[-3].i64)) } nonmutating set { self[-3] = UInt64(UInt(bitPattern: newValue?.bitPattern ?? 0)) } } @@ -248,7 +252,7 @@ extension Sp { nonmutating set { self[-1] = UInt64(UInt(bitPattern: newValue)) } } - fileprivate var currentInstance: InternalInstance? { + var currentInstance: InternalInstance? { currentFunction?.instance } } @@ -281,8 +285,7 @@ func executeWasm( store: Store, function handle: InternalFunction, type: FunctionType, - arguments: [Value], - callerInstance: InternalInstance + arguments: [Value] ) throws -> [Value] { // NOTE: `store` variable must not outlive this function let store = StoreRef(store) @@ -315,6 +318,43 @@ func executeWasm( } extension Execution { + + #if WasmDebuggingSupport + + /// Counterpart to the free `executeWasm` function but implemented as a method of `Execution`, + /// Useful for representation of debugger state that needs to own `Execution`'s memory. + mutating func executeWasm( + threadingModel: EngineConfiguration.ThreadingModel, + function handle: InternalFunction, + type: FunctionType, + arguments: [Value], + sp: Sp, + pc: Pc + ) throws -> [Value] { + // Advance the stack pointer to be able to reference negative indices + // for saving slots. + let sp = sp.advanced(by: FrameHeaderLayout.numberOfSavingSlots) + // Mark root stack pointer and current function as nil. + sp.previousSP = nil + sp.currentFunction = nil + for (index, argument) in arguments.enumerated() { + sp[VReg(index)] = UntypedValue(argument) + } + + try self.execute( + sp: sp, + pc: pc, + handle: handle, + type: type + ) + + return type.results.enumerated().map { (i, type) in + sp[VReg(i)].cast(to: type) + } + } + + #endif + /// A namespace for the "current memory" (Md and Ms) management. enum CurrentMemory { /// Assigns the current memory to the given internal memory. @@ -364,7 +404,10 @@ extension Execution { struct EndOfExecution: Error {} /// An ``Error`` thrown when a breakpoint is triggered. - struct Breakpoint: Error {} + struct Breakpoint: Error, @unchecked Sendable { + let sp: Sp + let pc: Pc + } /// The entry point for the execution of the WebAssembly function. @inline(never) @@ -516,6 +559,11 @@ extension Execution { self.trap = (rawError, sp) } + /// Used by the debugger to resume execution after breakpoints. + mutating func resetError() { + self.trap = nil + } + @inline(__always) func checkStackBoundary(_ sp: Sp) throws { guard sp < stackEnd else { throw Trap(.callStackExhausted) } diff --git a/Sources/WasmKit/Execution/Function.swift b/Sources/WasmKit/Execution/Function.swift index 3786cf16..93aa8bde 100644 --- a/Sources/WasmKit/Execution/Function.swift +++ b/Sources/WasmKit/Execution/Function.swift @@ -172,8 +172,7 @@ extension InternalFunction { store: store, function: self, type: resolvedType, - arguments: arguments, - callerInstance: entity.instance + arguments: arguments ) } else { let entity = host @@ -243,7 +242,7 @@ struct WasmFunctionEntity { switch code { case .uncompiled(let code): return try compile(store: store, code: code) - case .compiled(let iseq), .compiledAndPatchable(_, let iseq): + case .compiled(let iseq), .debuggable(_, let iseq): return iseq } } @@ -280,10 +279,14 @@ extension EntityHandle { case .uncompiled(let code): return try self.withValue { let iseq = try $0.compile(store: store, code: code) - $0.code = .compiled(iseq) + if $0.instance.isDebuggable { + $0.code = .debuggable(code, iseq) + } else { + $0.code = .compiled(iseq) + } return iseq } - case .compiled(let iseq), .compiledAndPatchable(_, let iseq): + case .compiled(let iseq), .debuggable(_, let iseq): return iseq } } @@ -316,7 +319,7 @@ struct InstructionSequence { enum CodeBody { case uncompiled(InternalUncompiledCode) case compiled(InstructionSequence) - case compiledAndPatchable(InternalUncompiledCode, InstructionSequence) + case debuggable(InternalUncompiledCode, InstructionSequence) } extension Reference { diff --git a/Sources/WasmKit/Execution/Instances.swift b/Sources/WasmKit/Execution/Instances.swift index 27a2452a..f5234b5d 100644 --- a/Sources/WasmKit/Execution/Instances.swift +++ b/Sources/WasmKit/Execution/Instances.swift @@ -83,6 +83,9 @@ struct InstanceEntity /* : ~Copyable */ { var functionRefs: Set var features: WasmFeatureSet var dataCount: UInt32? + var isDebuggable: Bool + + var instructionMapping: DebuggerInstructionMapping static var empty: InstanceEntity { InstanceEntity( @@ -96,7 +99,9 @@ struct InstanceEntity /* : ~Copyable */ { exports: [:], functionRefs: [], features: [], - dataCount: nil + dataCount: nil, + isDebuggable: false, + instructionMapping: .init() ) } diff --git a/Sources/WasmKit/Execution/Instructions/Control.swift b/Sources/WasmKit/Execution/Instructions/Control.swift index a3cdedba..94f12535 100644 --- a/Sources/WasmKit/Execution/Instructions/Control.swift +++ b/Sources/WasmKit/Execution/Instructions/Control.swift @@ -225,6 +225,10 @@ extension Execution { } mutating func breakpoint(sp: inout Sp, pc: Pc) throws -> (Pc, CodeSlot) { - throw Breakpoint() + throw Breakpoint( + sp: sp, + // Throw `pc` value before the breakpoint was triggered to allow resumption in same place + pc: pc - 1 + ) } } diff --git a/Sources/WasmKit/Execution/StoreAllocator.swift b/Sources/WasmKit/Execution/StoreAllocator.swift index f060dc56..8ca44f07 100644 --- a/Sources/WasmKit/Execution/StoreAllocator.swift +++ b/Sources/WasmKit/Execution/StoreAllocator.swift @@ -251,7 +251,8 @@ extension StoreAllocator { module: Module, engine: Engine, resourceLimiter: any ResourceLimiter, - imports: Imports + imports: Imports, + isDebuggable: Bool ) throws -> InternalInstance { // Step 1 of module allocation algorithm, according to Wasm 2.0 spec. @@ -450,7 +451,9 @@ extension StoreAllocator { exports: exports, functionRefs: functionRefs, features: module.features, - dataCount: module.dataCount + dataCount: module.dataCount, + isDebuggable: isDebuggable, + instructionMapping: .init() ) instancePointer.initialize(to: instanceEntity) instanceInitialized = true diff --git a/Sources/WasmKit/Module.swift b/Sources/WasmKit/Module.swift index 30070d1a..61ad28c8 100644 --- a/Sources/WasmKit/Module.swift +++ b/Sources/WasmKit/Module.swift @@ -138,9 +138,23 @@ public struct Module { Instance(handle: try self.instantiateHandle(store: store, imports: imports), store: store) } + #if WasmDebuggingSupport + /// Instantiate this module with the given imports. + /// + /// - Parameters: + /// - store: The ``Store`` to allocate the instance in. + /// - imports: The imports to use for instantiation. All imported entities + /// must be allocated in the given store. + /// - isDebuggable: Whether the module should support debugging actions + /// (breakpoints etc) after instantiation. + public func instantiate(store: Store, imports: Imports = [:], isDebuggable: Bool) throws -> Instance { + Instance(handle: try self.instantiateHandle(store: store, imports: imports, isDebuggable: isDebuggable), store: store) + } + #endif + /// > Note: /// - private func instantiateHandle(store: Store, imports: Imports) throws -> InternalInstance { + private func instantiateHandle(store: Store, imports: Imports, isDebuggable: Bool = false) throws -> InternalInstance { try ModuleValidator(module: self).validate() // Steps 5-8. @@ -152,7 +166,8 @@ public struct Module { let instance = try store.allocator.allocate( module: self, engine: store.engine, resourceLimiter: store.resourceLimiter, - imports: imports + imports: imports, + isDebuggable: isDebuggable ) if let nameSection = customSections.first(where: { $0.name == "name" }) { diff --git a/Sources/WasmKit/Translator.swift b/Sources/WasmKit/Translator.swift index 6f3e1116..2731927d 100644 --- a/Sources/WasmKit/Translator.swift +++ b/Sources/WasmKit/Translator.swift @@ -811,7 +811,7 @@ struct InstructionTranslator: InstructionVisitor { let allocator: ISeqAllocator let funcTypeInterner: Interner - let module: InternalInstance + var module: InternalInstance private var iseqBuilder: ISeqBuilder var controlStack: ControlStack var valueStack: ValueStack @@ -822,11 +822,19 @@ struct InstructionTranslator: InstructionVisitor { let functionIndex: FunctionIndex /// Whether a call to this function should be intercepted let isIntercepting: Bool - /// Whether Wasm debugging facilities are currently enabled. - let isDebugging: Bool var constantSlots: ConstSlots let validator: InstructionValidator + // Wasm debugging support. + + /// Current offset to an instruction in the original Wasm binary processed by this translator. + var binaryOffset: Int = 0 + + /// Mapping from `self.iseqBuilder.instructions` to Wasm instructions. + /// As mapping between iSeq to Wasm is many:many, but we only care about first mapping for overlapping address, + /// we need to iterate on it in the order the mappings were stored to ensure we don't overwrite the frist mapping. + var iseqToWasmMapping = [(iseq: Int, wasm: Int)]() + init( allocator: ISeqAllocator, engineConfiguration: EngineConfiguration, @@ -836,8 +844,7 @@ struct InstructionTranslator: InstructionVisitor { locals: [WasmTypes.ValueType], functionIndex: FunctionIndex, codeSize: Int, - isIntercepting: Bool, - isDebugging: Bool = false + isIntercepting: Bool ) throws { self.allocator = allocator self.funcTypeInterner = funcTypeInterner @@ -854,7 +861,6 @@ struct InstructionTranslator: InstructionVisitor { self.locals = Locals(types: type.parameters + locals) self.functionIndex = functionIndex self.isIntercepting = isIntercepting - self.isDebugging = isDebugging self.constantSlots = ConstSlots(stackLayout: stackLayout) self.validator = InstructionValidator(context: module) @@ -878,12 +884,14 @@ struct InstructionTranslator: InstructionVisitor { } private mutating func emit(_ instruction: Instruction, resultRelink: ISeqBuilder.ResultRelink? = nil) { + self.updateInstructionMapping() iseqBuilder.emit(instruction, resultRelink: resultRelink) } @discardableResult private mutating func emitCopyStack(from source: VReg, to dest: VReg) -> Bool { guard source != dest else { return false } + self.updateInstructionMapping() emit(.copyStack(Instruction.CopyStackOperand(source: LVReg(source), dest: LVReg(dest)))) return true } @@ -1069,6 +1077,7 @@ struct InstructionTranslator: InstructionVisitor { emit(.onExit(functionIndex)) } try visitReturnLike() + self.updateInstructionMapping() iseqBuilder.emit(._return) } private mutating func markUnreachable() throws { @@ -1088,9 +1097,18 @@ struct InstructionTranslator: InstructionVisitor { let instructions = iseqBuilder.finalize() // TODO: Figure out a way to avoid the copy here while keeping the execution performance. let buffer = allocator.allocateInstructions(capacity: instructions.count) - for (idx, instruction) in instructions.enumerated() { - buffer[idx] = instruction - } + let initializedElementsIndex = buffer.initialize(fromContentsOf: instructions) + assert(initializedElementsIndex == instructions.endIndex) + + #if WasmDebuggingSupport + for (iseq, wasm) in self.iseqToWasmMapping { + self.module.withValue { + let absoluteIseq = iseq + buffer.baseAddress.unsafelyUnwrapped + $0.instructionMapping.add(wasm: wasm, iseq: absoluteIseq) + } + } + #endif + let constants = allocator.allocateConstants(self.constantSlots.values) return InstructionSequence( instructions: buffer, @@ -1099,6 +1117,15 @@ struct InstructionTranslator: InstructionVisitor { ) } + private mutating func updateInstructionMapping() { + // This is a hot path, so best to exclude the code altogether if the trait isn't enabled. + #if WasmDebuggingSupport + guard self.module.isDebuggable else { return } + + self.iseqToWasmMapping.append((self.iseqBuilder.insertingPC.offsetFromHead, self.binaryOffset)) + #endif + } + // MARK: Main entry point /// Translate a Wasm expression into a sequence of instructions. @@ -1126,7 +1153,9 @@ struct InstructionTranslator: InstructionVisitor { emit(.unreachable) try markUnreachable() } - mutating func visitNop() -> Output { emit(.nop) } + mutating func visitNop() -> Output { + emit(.nop) + } mutating func visitBlock(blockType: WasmParser.BlockType) throws -> Output { let blockType = try module.resolveBlockType(blockType) @@ -1173,6 +1202,7 @@ struct InstructionTranslator: InstructionVisitor { ) ) guard let condition = condition else { return } + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, endLabel) { iseqBuilder, selfPC, endPC in let targetPC: MetaProgramCounter if let elsePC = iseqBuilder.resolveLabel(elseLabel) { @@ -1193,6 +1223,8 @@ struct InstructionTranslator: InstructionVisitor { preserveOnStack(depth: valueStack.height - frame.stackHeight) try controlStack.resetReachability() iseqBuilder.resetLastEmission() + + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, endLabel) { _, selfPC, endPC in let offset = endPC.offsetFromHead - selfPC.offsetFromHead return Int32(offset) @@ -1288,6 +1320,8 @@ struct InstructionTranslator: InstructionVisitor { currentFrame: try controlStack.currentFrame(), currentHeight: valueStack.height ) + + self.updateInstructionMapping() iseqBuilder.emitWithLabel(makeInstruction, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return make(Int32(relativeOffset), UInt32(copyCount), popCount) @@ -1321,6 +1355,7 @@ struct InstructionTranslator: InstructionVisitor { if frame.copyCount == 0 { guard let condition else { return } // Optimization where we don't need copying values when the branch taken + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIf, frame.continuation) { _, selfPC, continuation in let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead return Instruction.BrIfOperand( @@ -1353,11 +1388,13 @@ struct InstructionTranslator: InstructionVisitor { // [0x06] (local.get 1 reg:2) <----|---------+ // [0x07] ... <-------+ let onBranchNotTaken = iseqBuilder.allocLabel() + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.brIfNot, onBranchNotTaken) { _, conditionCheckAt, continuation in let relativeOffset = continuation.offsetFromHead - conditionCheckAt.offsetFromHead return Instruction.BrIfOperand(condition: LVReg(condition), offset: Int32(relativeOffset)) } try copyOnBranch(targetFrame: frame) + self.updateInstructionMapping() try emitBranch(Instruction.br, relativeDepth: relativeDepth) { offset, copyCount, popCount in return offset } @@ -1384,6 +1421,7 @@ struct InstructionTranslator: InstructionVisitor { baseAddress: tableBuffer.baseAddress!, count: UInt16(tableBuffer.count), index: index ) + self.updateInstructionMapping() iseqBuilder.emit(.brTable(operand)) let brTableAt = iseqBuilder.insertingPC @@ -1433,6 +1471,7 @@ struct InstructionTranslator: InstructionVisitor { } let emittedCopy = try copyOnBranch(targetFrame: frame) if emittedCopy { + self.updateInstructionMapping() iseqBuilder.emitWithLabel(Instruction.br, frame.continuation) { _, brAt, continuation in let relativeOffset = continuation.offsetFromHead - brAt.offsetFromHead return Int32(relativeOffset) @@ -1858,6 +1897,7 @@ struct InstructionTranslator: InstructionVisitor { } private mutating func visitConst(_ type: ValueType, _ value: Value) { + // TODO: document this behavior if let constSlotIndex = constantSlots.allocate(value) { valueStack.pushConst(constSlotIndex, type: type) iseqBuilder.resetLastEmission() @@ -2260,16 +2300,6 @@ struct InstructionTranslator: InstructionVisitor { return .tableSize(Instruction.TableSizeOperand(tableIndex: table, result: LVReg(result))) } } - - mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { - guard self.isDebugging && opcode.count == 1 && opcode[0] == 0xFF else { - return false - } - - emit(.breakpoint) - - return true - } } struct TranslationError: Error, CustomStringConvertible { diff --git a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift index 5313bdd3..35fd0adc 100644 --- a/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift +++ b/Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift @@ -18,6 +18,7 @@ import NIOFileSystem import SystemPackage import WasmKit + import WasmKitWASI extension BinaryInteger { init?(hexEncoded: Substring) { @@ -31,25 +32,41 @@ } } + private let codeOffset = UInt64(0x4000_0000_0000_0000) + package actor WasmKitGDBHandler { enum Error: Swift.Error { case unknownTransferArguments case unknownReadMemoryArguments + case stoppingAtEntrypointFailed } private let wasmBinary: ByteBuffer private let moduleFilePath: FilePath private let logger: Logger - private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = [] + private let allocator: ByteBufferAllocator + private var debugger: Debugger - package init(logger: Logger, moduleFilePath: FilePath) async throws { + package init(moduleFilePath: FilePath, logger: Logger, allocator: ByteBufferAllocator) async throws { self.logger = logger + self.allocator = allocator self.wasmBinary = try await FileSystem.shared.withFileHandle(forReadingAt: moduleFilePath) { try await $0.readToEnd(maximumSizeAllowed: .unlimited) } self.moduleFilePath = moduleFilePath + + let store = Store(engine: Engine()) + var imports = Imports() + 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) + try self.debugger.stopAtEntrypoint() + guard try self.debugger.run() == nil else { + throw Error.stoppingAtEntrypointFailed + } } package func handle(command: GDBHostCommand) throws -> GDBTargetResponse { @@ -101,7 +118,7 @@ case .subsequentThreadInfo: responseKind = .string("l") - case .targetStatus: + case .targetStatus, .threadStopInfo: responseKind = .keyValuePairs([ "T05thread": "1", "reason": "trace", @@ -144,7 +161,7 @@ var length = Int(hexEncoded: argumentsArray[1]) else { throw Error.unknownReadMemoryArguments } - let binaryOffset = Int(address - 0x4000_0000_0000_0000) + let binaryOffset = Int(address - codeOffset) if binaryOffset + length > wasmBinary.readableBytes { length = wasmBinary.readableBytes - binaryOffset @@ -152,7 +169,15 @@ responseKind = .hexEncodedBinary(wasmBinary.readableBytesView[binaryOffset..<(binaryOffset + length)]) - case .wasmCallStack, .generalRegisters: + case .wasmCallStack: + let callStack = self.debugger.currentCallStack + var buffer = self.allocator.buffer(capacity: callStack.count * 8) + for pc in callStack { + buffer.writeInteger(UInt64(pc) + codeOffset, endianness: .little) + } + responseKind = .hexEncodedBinary(buffer.readableBytesView) + + case .generalRegisters: fatalError() } diff --git a/Sources/WasmParser/BinaryInstructionDecoder.swift b/Sources/WasmParser/BinaryInstructionDecoder.swift index 5cc84908..a6a4ac6d 100644 --- a/Sources/WasmParser/BinaryInstructionDecoder.swift +++ b/Sources/WasmParser/BinaryInstructionDecoder.swift @@ -6,6 +6,9 @@ import WasmTypes @usableFromInline protocol BinaryInstructionDecoder { + /// Current offset in the decoded Wasm binary. + var offset: Int { get } + /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -91,6 +94,7 @@ protocol BinaryInstructionDecoder { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { + visitor.binaryOffset = decoder.offset let opcode0 = try decoder.claimNextByte() switch opcode0 { case 0x00: diff --git a/Sources/WasmParser/InstructionVisitor.swift b/Sources/WasmParser/InstructionVisitor.swift index 2a6b0271..b44d39a8 100644 --- a/Sources/WasmParser/InstructionVisitor.swift +++ b/Sources/WasmParser/InstructionVisitor.swift @@ -310,6 +310,9 @@ extension AnyInstructionVisitor { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { + /// Current offset in visitor's instruction stream. + var binaryOffset: Int { get set } + /// Visiting `unreachable` instruction. mutating func visitUnreachable() throws /// Visiting `nop` instruction. diff --git a/Sources/WasmParser/WasmParser.swift b/Sources/WasmParser/WasmParser.swift index 0bca6049..0b7c7199 100644 --- a/Sources/WasmParser/WasmParser.swift +++ b/Sources/WasmParser/WasmParser.swift @@ -757,6 +757,8 @@ extension Parser: BinaryInstructionDecoder { @usableFromInline struct InstructionFactory: AnyInstructionVisitor { + @usableFromInline var binaryOffset: Int = 0 + @usableFromInline var insts: [Instruction] = [] @inlinable init() {} diff --git a/Sources/WasmParser/WasmTypes.swift b/Sources/WasmParser/WasmTypes.swift index df95b6c2..153f8cb9 100644 --- a/Sources/WasmParser/WasmTypes.swift +++ b/Sources/WasmParser/WasmTypes.swift @@ -15,6 +15,10 @@ public struct Code { @usableFromInline internal let features: WasmFeatureSet + #if WasmDebuggingSupport + package var originalAddress: Int { self.offset } + #endif + @inlinable init(locals: [ValueType], expression: ArraySlice, offset: Int, features: WasmFeatureSet) { self.locals = locals diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift new file mode 100644 index 00000000..267e2ea1 --- /dev/null +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -0,0 +1,55 @@ +#if WasmDebuggingSupport + + import Testing + import WAT + @testable import WasmKit + + private let trivialModuleWAT = """ + (module + (func (export "_start") (result i32) (local $x i32) + (i32.const 42) + (i32.const 0) + (i32.eqz) + (drop) + (local.set $x) + (local.get $x) + ) + ) + """ + + @Suite + struct DebuggerTests { + @Test + func stopAtEntrypoint() 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: [:]) + + try debugger.stopAtEntrypoint() + #expect(debugger.breakpoints.count == 1) + + #expect(try debugger.run() == nil) + + let expectedPc = try #require(debugger.breakpoints.keys.first) + #expect(debugger.currentCallStack == [expectedPc]) + + #expect(try debugger.run() == [.i32(42)]) + } + + @Test + func binarySearch() throws { + #expect([Int]().binarySearch(nextClosestTo: 42) == nil) + + var result = try #require([1].binarySearch(nextClosestTo: 8)) + #expect(result == 1) + + result = try #require([9, 15, 37].binarySearch(nextClosestTo: 28)) + #expect(result == 37) + + result = try #require([9, 15, 37].binarySearch(nextClosestTo: 0)) + #expect(result == 9) + } + } + +#endif diff --git a/Tests/WasmKitTests/ExecutionTests.swift b/Tests/WasmKitTests/ExecutionTests.swift index 501789ed..79b9201f 100644 --- a/Tests/WasmKitTests/ExecutionTests.swift +++ b/Tests/WasmKitTests/ExecutionTests.swift @@ -110,7 +110,7 @@ struct ExecutionTests { """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.?.name) == [ + trap.backtrace?.symbols.compactMap(\.name) == [ "foo", "bar", "_start", @@ -137,7 +137,7 @@ struct ExecutionTests { """ ) { trap in #expect( - trap.backtrace?.symbols.compactMap(\.?.name) == [ + trap.backtrace?.symbols.compactMap(\.name) == [ "wasm function[1]", "bar", "_start", diff --git a/Utilities/Sources/WasmGen.swift b/Utilities/Sources/WasmGen.swift index af354614..1e217bda 100644 --- a/Utilities/Sources/WasmGen.swift +++ b/Utilities/Sources/WasmGen.swift @@ -96,6 +96,9 @@ enum WasmGen { /// The visitor pattern is used while parsing WebAssembly expressions to allow for easy extensibility. /// See the expression parsing method ``Code/parseExpression(visitor:)`` public protocol InstructionVisitor { + /// Current offset in visitor's instruction stream. + var binaryOffset: Int { get set } + """ for instruction in instructions.categorized { @@ -531,6 +534,9 @@ enum WasmGen { @usableFromInline protocol BinaryInstructionDecoder { + /// Current offset in the decoded Wasm binary. + var offset: Int { get } + /// Claim the next byte to be decoded @inlinable func claimNextByte() throws -> UInt8 @@ -562,6 +568,7 @@ enum WasmGen { @inlinable func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool { + visitor.binaryOffset = decoder.offset """ func renderSwitchCase(_ root: Trie, depth: Int = 0) { diff --git a/Utilities/format.py b/Utilities/format.py index eb5b9c4e..70d93143 100755 --- a/Utilities/format.py +++ b/Utilities/format.py @@ -58,6 +58,7 @@ def main(): for target in targets: arguments.append(os.path.join(SOURCE_ROOT, target)) arguments.append(os.path.join(SOURCE_ROOT, "Package.swift")) + arguments.append(os.path.join(SOURCE_ROOT, "Package@swift-6.1.swift")) run(arguments)