Skip to content

Commit 7f5f88e

Browse files
authored
GDB RP: breakpoint commands, fix breakpoints across function calls (#225)
This allows setting breakpoints at arbitrary line or function in LLDB CLI interface. Also fixed multiple bugs: 1. Attempts to set breakpoints for any function other than the entrypoint failed as those were uncompiled functions when lazy compilation is used. 2. WasmKit used wrong order of call stack Pc addresses when sending `qWasmCallStack` response back via GDB RP.
1 parent 2e5eb3e commit 7f5f88e

File tree

6 files changed

+190
-57
lines changed

6 files changed

+190
-57
lines changed

Sources/CLI/DebuggerServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
// isn't taking down the entire server. In our case we need to be able to shut down the server on
8383
// debugger client's request, so let's wrap the discarding task group with a throwing task group
8484
// for cancellation.
85-
try await withThrowingTaskGroup { cancellableGroup in
85+
await withThrowingTaskGroup { cancellableGroup in
8686
// Use `AsyncStream` for sending a signal out of the discarding group.
8787
let (shutDownStream, shutDownContinuation) = AsyncStream<()>.makeStream()
8888

Sources/GDBRemoteProtocol/GDBHostCommand.swift

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ package struct GDBHostCommand: Equatable {
4545
case resumeThreads
4646
case `continue`
4747
case kill
48+
case insertSoftwareBreakpoint
49+
case removeSoftwareBreakpoint
4850

4951
case generalRegisters
5052

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

113+
/// Helper type for representing parsing prefixes in host commands.
114+
private struct ParsingRule {
115+
/// Kind of the host command parsed by this rul.
116+
let kind: Kind
117+
118+
/// String prefix required for the raw string to match for the rule
119+
/// to yield a parsed command.
120+
let prefix: String
121+
122+
/// Whether command arguments use a `:` delimiter, which usually otherwise
123+
/// separates command kind from arguments.
124+
var argumentsContainColonDelimiter = false
125+
}
126+
127+
private static let parsingRules: [ParsingRule] = [
128+
.init(
129+
kind: .readMemoryBinaryData,
130+
prefix: "x",
131+
),
132+
.init(
133+
kind: .readMemory,
134+
prefix: "m",
135+
),
136+
.init(
137+
kind: .insertSoftwareBreakpoint,
138+
prefix: "Z0",
139+
),
140+
.init(
141+
kind: .removeSoftwareBreakpoint,
142+
prefix: "z0",
143+
),
144+
.init(
145+
kind: .registerInfo,
146+
prefix: "qRegisterInfo",
147+
),
148+
.init(
149+
kind: .threadStopInfo,
150+
prefix: "qThreadStopInfo",
151+
),
152+
.init(
153+
kind: .resumeThreads,
154+
prefix: "vCont;",
155+
argumentsContainColonDelimiter: true
156+
),
157+
]
158+
111159
/// Initialize a host command from raw strings sent from a host.
112160
/// - Parameters:
113161
/// - kindString: raw ``String`` that denotes kind of the command.
114162
/// - arguments: raw arguments that immediately follow kind of the command.
115163
package init(kindString: String, arguments: String) throws(GDBHostCommandDecoder.Error) {
116-
let registerInfoPrefix = "qRegisterInfo"
117-
let threadStopInfoPrefix = "qThreadStopInfo"
118-
let resumeThreadsPrefix = "vCont"
164+
for rule in Self.parsingRules {
165+
if kindString.starts(with: rule.prefix) {
166+
self.kind = rule.kind
167+
let prependedArguments = kindString.dropFirst(rule.prefix.count)
119168

120-
if kindString.starts(with: "x") {
121-
self.kind = .readMemoryBinaryData
122-
self.arguments = String(kindString.dropFirst())
123-
return
124-
} else if kindString.starts(with: "m") {
125-
self.kind = .readMemory
126-
self.arguments = String(kindString.dropFirst())
127-
return
128-
} else if kindString.starts(with: registerInfoPrefix) {
129-
self.kind = .registerInfo
130-
131-
guard arguments.isEmpty else {
132-
throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue
133-
}
134-
self.arguments = String(kindString.dropFirst(registerInfoPrefix.count))
135-
return
136-
} else if kindString.starts(with: threadStopInfoPrefix) {
137-
self.kind = .threadStopInfo
138-
139-
guard arguments.isEmpty else {
140-
throw GDBHostCommandDecoder.Error.unexpectedArgumentsValue
169+
if rule.argumentsContainColonDelimiter {
170+
self.arguments = "\(prependedArguments):\(arguments)"
171+
} else {
172+
self.arguments = prependedArguments + arguments
173+
}
174+
return
141175
}
142-
self.arguments = String(kindString.dropFirst(registerInfoPrefix.count))
143-
return
144-
} else if kindString != "vCont?" && kindString.starts(with: resumeThreadsPrefix) {
145-
self.kind = .resumeThreads
176+
}
146177

147-
// Strip the prefix and a semicolon ';' delimiter, append arguments back with the original delimiter.
148-
self.arguments = String(kindString.dropFirst(resumeThreadsPrefix.count + 1)) + ":" + arguments
149-
return
150-
} else if let kind = Kind(rawValue: kindString) {
178+
if let kind = Kind(rawValue: kindString) {
151179
self.kind = kind
152180
} else {
153181
throw GDBHostCommandDecoder.Error.unknownCommand(kind: kindString, arguments: arguments)

Sources/WasmKit/Execution/Debugger.swift

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
case instantiated
1313
case stoppedAtBreakpoint(BreakpointState)
1414
case trapped(String)
15-
case wasiModuleExited(exitCode: UInt32)
1615
case entrypointReturned([Value])
1716
}
1817

@@ -46,6 +45,11 @@
4645

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

48+
/// Addresses of functions in the original Wasm binary, used for looking up functions when a breakpoint
49+
/// is enabled at an arbitrary address if it isn't present in ``InstructionMapping`` yet (i.e. the
50+
/// was not compiled yet in lazy compilation mode).
51+
private let functionAddresses: [(address: Int, instanceFunctionIndex: Int)]
52+
4953
/// Initializes a new debugger state instance.
5054
/// - Parameters:
5155
/// - module: Wasm module to instantiate.
@@ -60,6 +64,14 @@
6064
}
6165

6266
self.instance = instance
67+
self.functionAddresses = instance.handle.functions.enumerated().filter { $0.element.isWasm }.lazy.map {
68+
switch $0.element.wasm.code {
69+
case .uncompiled(let wasm), .debuggable(let wasm, _):
70+
return (address: wasm.originalAddress, instanceFunctionIndex: $0.offset)
71+
case .compiled:
72+
fatalError()
73+
}
74+
}
6375
self.module = module
6476
self.entrypointFunction = entrypointFunction
6577
self.valueStack = UnsafeMutablePointer<StackSlot>.allocate(capacity: limit)
@@ -93,38 +105,52 @@
93105
}
94106
}
95107

108+
private func findIseq(forWasmAddress address: Int) throws -> (iseq: Pc, wasm: Int) {
109+
if let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) {
110+
return (iseq, wasm)
111+
}
112+
113+
let followingIndex = self.functionAddresses.firstIndex(where: { $0.address > address }) ?? self.functionAddresses.endIndex
114+
let functionIndex = self.functionAddresses[followingIndex - 1].instanceFunctionIndex
115+
let function = instance.handle.functions[functionIndex]
116+
try function.wasm.ensureCompiled(store: StoreRef(self.store))
117+
118+
if let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) {
119+
return (iseq, wasm)
120+
}
121+
122+
throw Error.noInstructionMappingAvailable(address)
123+
}
124+
96125
/// Enables a breakpoint at a given Wasm address.
97126
/// - Parameter address: byte offset of the Wasm instruction that will be replaced with a breakpoint. If no
98127
/// direct internal bytecode matching instruction is found, the next closest internal bytecode instruction
99128
/// is replaced with a breakpoint. The original instruction to be restored is preserved in debugger state
100129
/// represented by `self`.
101130
/// See also ``Debugger/disableBreakpoint(address:)``.
102-
package mutating func enableBreakpoint(address: Int) throws(Error) {
131+
@discardableResult
132+
package mutating func enableBreakpoint(address: Int) throws -> Int {
103133
guard self.breakpoints[address] == nil else {
104-
return
105-
}
106-
107-
guard let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else {
108-
throw Error.noInstructionMappingAvailable(address)
134+
return address
109135
}
110136

137+
let (iseq, wasm) = try self.findIseq(forWasmAddress: address)
111138
self.breakpoints[wasm] = iseq.pointee
112139
iseq.pointee = Instruction.breakpoint.headSlot(threadingModel: self.threadingModel)
140+
return wasm
113141
}
114142

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

125-
guard let (iseq, wasm) = self.instance.handle.instructionMapping.findIseq(forWasmAddress: address) else {
126-
throw Error.noInstructionMappingAvailable(address)
127-
}
153+
let (iseq, wasm) = try self.findIseq(forWasmAddress: address)
128154

129155
self.breakpoints[wasm] = nil
130156
iseq.pointee = oldCodeSlot
@@ -135,7 +161,8 @@
135161
/// executed. If the module is not stopped at a breakpoint, this function returns immediately.
136162
package mutating func run() throws {
137163
do {
138-
if case .stoppedAtBreakpoint(let breakpoint) = self.state {
164+
switch self.state {
165+
case .stoppedAtBreakpoint(let breakpoint):
139166
// Remove the breakpoint before resuming
140167
try self.disableBreakpoint(address: breakpoint.wasmPc)
141168
self.execution.resetError()
@@ -169,9 +196,10 @@
169196
self.state = .entrypointReturned(
170197
type.results.enumerated().map { (i, type) in
171198
sp[VReg(i)].cast(to: type)
172-
})
199+
}
200+
)
173201
}
174-
} else {
202+
case .instantiated:
175203
let result = try self.execution.executeWasm(
176204
threadingModel: self.threadingModel,
177205
function: self.entrypointFunction.handle,
@@ -181,6 +209,9 @@
181209
pc: self.pc
182210
)
183211
self.state = .entrypointReturned(result)
212+
213+
case .trapped, .entrypointReturned:
214+
fatalError("Restarting a Wasm module from the debugger is not implemented yet.")
184215
}
185216
} catch let breakpoint as Execution.Breakpoint {
186217
let pc = breakpoint.pc
@@ -212,10 +243,11 @@
212243
return []
213244
}
214245

215-
var result = Execution.captureBacktrace(sp: breakpoint.iseq.sp, store: self.store).symbols.compactMap {
216-
return self.instance.handle.instructionMapping.findWasm(forIseqAddress: $0.address)
217-
}
218-
result.append(breakpoint.wasmPc)
246+
var result = [breakpoint.wasmPc]
247+
result.append(
248+
contentsOf: Execution.captureBacktrace(sp: breakpoint.iseq.sp, store: self.store).symbols.compactMap {
249+
return self.instance.handle.instructionMapping.findWasm(forIseqAddress: $0.address)
250+
})
219251

220252
return result
221253
}

Sources/WasmKit/Execution/Function.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,12 @@ extension InternalFunction {
216216
function: EntityHandle<WasmFunctionEntity>
217217
) {
218218
let entity = self.wasm
219-
guard case .compiled(let iseq) = entity.code else {
219+
switch entity.code {
220+
case .compiled(let iseq), .debuggable(_, let iseq):
221+
return (iseq, entity.numberOfNonParameterLocals, entity)
222+
case .uncompiled:
220223
preconditionFailure()
221224
}
222-
return (iseq, entity.numberOfNonParameterLocals, entity)
223225
}
224226
}
225227

Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
case hostCommandNotImplemented(GDBHostCommand.Kind)
5050
case exitCodeUnknown([Value])
5151
case killRequestReceived
52+
case unknownHexEncodedArguments(String)
5253
}
5354

