Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
136c7a9
Add `GDBHostCommand.ParsingRule` type for better parsing
MaxDesiatov Nov 1, 2025
20a2101
Enable breakpoint commands
MaxDesiatov Nov 3, 2025
10d1c0b
format
MaxDesiatov Nov 3, 2025
bd79763
Fix multiple bugs in breakpoint handling
MaxDesiatov Nov 3, 2025
ad8d117
Test coverage WIP
MaxDesiatov Nov 3, 2025
9215fe0
Add tests for debugging across function calls
MaxDesiatov Nov 4, 2025
909178f
Fix formatting
MaxDesiatov Nov 4, 2025
55b10e9
Remove dependency on WASI from `WasmKit.Debugger`
MaxDesiatov Nov 4, 2025
1820a66
Make `ParsingRules` private, add doc comments
MaxDesiatov Nov 4, 2025
46a8196
Add scaffolding for reading Wasm locals
MaxDesiatov Nov 5, 2025
d731564
Fix formatting
MaxDesiatov Nov 5, 2025
6583f1d
Merge branch 'main' into maxd/wasm-local
MaxDesiatov Nov 5, 2025
bc1eb32
Add `CallStack: Sequence`, `Debugger.LocalAddress` types
MaxDesiatov Nov 11, 2025
3a2eb99
Fix formatting
MaxDesiatov Nov 11, 2025
85b2248
Fix build errors
MaxDesiatov Nov 11, 2025
185328e
Expose interpreter stack via GDB RP
MaxDesiatov Nov 11, 2025
132dd94
Add scaffolding for debugging packed stack frames
MaxDesiatov Nov 13, 2025
7be106a
Get `maxStackHeight` in `packedStackFrame`
MaxDesiatov Nov 13, 2025
6d2207c
Move packed stack frame code to separate `DebuggerStackFrame` type
MaxDesiatov Nov 13, 2025
1bb32b5
Fix formatting
MaxDesiatov Nov 13, 2025
6f2cb82
Write out the stack frame, propagate layout back to the debugger
MaxDesiatov Nov 14, 2025
3fecaf9
Move debugger code from Swift 6.1 to 6.2 for `Span` availability
MaxDesiatov Nov 14, 2025
894b716
Create separate `DebuggerMemoryCache` type
MaxDesiatov Nov 18, 2025
6b0cb81
Apply formatter
MaxDesiatov Nov 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Examples/wasm/ctest.wasm
Binary file not shown.
6 changes: 3 additions & 3 deletions Package@swift-6.1.swift → Package@swift-6.2.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:6.1
// swift-tools-version:6.2

import PackageDescription

Expand Down Expand Up @@ -32,7 +32,7 @@ let package = Package(
.library(name: "_CabiShims", targets: ["_CabiShims"]),
],
traits: [
.default(enabledTraits: []),
.default(enabledTraits: ["WasmDebuggingSupport"]),
"WasmDebuggingSupport",
],
targets: [
Expand Down Expand Up @@ -139,7 +139,7 @@ if ProcessInfo.processInfo.environment["SWIFTCI_USE_LOCAL_DEPS"] == nil {
package.dependencies += [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.1"),
.package(url: "https://github.com/apple/swift-system", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-nio", from: "2.86.2"),
.package(url: "https://github.com/apple/swift-nio", from: "2.90.0"),
.package(url: "https://github.com/apple/swift-log", from: "1.6.4"),
]
} else {
Expand Down
6 changes: 6 additions & 0 deletions Sources/GDBRemoteProtocol/GDBHostCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ package struct GDBHostCommand: Equatable {
case kill
case insertSoftwareBreakpoint
case removeSoftwareBreakpoint
case wasmLocal
case memoryRegionInfo

case generalRegisters

Expand Down Expand Up @@ -97,6 +99,10 @@ package struct GDBHostCommand: Equatable {
self = .continue
case "k":
self = .kill
case "qWasmLocal":
self = .wasmLocal
case "qMemoryRegionInfo":
self = .memoryRegionInfo

default:
return nil
Expand Down
36 changes: 29 additions & 7 deletions Sources/WasmKit/Execution/Debugger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
case unknownCurrentFunctionForResumedBreakpoint(UnsafeMutablePointer<UInt64>)
case noInstructionMappingAvailable(Int)
case noReverseInstructionMappingAvailable(UnsafeMutablePointer<UInt64>)
case stackFrameIndexOOB(Int)
case stackLocalIndexOOB(UInt32)
case notStoppedAtBreakpoint
}

private let valueStack: Sp
Expand All @@ -43,13 +46,16 @@

package private(set) var state: State

private var pc = Pc.allocate(capacity: 1)
/// Pc ofthe final instruction that a successful program will execute, initialized with `Instruction.endofExecution`
private let endOfExecution = Pc.allocate(capacity: 1)

/// Addresses of functions in the original Wasm binary, used for looking up functions when a breakpoint
/// Addresses of functions in the original Wasm binary, used looking up functions when a breakpoint
/// is enabled at an arbitrary address if it isn't present in ``InstructionMapping`` yet (i.e. the
/// was not compiled yet in lazy compilation mode).
private let functionAddresses: [(address: Int, instanceFunctionIndex: Int)]

private var stackFrame = DebuggerStackFrame()

/// Initializes a new debugger state instance.
/// - Parameters:
/// - module: Wasm module to instantiate.
Expand All @@ -76,9 +82,13 @@
self.entrypointFunction = entrypointFunction
self.valueStack = UnsafeMutablePointer<StackSlot>.allocate(capacity: limit)
self.store = store
self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit))
self.execution = Execution(
store: StoreRef(store),
stackStart: .init(start: valueStack, count: limit),
stackEnd: valueStack.advanced(by: limit)
)
self.threadingModel = store.engine.configuration.threadingModel
self.pc.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel)
self.endOfExecution.pointee = Instruction.endOfExecution.headSlot(threadingModel: threadingModel)
self.state = .instantiated
}

