Skip to content

Commit 75c3b49

Browse files
authored
GDB RP: add support for reading Wasm locals (#226)
This change fully implements `qWasmLocal` GDB RP host command per the LLDB extensions specification updated in llvm/llvm-project#170393.
1 parent 6e024fc commit 75c3b49

File tree

10 files changed

+295
-53
lines changed

10 files changed

+295
-53
lines changed

.github/workflows/main.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,6 @@ jobs:
137137
wasi-swift-sdk-checksum: "b64dfad9e1c9ccdf06f35cf9b1a00317e000df0c0de0b3eb9f49d6db0fcba4d9"
138138
test-args: "--traits WasmDebuggingSupport --enable-code-coverage"
139139
build-dev-dashboard: true
140-
# Disabled until a toolchain containing https://github.com/swiftlang/swift/commit/b219d4089c922ceb8b700424236ca97f6087a9a1
141-
# is tagged.
142140
- swift: "swiftlang/swift:nightly-main-noble"
143141
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"
144142
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"

.swift-format

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
"lineLength": 200,
44
"indentation": {
55
"spaces": 4
6-
}
6+
},
7+
"ReturnVoidInsteadOfEmptyTuple": false
78
}

[email protected]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
139139
package.dependencies += [
140140
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.1"),
141141
.package(url: "https://github.com/apple/swift-system", from: "1.5.0"),
142-
.package(url: "https://github.com/apple/swift-nio", from: "2.86.2"),
142+
.package(url: "https://github.com/apple/swift-nio", from: "2.90.0"),
143143
.package(url: "https://github.com/apple/swift-log", from: "1.6.4"),
144144
]
145145
} else {

Sources/GDBRemoteProtocol/GDBHostCommand.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ package struct GDBHostCommand: Equatable {
4747
case kill
4848
case insertSoftwareBreakpoint
4949
case removeSoftwareBreakpoint
50+
case wasmLocal
51+
case memoryRegionInfo
5052

5153
case generalRegisters
5254

@@ -97,6 +99,10 @@ package struct GDBHostCommand: Equatable {
9799
self = .continue
98100
case "k":
99101
self = .kill
102+
case "qWasmLocal":
103+
self = .wasmLocal
104+
case "qMemoryRegionInfo":
105+
self = .memoryRegionInfo
100106

101107
default:
102108
return nil

Sources/WasmKit/Execution/Debugger.swift

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer<UInt64>)
2121
case noInstructionMappingAvailable(Int)
2222
case noReverseInstructionMappingAvailable(UnsafeMutablePointer<UInt64>)
23+
case stackFrameIndexOOB(UInt)
24+
case stackLocalIndexOOB(UInt)
25+
case notStoppedAtBreakpoint
26+
case linearMemoryNotInitialized
27+
case linearMemoryOOB(Range<Int>)
2328
}
2429

2530
private let valueStack: Sp
@@ -43,7 +48,11 @@
4348

4449
package private(set) var state: State
4550

46-
private var pc = Pc.allocate(capacity: 1)
51+
/// Pc of the final instruction that a successful program will execute, initialized with `Instruction.endofExecution`
52+
private var endOfExecution: CodeSlot
53+
54+
private var md: Md = nil
55+
private var ms: Ms = 0
4756

4857
/// Addresses of functions in the original Wasm binary, used for looking up functions when a breakpoint
4958
/// is enabled at an arbitrary address if it isn't present in ``InstructionMapping`` yet (i.e. the
@@ -76,9 +85,12 @@
7685
self.entrypointFunction = entrypointFunction
7786
self.valueStack = UnsafeMutablePointer<StackSlot>.allocate(capacity: limit)
7887
self.store = store
79-
self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit))
88+
self.execution = Execution(
89+
store: StoreRef(store),
90+
stackEnd: valueStack.advanced(by: limit)
91+
)
8092
self.threadingModel = store.engine.configuration.threadingModel
81-
self.pc.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel)
93+
self.endOfExecution = Instruction.endOfExecution.headSlot(threadingModel: threadingModel)
8294
self.state = .instantiated
8395
}
8496

