diff --git a/Sources/GDBRemoteProtocol/GDBHostCommand.swift b/Sources/GDBRemoteProtocol/GDBHostCommand.swift index ff8cc6b7..d8ec29a5 100644 --- a/Sources/GDBRemoteProtocol/GDBHostCommand.swift +++ b/Sources/GDBRemoteProtocol/GDBHostCommand.swift @@ -39,6 +39,10 @@ package struct GDBHostCommand: Equatable { case readMemory case wasmCallStack case threadStopInfo + case symbolLookup + case jsonThreadsInfo + case jsonThreadExtendedInfo + case resumeThreads case generalRegisters @@ -79,6 +83,12 @@ package struct GDBHostCommand: Equatable { self = .transfer case "qWasmCallStack": self = .wasmCallStack + case "qSymbol": + self = .symbolLookup + case "jThreadsInfo": + self = .jsonThreadsInfo + case "jThreadExtendedInfo": + self = .jsonThreadExtendedInfo default: return nil @@ -99,6 +109,7 @@ package struct GDBHostCommand: Equatable { 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 @@ -124,6 +135,12 @@ package struct GDBHostCommand: Equatable { } 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) { self.kind = kind } else { diff --git a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift index 77391d73..76275fb5 100644 --- a/Sources/GDBRemoteProtocol/GDBTargetResponse.swift +++ b/Sources/GDBRemoteProtocol/GDBTargetResponse.swift @@ -34,7 +34,7 @@ package struct GDBTargetResponse { /// A list of key-value pairs, with keys delimited from values by a colon `:` /// character, and pairs in the list delimited by the semicolon `;` character. - case keyValuePairs(KeyValuePairs) + case keyValuePairs([(String, String)]) /// List of ``VContActions`` values delimited by the semicolon `;` character. case vContSupportedActions([VContActions]) diff --git a/Sources/WasmKit/Execution/Debugger.swift b/Sources/WasmKit/Execution/Debugger.swift index 01a22983..b2a70bde 100644 --- a/Sources/WasmKit/Execution/Debugger.swift +++ b/Sources/WasmKit/Execution/Debugger.swift @@ -151,6 +151,7 @@ try self.execution.runTokenThreaded(sp: &sp, pc: &pc, md: &md, ms: &ms) } } catch is Execution.EndOfExecution { + // The module successfully executed till the "end of execution" instruction. } let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type) @@ -178,6 +179,21 @@ } } + /// Steps by a single Wasm instruction in the module instantiated by the debugger stopped at a breakpoint. + /// The current breakpoint is disabled and new breakpoints are put on the next instruction (or instructions in case + /// of multiple possible execution branches). After breakpoints setup, execution is resumed until suspension. + /// If the module is not stopped at a breakpoint, this function returns immediately. + package mutating func step() throws { + guard let currentBreakpoint else { + return + } + + // TODO: analyze actual instruction branching to set the breakpoint correctly. + try self.enableBreakpoint(address: currentBreakpoint.wasmPc + 1) + let result = try self.run() + assert(result == nil) + } + /// Array of addresses in the Wasm binary of executed instructions on the call stack. package var currentCallStack: [Int] { guard let currentBreakpoint else { diff --git a/Sources/WasmKit/Execution/DebuggerInstructionMapping.swift b/Sources/WasmKit/Execution/DebuggerInstructionMapping.swift index fa076819..46a1e110 100644 --- a/Sources/WasmKit/Execution/DebuggerInstructionMapping.swift +++ b/Sources/WasmKit/Execution/DebuggerInstructionMapping.swift @@ -23,7 +23,9 @@ struct DebuggerInstructionMapping { if self.wasmToIseq[wasm] == nil { self.wasmToIseq[wasm] = iseq } - self.wasmMappings.append(wasm) + if self.wasmMappings.last != wasm { + self.wasmMappings.append(wasm) + } } /// Computes an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address. @@ -65,14 +67,20 @@ struct DebuggerInstructionMapping { return nil default: var slice = self[0.. 1 { + while let last = slice.last { + guard last >= value else { return nil } + let middle = (slice.endIndex - slice.startIndex) / 2 + guard middle > 0 else { break } + if slice[middle] < value { // Not found anything in the lower half, assigning higher half to `slice`. slice = slice[(middle + 1).. value && slice[middle - 1] > value { // Not found anything in the higher half, assigning lower half to `slice`. slice = slice[slice.startIndex.. GDBTargetResponse { let responseKind: GDBTargetResponse.Kind logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)]) @@ -84,11 +110,11 @@ case .hostInfo: responseKind = .keyValuePairs([ - "arch": "wasm32", - "ptrsize": "4", - "endian": "little", - "ostype": "wasip1", - "vendor": "WasmKit", + ("arch", "wasm32"), + ("ptrsize", "4"), + ("endian", "little"), + ("ostype", "wasip1"), + ("vendor", "WasmKit"), ]) case .supportedFeatures: @@ -97,16 +123,17 @@ case .vContSupportedActions: responseKind = .vContSupportedActions([.continue, .step]) - case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData: + case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData, + .symbolLookup, .jsonThreadsInfo, .jsonThreadExtendedInfo: responseKind = .empty case .processInfo: responseKind = .keyValuePairs([ - "pid": "1", - "parent-pid": "1", - "arch": "wasm32", - "endian": "little", - "ptrsize": "4", + ("pid", "1"), + ("parent-pid", "1"), + ("arch", "wasm32"), + ("endian", "little"), + ("ptrsize", "4"), ]) case .currentThreadID: @@ -119,23 +146,20 @@ responseKind = .string("l") case .targetStatus, .threadStopInfo: - responseKind = .keyValuePairs([ - "T05thread": "1", - "reason": "trace", - ]) + responseKind = self.currentThreadStopInfo case .registerInfo: if command.arguments == "0" { responseKind = .keyValuePairs([ - "name": "pc", - "bitsize": "64", - "offset": "0", - "encoding": "uint", - "format": "hex", - "set": "General Purpose Registers", - "gcc": "16", - "dwarf": "16", - "generic": "pc", + ("name", "pc"), + ("bitsize", "64"), + ("offset", "0"), + ("encoding", "uint"), + ("format", "hex"), + ("set", "General Purpose Registers"), + ("gcc", "16"), + ("dwarf", "16"), + ("generic", "pc"), ]) } else { responseKind = .string("E45") @@ -177,8 +201,23 @@ } responseKind = .hexEncodedBinary(buffer.readableBytesView) + case .resumeThreads: + // TODO: support multiple threads each with its own action here. + let threadActions = command.arguments.components(separatedBy: ":") + guard threadActions.count == 2, let threadActionString = threadActions.first else { + throw Error.multipleThreadsNotSupported + } + + guard let threadAction = ResumeThreadsAction(rawValue: threadActionString) else { + throw Error.unknownThreadAction(threadActionString) + } + + try self.debugger.step() + + responseKind = self.currentThreadStopInfo + case .generalRegisters: - fatalError() + throw Error.hostCommandNotImplemented(command.kind) } logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")]) diff --git a/Tests/WasmKitTests/DebuggerTests.swift b/Tests/WasmKitTests/DebuggerTests.swift index 267e2ea1..703b9105 100644 --- a/Tests/WasmKitTests/DebuggerTests.swift +++ b/Tests/WasmKitTests/DebuggerTests.swift @@ -20,7 +20,7 @@ @Suite struct DebuggerTests { @Test - func stopAtEntrypoint() throws { + func breakpoints() throws { let store = Store(engine: Engine()) let bytes = try wat2wasm(trivialModuleWAT) let module = try parseWasm(bytes: bytes) @@ -31,8 +31,15 @@ #expect(try debugger.run() == nil) - let expectedPc = try #require(debugger.breakpoints.keys.first) - #expect(debugger.currentCallStack == [expectedPc]) + let firstExpectedPc = try #require(debugger.breakpoints.keys.first) + #expect(debugger.currentCallStack == [firstExpectedPc]) + + try debugger.step() + #expect(try debugger.breakpoints.count == 1) + let secondExpectedPc = try #require(debugger.breakpoints.keys.first) + #expect(debugger.currentCallStack == [secondExpectedPc]) + + #expect(firstExpectedPc < secondExpectedPc) #expect(try debugger.run() == [.i32(42)]) } @@ -41,14 +48,14 @@ func binarySearch() throws { #expect([Int]().binarySearch(nextClosestTo: 42) == nil) - var result = try #require([1].binarySearch(nextClosestTo: 8)) - #expect(result == 1) + #expect([1].binarySearch(nextClosestTo: 1) == 1) + #expect([1].binarySearch(nextClosestTo: 8) == nil) - result = try #require([9, 15, 37].binarySearch(nextClosestTo: 28)) - #expect(result == 37) + #expect([9, 15, 37].binarySearch(nextClosestTo: 28) == 37) + #expect([9, 15, 37].binarySearch(nextClosestTo: 0) == 9) + #expect([9, 15, 37].binarySearch(nextClosestTo: 42) == nil) - result = try #require([9, 15, 37].binarySearch(nextClosestTo: 0)) - #expect(result == 9) + #expect([106, 110, 111].binarySearch(nextClosestTo: 107) == 110) } }