Expand All @@ -91,7 +101,7 @@
/// 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 {
package func originalAddress(function: Function) throws -> Int {
precondition(function.handle.isWasm)

switch function.handle.wasm.code {
Expand Down Expand Up @@ -206,7 +216,7 @@
type: self.entrypointFunction.type,
arguments: [],
sp: self.valueStack,
pc: self.pc
pc: self.endOfExecution
)
self.state = .entrypointReturned(result)

Expand Down Expand Up @@ -237,6 +247,14 @@
try self.run()
}

package mutating func packedStackFrame<T>(frameIndex: Int, reader: (RawSpan, DebuggerStackFrame.Layout) -> T) throws -> T {
guard case .stoppedAtBreakpoint(let breakpoint) = self.state else {
throw Error.notStoppedAtBreakpoint
}

return try self.stackFrame.withFrames(sp: breakpoint.iseq.sp, frameIndex: frameIndex, store: self.store, reader: reader)
}

/// Array of addresses in the Wasm binary of executed instructions on the call stack.
package var currentCallStack: [Int] {
guard case .stoppedAtBreakpoint(let breakpoint) = self.state else {
Expand All @@ -252,9 +270,13 @@
return result
}

package var stackMemory: UnsafeRawBufferPointer {
UnsafeRawBufferPointer(self.execution.stackStart)
}

deinit {
self.valueStack.deallocate()
self.pc.deallocate()
self.endOfExecution.deallocate()
}
}

Expand Down
97 changes: 97 additions & 0 deletions Sources/WasmKit/Execution/DebuggerStackFrame.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#if WasmDebuggingSupport

package struct DebuggerStackFrame: ~Copyable {
private var buffer = UnsafeMutableRawBufferPointer.allocate(byteCount: 1, alignment: 8)

package struct Layout {
package fileprivate(set) var localOffsets = [Int]()
}

init() {
buffer.initializeMemory(as: Int.self, repeating: 0)
}

mutating func withFrames<T>(sp: Sp, frameIndex: Int, store: Store, reader: (borrowing RawSpan, Layout) -> T) throws -> T {
self.buffer.initializeMemory(as: Int.self, repeating: 0)

var i = 0
for frame in Execution.CallStack(sp: sp) {
guard frameIndex == i else {
i += 1
continue
}

guard let currentFunction = frame.sp.currentFunction else {
throw Debugger.Error.unknownCurrentFunctionForResumedBreakpoint(frame.sp)
}

try currentFunction.ensureCompiled(store: StoreRef(store))

guard case .debuggable(let wasm, let iseq) = currentFunction.code else {
fatalError()
}

// Wasm function arguments are also addressed as locals.
let functionType = store.engine.funcTypeInterner.resolve(currentFunction.type)

let stackSlotByteCount = MemoryLayout<StackSlot>.size

let pessimisticByteCount = functionType.parameters.count * stackSlotByteCount + wasm.locals.count * stackSlotByteCount + iseq.maxStackHeight * stackSlotByteCount

if pessimisticByteCount > self.buffer.count {
let newBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: pessimisticByteCount, alignment: MemoryLayout<StackSlot>.alignment)
newBuffer.copyBytes(from: self.buffer)
self.buffer.deallocate()
self.buffer = newBuffer
}

var span = OutputRawSpan(buffer: self.buffer, initializedCount: 0)
var layout = Layout()

for (i, type) in functionType.parameters.enumerated() {
// See ``FrameHeaderLayout`` documentation for offset calculation details.
type.append(to: &span, frame, offset: i - max(functionType.parameters.count, functionType.results.count))
layout.localOffsets.append(span.byteCount)
}

for (i, type) in wasm.locals.enumerated() {
type.append(to: &span, frame, offset: i)
layout.localOffsets.append(span.byteCount)
}

// FIXME: copy over actual stack values
span.append(repeating: 0, count: iseq.maxStackHeight, as: UInt64.self)

_ = span.finalize(for: self.buffer)

return reader(self.buffer.bytes, layout)
}

throw Debugger.Error.stackFrameIndexOOB(frameIndex)
}