@@ -91,7 +103,7 @@
91103
/// Finds a Wasm address for the first instruction in a given function.
92104
/// - Parameter function: the Wasm function to find the first Wasm instruction address for.
93105
/// - Returns: byte offset of the first Wasm instruction of given function in the module it was parsed from.
94-
private func originalAddress(function: Function) throws -> Int {
106+
package func originalAddress(function: Function) throws -> Int {
95107
precondition(function.handle.isWasm)
96108

97109
switch function.handle.wasm.code {
@@ -140,6 +152,14 @@
140152
return wasm
141153
}
142154

155+
package mutating func enableBreakpoint(
156+
module: Module,
157+
function: Int,
158+
offsetWithinFunction: Int = 0
159+
) throws -> Int {
160+
try self.enableBreakpoint(address: module.functions[function].code.originalAddress + offsetWithinFunction)
161+
}
162+
143163
/// Disables a breakpoint at a given Wasm address. If no breakpoint at a given address was previously set with
144164
/// `self.enableBreakpoint(address:), this function immediately returns.
145165
/// - Parameter address: byte offset of the Wasm instruction that was replaced with a breakpoint. The original
@@ -170,8 +190,6 @@
170190
let iseq = breakpoint.iseq
171191
var sp = iseq.sp
172192
var pc = iseq.pc
173-
var md: Md = nil
174-
var ms: Ms = 0
175193

176194
guard let currentFunction = sp.currentFunction else {
177195
throw Error.unknownCurrentFunctionForResumedBreakpoint(sp)
@@ -206,7 +224,7 @@
206224
type: self.entrypointFunction.type,
207225
arguments: [],
208226
sp: self.valueStack,
209-
pc: self.pc
227+
pc: &self.endOfExecution
210228
)
211229
self.state = .entrypointReturned(result)
212230

@@ -237,6 +255,66 @@
237255
try self.run()
238256
}
239257

258+
package func getLocal(frameIndex: UInt, localIndex: UInt) throws -> UInt64 {
259+
guard case .stoppedAtBreakpoint(let breakpoint) = self.state else {
260+
throw Error.notStoppedAtBreakpoint
261+
}
262+
263+
var i = 0
264+
for frame in Execution.CallStack(sp: breakpoint.iseq.sp) {
265+
guard frameIndex == i else {
266+
i += 1
267+
continue
268+
}
269+
270+
guard let currentFunction = frame.sp.currentFunction else {
271+
throw Debugger.Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp)
272+
}
273+
274+
try currentFunction.ensureCompiled(store: StoreRef(store))
275+
276+
guard case .debuggable(let wasm, _) = currentFunction.code else {
277+
fatalError()
278+
}
279+
280+
// Wasm function arguments are also addressed as locals.
281+
let functionType = store.engine.funcTypeInterner.resolve(currentFunction.type)
282+
283+
let localsCount = functionType.parameters.count + wasm.locals.count
284+
285+
guard localIndex < localsCount else {
286+
throw Debugger.Error.stackLocalIndexOOB(localIndex)
287+
}
288+
289+
if localIndex < functionType.parameters.count {
290+
let localIndex = Int(localIndex) - 4
291+
return frame.sp[localIndex].storage
292+
} else {
293+
let localIndex = Int(localIndex) - functionType.parameters.count
294+
return frame.sp[localIndex].storage
295+
}
296+
}
297+
298+
throw Error.stackFrameIndexOOB(frameIndex)
299+
}
300+
301+
package func readLinearMemory<T>(address: UInt, length: UInt, reader: (UnsafeRawBufferPointer) -> T) throws(Error) -> T {
302+
guard let md, ms > 0 else {
303+
throw Error.linearMemoryNotInitialized
304+
}
305+
306+
let upperBound = address + length
307+
let range = Int(address)..<Int(upperBound)
308+
309+
guard address + length < ms else {
310+
throw Error.linearMemoryOOB(range)
311+
}
312+
313+
let memory = UnsafeRawBufferPointer(start: md, count: ms)
314+
315+
return reader(UnsafeRawBufferPointer(rebasing: memory[range]))
316+
}
317+
240318
/// Array of addresses in the Wasm binary of executed instructions on the call stack.
241319
package var currentCallStack: [Int] {
242320
guard case .stoppedAtBreakpoint(let breakpoint) = self.state else {
@@ -254,7 +332,6 @@
254332

255333
deinit {
256334
self.valueStack.deallocate()
257-
self.pc.deallocate()
258335
}
259336
}
260337

Sources/WasmKit/Execution/Execution.swift

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import _CWasmKit
77
struct Execution: ~Copyable {
88
/// The reference to the ``Store`` associated with the execution.
99
let store: StoreRef
10+
1011
/// The end of the VM stack space.
11-
private var stackEnd: UnsafeMutablePointer<StackSlot>
12+
private let stackEnd: UnsafeMutablePointer<StackSlot>
1213
/// The error trap thrown during execution.
1314
/// This property must not be assigned to be non-nil more than once.
1415
/// - Note: If the trap is set, it must be released manually.
@@ -42,36 +43,44 @@ struct Execution: ~Copyable {
4243
sp.currentInstance.unsafelyUnwrapped
4344
}
4445

45-
/// An iterator for the call frames in the VM stack.
46-
struct FrameIterator: IteratorProtocol {
47-
struct Element {
48-
let pc: Pc
49-
let function: EntityHandle<WasmFunctionEntity>?
50-
}
46+
struct CallStack: Sequence {
47+
/// An iterator for the call frames in the VM stack.
48+
struct FrameIterator: IteratorProtocol {
49+
struct Element {
50+
let pc: Pc
51+
let sp: Sp
52+
}
5153

52-
/// The stack pointer currently traversed.
53-
private var sp: Sp?
54+
/// The stack pointer currently traversed.
55+
private var sp: Sp?
5456

55-
init(sp: Sp) {
56-
self.sp = sp
57-
}
57+
init(sp: Sp) {
58+
self.sp = sp
59+
}
5860

59-
mutating func next() -> Element? {
60-
guard let sp = self.sp, let pc = sp.returnPC else {
61-
// Reached the root frame, whose stack pointer is nil.
62-
return nil
61+
mutating func next() -> Element? {
62+
guard let sp = self.sp, let pc = sp.returnPC else {
63+
// Reached the root frame, whose stack pointer is nil.
64+
return nil
65+
}
66+
self.sp = sp.previousSP
67+
return Element(pc: pc, sp: sp)
6368
}
64-
self.sp = sp.previousSP
65-
return Element(pc: pc, function: sp.currentFunction)
69+
}
70+
71+
let sp: Sp
72+
73+
func makeIterator() -> FrameIterator {
74+
FrameIterator(sp: self.sp)
6675
}
6776
}
6877

6978
static func captureBacktrace(sp: Sp, store: Store) -> Backtrace {
70-
var frames = FrameIterator(sp: sp)
79+
let callStack = CallStack(sp: sp)
7180
var symbols: [Backtrace.Symbol] = []
7281

73-
while let frame = frames.next() {
74-
guard let function = frame.function else {
82+
for frame in callStack {
83+
guard let function = frame.sp.currentFunction else {
7584
symbols.append(.init(name: nil, address: frame.pc))
7685
continue
7786
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#if WasmDebuggingSupport
14+
15+
import NIOCore
16+
import WasmKit
17+
18+
package struct DebuggerMemoryView: ~Copyable {
19+
package static let executableCodeOffset = UInt64(0x4000_0000_0000_0000)
20+
21+
private let allocator: ByteBufferAllocator
22+
23+
/// WebAssembly binary loaded into memory for execution
24+
/// and for disassembly by the debugger.
25+
private let wasmBinary: ByteBuffer
26+
27+
package init(allocator: ByteBufferAllocator, wasmBinary: ByteBuffer) {
28+
self.allocator = allocator
29+
30+
self.wasmBinary = wasmBinary
31+
}
32+
33+
package func readMemory(
34+
debugger: borrowing Debugger,
35+
addressInProtocolSpace: UInt64,
36+
length: UInt
37+
) throws(Debugger.Error) -> ByteBufferView {
38+
if addressInProtocolSpace >= Self.executableCodeOffset {
39+
var length = Int(length)
40+
let codeAddress = Int(addressInProtocolSpace - Self.executableCodeOffset)
41+
if codeAddress + length > wasmBinary.readableBytes {
42+
length = wasmBinary.readableBytes - codeAddress
43+
}
44+
45+
return wasmBinary.readableBytesView[codeAddress..<(codeAddress + length)]
46+
} else {
47+
return try debugger.readLinearMemory(address: UInt(addressInProtocolSpace), length: length) {
48+
var buffer = self.allocator.buffer(capacity: $0.count)
49+
buffer.writeBytes($0)
50+
return buffer.readableBytesView
51+
}
52+
}
53+
}
54+
55+
}
56+
57+
#endif
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftNIO open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftNIO project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
extension Int {
16+
mutating func roundUpToAlignment<Type>(for: Type.Type) {
17+
// Alignment is always positive, we can use unchecked subtraction here.
18+
let alignmentGuide = MemoryLayout<Type>.alignment &- 1
19+
20+
// But we can't use unchecked addition.
21+
self = (self + alignmentGuide) & (~alignmentGuide)
22+
}
23+
}

0 commit comments

Comments
 (0)