5455
private let wasmBinary: ByteBuffer
@@ -91,6 +92,20 @@
9192
return buffer.hexDump(format: .compact)
9293
}
9394

95+
private func firstHexArgument<I: FixedWidthInteger>(argumentsString: String, separator: Character, endianness: Endianness) throws -> I {
96+
guard let hexString = argumentsString.split(separator: separator).first else {
97+
throw Error.unknownHexEncodedArguments(argumentsString)
98+
}
99+
100+
var hexBuffer = try self.allocator.buffer(plainHexEncodedBytes: String(hexString))
101+
102+
guard let argument = hexBuffer.readInteger(endianness: endianness, as: I.self) else {
103+
throw Error.unknownHexEncodedArguments(argumentsString)
104+
}
105+
106+
return argument
107+
}
108+
94109
var currentThreadStopInfo: GDBTargetResponse.Kind {
95110
get throws {
96111
var result: [(String, String)] = [
@@ -106,9 +121,6 @@
106121
result.append(("reason", "trace"))
107122
return .keyValuePairs(result)
108123

109-
case .wasiModuleExited(let exitCode):
110-
return .string("W\(self.hexDump(exitCode, endianness: .big))")
111-
112124
case .entrypointReturned(let values):
113125
guard !values.isEmpty else {
114126
return .string("W\(self.hexDump(0 as UInt8, endianness: .big))")
@@ -265,6 +277,28 @@
265277
case .kill:
266278
throw Error.killRequestReceived
267279

280+
case .insertSoftwareBreakpoint:
281+
try self.debugger.enableBreakpoint(
282+
address: Int(
283+
self.firstHexArgument(
284+
argumentsString: command.arguments,
285+
separator: ",",
286+
endianness: .big
287+
) - codeOffset)
288+
)
289+
responseKind = .ok
290+
291+
case .removeSoftwareBreakpoint:
292+
try self.debugger.disableBreakpoint(
293+
address: Int(
294+
self.firstHexArgument(
295+
argumentsString: command.arguments,
296+
separator: ",",
297+
endianness: .big
298+
) - codeOffset)
299+
)
300+
responseKind = .ok
301+
268302
case .generalRegisters:
269303
throw Error.hostCommandNotImplemented(command.kind)
270304
}

0 commit comments

Comments
 (0)