deinit {
buffer.deallocate()
}
}

extension ValueType {
fileprivate func append(
to span: inout OutputRawSpan,
_ frame: Execution.CallStack.FrameIterator.Element,
offset: Int
) {
switch self {
case .i32, .f32:
span.append(frame.sp[i32: offset], as: UInt32.self)
case .i64, .f64:
span.append(frame.sp[i64: offset], as: UInt64.self)
case .v128:
fatalError("SIMD is not yet supported in the Wasm debugger")
case .ref:
fatalError("References are not yet supported in the wasm debugger")
}
}
}

#endif
59 changes: 36 additions & 23 deletions Sources/WasmKit/Execution/Execution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import _CWasmKit
struct Execution: ~Copyable {
/// The reference to the ``Store`` associated with the execution.
let store: StoreRef

/// The start of the VM stack space, used for debugging purposes.
let stackStart: UnsafeBufferPointer<StackSlot>

/// The end of the VM stack space.
private var stackEnd: UnsafeMutablePointer<StackSlot>
private let stackEnd: UnsafeMutablePointer<StackSlot>
/// The error trap thrown during execution.
/// This property must not be assigned to be non-nil more than once.
/// - Note: If the trap is set, it must be released manually.
private var trap: (error: UnsafeRawPointer, sp: Sp)? = nil

#if WasmDebuggingSupport
package init(store: StoreRef, stackEnd: UnsafeMutablePointer<StackSlot>) {
package init(store: StoreRef, stackStart: UnsafeBufferPointer<StackSlot>, stackEnd: UnsafeMutablePointer<StackSlot>) {
self.store = store
self.stackStart = stackStart
self.stackEnd = stackEnd
}
#endif
Expand All @@ -32,7 +37,7 @@ struct Execution: ~Copyable {
defer {
valueStack.deallocate()
}
var context = Execution(store: store, stackEnd: valueStack.advanced(by: limit))
var context = Execution(store: store, stackStart: .init(start: valueStack, count: limit), stackEnd: valueStack.advanced(by: limit))
return try body(&context, valueStack)
}

Expand All @@ -42,36 +47,44 @@ struct Execution: ~Copyable {
sp.currentInstance.unsafelyUnwrapped
}

/// An iterator for the call frames in the VM stack.
struct FrameIterator: IteratorProtocol {
struct Element {
let pc: Pc
let function: EntityHandle<WasmFunctionEntity>?
}
struct CallStack: Sequence {
/// An iterator for the call frames in the VM stack.
struct FrameIterator: IteratorProtocol {
struct Element {
let pc: Pc
let sp: Sp
}

/// The stack pointer currently traversed.
private var sp: Sp?
/// The stack pointer currently traversed.
private var sp: Sp?

init(sp: Sp) {
self.sp = sp
}
init(sp: Sp) {
self.sp = sp
}

mutating func next() -> Element? {
guard let sp = self.sp, let pc = sp.returnPC else {
// Reached the root frame, whose stack pointer is nil.
return nil
mutating func next() -> Element? {
guard let sp = self.sp, let pc = sp.returnPC else {
// Reached the root frame, whose stack pointer is nil.
return nil
}
self.sp = sp.previousSP
return Element(pc: pc, sp: sp)
}
self.sp = sp.previousSP
return Element(pc: pc, function: sp.currentFunction)
}

let sp: Sp

func makeIterator() -> FrameIterator {
FrameIterator(sp: self.sp)
}
}

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

while let frame = frames.next() {
guard let function = frame.function else {
for frame in callStack {
guard let function = frame.sp.currentFunction else {
symbols.append(.init(name: nil, address: frame.pc))
continue
}
Expand Down
Loading
Loading