Skip to content

Commit 796f59f

Browse files
authored
GDB RP: handle instruction stepping debugger host command (#215)
1 parent fff472d commit 796f59f

File tree

6 files changed

+125
-38
lines changed

6 files changed

+125
-38
lines changed

Sources/GDBRemoteProtocol/GDBHostCommand.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ package struct GDBHostCommand: Equatable {
3939
case readMemory
4040
case wasmCallStack
4141
case threadStopInfo
42+
case symbolLookup
43+
case jsonThreadsInfo
44+
case jsonThreadExtendedInfo
45+
case resumeThreads
4246

4347
case generalRegisters
4448

@@ -79,6 +83,12 @@ package struct GDBHostCommand: Equatable {
7983
self = .transfer
8084
case "qWasmCallStack":
8185
self = .wasmCallStack
86+
case "qSymbol":
87+
self = .symbolLookup
88+
case "jThreadsInfo":
89+
self = .jsonThreadsInfo
90+
case "jThreadExtendedInfo":
91+
self = .jsonThreadExtendedInfo
8292

8393
default:
8494
return nil
@@ -99,6 +109,7 @@ package struct GDBHostCommand: Equatable {
99109
package init(kindString: String, arguments: String) throws(GDBHostCommandDecoder.Error) {
100110
let registerInfoPrefix = "qRegisterInfo"
101111
let threadStopInfoPrefix = "qThreadStopInfo"
112+
let resumeThreadsPrefix = "vCont"
102113

103114
if kindString.starts(with: "x") {
104115
self.kind = .readMemoryBinaryData
@@ -124,6 +135,12 @@ package struct GDBHostCommand: Equatable {
124135
}
125136
self.arguments = String(kindString.dropFirst(registerInfoPrefix.count))
126137
return
138+
} else if kindString != "vCont?" && kindString.starts(with: resumeThreadsPrefix) {
139+
self.kind = .resumeThreads
140+
141+
// Strip the prefix and a semicolon ';' delimiter, append arguments back with the original delimiter.
142+
self.arguments = String(kindString.dropFirst(resumeThreadsPrefix.count + 1)) + ":" + arguments
143+
return
127144
} else if let kind = Kind(rawValue: kindString) {
128145
self.kind = kind
129146
} else {

Sources/GDBRemoteProtocol/GDBTargetResponse.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ package struct GDBTargetResponse {
3434

3535
/// A list of key-value pairs, with keys delimited from values by a colon `:`
3636
/// character, and pairs in the list delimited by the semicolon `;` character.
37-
case keyValuePairs(KeyValuePairs<String, String>)
37+
case keyValuePairs([(String, String)])
3838

3939
/// List of ``VContActions`` values delimited by the semicolon `;` character.
4040
case vContSupportedActions([VContActions])

Sources/WasmKit/Execution/Debugger.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
try self.execution.runTokenThreaded(sp: &sp, pc: &pc, md: &md, ms: &ms)
152152
}
153153
} catch is Execution.EndOfExecution {
154+
// The module successfully executed till the "end of execution" instruction.
154155
}
155156

156157
let type = self.store.engine.funcTypeInterner.resolve(currentFunction.type)
@@ -178,6 +179,21 @@
178179
}
179180
}
180181

182+
/// Steps by a single Wasm instruction in the module instantiated by the debugger stopped at a breakpoint.
183+
/// The current breakpoint is disabled and new breakpoints are put on the next instruction (or instructions in case
184+
/// of multiple possible execution branches). After breakpoints setup, execution is resumed until suspension.
185+
/// If the module is not stopped at a breakpoint, this function returns immediately.
186+
package mutating func step() throws {
187+
guard let currentBreakpoint else {
188+
return
189+
}
190+
191+
// TODO: analyze actual instruction branching to set the breakpoint correctly.
192+
try self.enableBreakpoint(address: currentBreakpoint.wasmPc + 1)
193+
let result = try self.run()
194+
assert(result == nil)
195+
}
196+
181197
/// Array of addresses in the Wasm binary of executed instructions on the call stack.
182198
package var currentCallStack: [Int] {
183199
guard let currentBreakpoint else {

Sources/WasmKit/Execution/DebuggerInstructionMapping.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ struct DebuggerInstructionMapping {
2323
if self.wasmToIseq[wasm] == nil {
2424
self.wasmToIseq[wasm] = iseq
2525
}
26-
self.wasmMappings.append(wasm)
26+
if self.wasmMappings.last != wasm {
27+
self.wasmMappings.append(wasm)
28+
}
2729
}
2830

2931
/// Computes an address of WasmKit's iseq bytecode instruction that matches a given Wasm instruction address.
@@ -65,14 +67,20 @@ struct DebuggerInstructionMapping {
6567
return nil
6668
default:
6769
var slice = self[0..<self.count]
68-
while slice.count > 1 {
70+
while let last = slice.last {
71+
guard last >= value else { return nil }
72+
6973
let middle = (slice.endIndex - slice.startIndex) / 2
74+
guard middle > 0 else { break }
75+
7076
if slice[middle] < value {
7177
// Not found anything in the lower half, assigning higher half to `slice`.
7278
slice = slice[(middle + 1)..<slice.endIndex]
73-
} else {
79+
} else if slice[middle] > value && slice[middle - 1] > value {
7480
// Not found anything in the higher half, assigning lower half to `slice`.
7581
slice = slice[slice.startIndex..<middle]
82+
} else {
83+
return self[middle]
7684
}
7785
}
7886

Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,17 @@
3535
private let codeOffset = UInt64(0x4000_0000_0000_0000)
3636

3737
package actor WasmKitGDBHandler {
38+
enum ResumeThreadsAction: String {
39+
case step = "s"
40+
}
41+
3842
enum Error: Swift.Error {
3943
case unknownTransferArguments
4044
case unknownReadMemoryArguments
4145
case stoppingAtEntrypointFailed
46+
case multipleThreadsNotSupported
47+
case unknownThreadAction(String)
48+
case hostCommandNotImplemented(GDBHostCommand.Kind)
4249
}
4350

4451
private let wasmBinary: ByteBuffer
@@ -69,6 +76,25 @@
6976
}
7077
}
7178

79+
var currentThreadStopInfo: GDBTargetResponse.Kind {
80+
var result: [(String, String)] = [
81+
("T05thread", "1"),
82+
("reason", "trace"),
83+
("threads", "1"),
84+
]
85+
if let pc = self.debugger.currentCallStack.first {
86+
let pcInHostAddressSpace = UInt64(pc) + codeOffset
87+
var beBuffer = self.allocator.buffer(capacity: 8)
88+
beBuffer.writeInteger(pcInHostAddressSpace, endianness: .big)
89+
result.append(("thread-pcs", beBuffer.hexDump(format: .compact)))
90+
var leBuffer = self.allocator.buffer(capacity: 8)
91+
leBuffer.writeInteger(pcInHostAddressSpace, endianness: .little)
92+
result.append(("00", leBuffer.hexDump(format: .compact)))
93+
}
94+
95+
return .keyValuePairs(result)
96+
}
97+
7298
package func handle(command: GDBHostCommand) throws -> GDBTargetResponse {
7399
let responseKind: GDBTargetResponse.Kind
74100
logger.trace("handling GDB host command", metadata: ["GDBHostCommand": .string(command.kind.rawValue)])
@@ -84,11 +110,11 @@
84110

85111
case .hostInfo:
86112
responseKind = .keyValuePairs([
87-
"arch": "wasm32",
88-
"ptrsize": "4",
89-
"endian": "little",
90-
"ostype": "wasip1",
91-
"vendor": "WasmKit",
113+
("arch", "wasm32"),
114+
("ptrsize", "4"),
115+
("endian", "little"),
116+
("ostype", "wasip1"),
117+
("vendor", "WasmKit"),
92118
])
93119

94120
case .supportedFeatures:
@@ -97,16 +123,17 @@
97123
case .vContSupportedActions:
98124
responseKind = .vContSupportedActions([.continue, .step])
99125

100-
case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData:
126+
case .isVAttachOrWaitSupported, .enableErrorStrings, .structuredDataPlugins, .readMemoryBinaryData,
127+
.symbolLookup, .jsonThreadsInfo, .jsonThreadExtendedInfo:
101128
responseKind = .empty
102129

103130
case .processInfo:
104131
responseKind = .keyValuePairs([
105-
"pid": "1",
106-
"parent-pid": "1",
107-
"arch": "wasm32",
108-
"endian": "little",
109-
"ptrsize": "4",
132+
("pid", "1"),
133+
("parent-pid", "1"),
134+
("arch", "wasm32"),
135+
("endian", "little"),
136+
("ptrsize", "4"),
110137
])
111138

112139
case .currentThreadID:
@@ -119,23 +146,20 @@
119146
responseKind = .string("l")
120147

121148
case .targetStatus, .threadStopInfo:
122-
responseKind = .keyValuePairs([
123-
"T05thread": "1",
124-
"reason": "trace",
125-
])
149+
responseKind = self.currentThreadStopInfo
126150

127151
case .registerInfo:
128152
if command.arguments == "0" {
129153
responseKind = .keyValuePairs([
130-
"name": "pc",
131-
"bitsize": "64",
132-
"offset": "0",
133-
"encoding": "uint",
134-
"format": "hex",
135-
"set": "General Purpose Registers",
136-
"gcc": "16",
137-
"dwarf": "16",
138-
"generic": "pc",
154+
("name", "pc"),
155+
("bitsize", "64"),
156+
("offset", "0"),
157+
("encoding", "uint"),
158+
("format", "hex"),
159+
("set", "General Purpose Registers"),
160+
("gcc", "16"),
161+
("dwarf", "16"),
162+
("generic", "pc"),
139163
])
140164
} else {
141165
responseKind = .string("E45")
@@ -177,8 +201,23 @@
177201
}
178202
responseKind = .hexEncodedBinary(buffer.readableBytesView)
179203

204+
case .resumeThreads:
205+
// TODO: support multiple threads each with its own action here.
206+
let threadActions = command.arguments.components(separatedBy: ":")
207+
guard threadActions.count == 2, let threadActionString = threadActions.first else {
208+
throw Error.multipleThreadsNotSupported
209+
}
210+
211+
guard let threadAction = ResumeThreadsAction(rawValue: threadActionString) else {
212+
throw Error.unknownThreadAction(threadActionString)
213+
}
214+
215+
try self.debugger.step()
216+
217+
responseKind = self.currentThreadStopInfo
218+
180219
case .generalRegisters:
181-
fatalError()
220+
throw Error.hostCommandNotImplemented(command.kind)
182221
}
183222

184223
logger.trace("handler produced a response", metadata: ["GDBTargetResponse": .string("\(responseKind)")])

Tests/WasmKitTests/DebuggerTests.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
@Suite
2121
struct DebuggerTests {
2222
@Test
23-
func stopAtEntrypoint() throws {
23+
func breakpoints() throws {
2424
let store = Store(engine: Engine())
2525
let bytes = try wat2wasm(trivialModuleWAT)
2626
let module = try parseWasm(bytes: bytes)
@@ -31,8 +31,15 @@
3131

3232
#expect(try debugger.run() == nil)
3333

34-
let expectedPc = try #require(debugger.breakpoints.keys.first)
35-
#expect(debugger.currentCallStack == [expectedPc])
34+
let firstExpectedPc = try #require(debugger.breakpoints.keys.first)
35+
#expect(debugger.currentCallStack == [firstExpectedPc])
36+
37+
try debugger.step()
38+
#expect(try debugger.breakpoints.count == 1)
39+
let secondExpectedPc = try #require(debugger.breakpoints.keys.first)
40+
#expect(debugger.currentCallStack == [secondExpectedPc])
41+
42+
#expect(firstExpectedPc < secondExpectedPc)
3643

3744
#expect(try debugger.run() == [.i32(42)])
3845
}
@@ -41,14 +48,14 @@
4148
func binarySearch() throws {
4249
#expect([Int]().binarySearch(nextClosestTo: 42) == nil)
4350

44-
var result = try #require([1].binarySearch(nextClosestTo: 8))
45-
#expect(result == 1)
51+
#expect([1].binarySearch(nextClosestTo: 1) == 1)
52+
#expect([1].binarySearch(nextClosestTo: 8) == nil)
4653

47-
result = try #require([9, 15, 37].binarySearch(nextClosestTo: 28))
48-
#expect(result == 37)
54+
#expect([9, 15, 37].binarySearch(nextClosestTo: 28) == 37)
55+
#expect([9, 15, 37].binarySearch(nextClosestTo: 0) == 9)
56+
#expect([9, 15, 37].binarySearch(nextClosestTo: 42) == nil)
4957

50-
result = try #require([9, 15, 37].binarySearch(nextClosestTo: 0))
51-
#expect(result == 9)
58+
#expect([106, 110, 111].binarySearch(nextClosestTo: 107) == 110)
5259
}
5360
}
5461

0 commit comments

Comments
 (0)