Skip to content

Commit da43827

Browse files
committed
Enable iseq -> wasm instruction mapping
1 parent 5c09270 commit da43827

File tree

14 files changed

+129
-47
lines changed

14 files changed

+129
-47
lines changed

.sourcekit-lsp/config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"swiftPM": {
3+
"traits": ["WasmDebuggingSupport"]
4+
}
5+
}

Sources/WAT/Encoder.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ extension TableType: WasmEncodable {
158158
struct ElementExprCollector: AnyInstructionVisitor {
159159
typealias Output = Void
160160

161+
var binaryOffset: Int = 0
161162
var isAllRefFunc: Bool = true
162163
var instructions: [Instruction] = []
163164

@@ -443,6 +444,7 @@ extension WatParser.DataSegmentDecl {
443444
}
444445

445446
struct ExpressionEncoder: BinaryInstructionEncoder {
447+
var binaryOffset: Int = 0
446448
var encoder = Encoder()
447449
var hasDataSegmentInstruction: Bool = false
448450

Sources/WAT/Parser/WastParser.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ struct WastParser {
5454
}
5555

5656
struct ConstExpressionCollector: WastConstInstructionVisitor {
57+
var binaryOffset: Int = 0
5758
let addValue: (Value) -> Void
5859

5960
mutating func visitI32Const(value: Int32) throws { addValue(.i32(UInt32(bitPattern: value))) }

Sources/WasmKit/Execution/Debugger.swift

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,69 @@
11
#if WasmDebuggingSupport
22

33
package struct Debugger: ~Copyable {
4+
enum Error: Swift.Error {
5+
case entrypointFunctionNotFound
6+
}
7+
48
let valueStack: Sp
59
let execution: Execution
610
let store: Store
711

8-
package init(store: Store) {
12+
private let module: Module
13+
private let instance: Instance
14+
15+
/// Addresses of each function in the code section of ``module``
16+
let functionAddresses: [Int]
17+
let entrypointFunction: Function
18+
19+
package init(module: Module, store: Store, imports: Imports) throws(Error) {
920
let limit = store.engine.configuration.stackSize / MemoryLayout<StackSlot>.stride
21+
let instance = try module.instantiate(store: store, imports: imports, isDebuggable: true)
22+
23+
guard case .function(let entrypointFunction) = instance.exports["_start"] else {
24+
throw Error.entrypointFunctionNotFound
25+
}
26+
27+
self.instance = instance
28+
self.module = module
29+
self.functionAddresses = module.functions.map { $0.code.originalAddress }
30+
self.entrypointFunction = entrypointFunction
1031
self.valueStack = UnsafeMutablePointer<StackSlot>.allocate(capacity: limit)
1132
self.store = store
1233
self.execution = Execution(store: StoreRef(store), stackEnd: valueStack.advanced(by: limit))
1334
}
1435

15-
package func toggleBreakpoint() {
36+
package func stopAtEntrypoint() throws(Error) {
37+
try self.toggleBreakpoint(address: self.originalAddress(function: entrypointFunction))
38+
}
39+
40+
package func originalAddress(function: Function) throws(Error) -> Int {
41+
precondition(function.handle.isWasm)
42+
43+
switch function.handle.wasm.code {
44+
case .debuggable(let wasm, _):
45+
return wasm.originalAddress
46+
case .uncompiled:
47+
try function.handle.wasm.ensureCompiled(store: StoreRef(self.store))
48+
return try self.originalAddress(function: function)
49+
case .compiled:
50+
print(function.handle.wasm.code)
51+
fatalError()
52+
}
53+
}
1654

55+
package mutating func toggleBreakpoint(address: Int) throws(Error) {
56+
print("attempt to toggle a breakpoint at \(address)")
1757
}
1858

1959
/// Array of addresses in the Wasm binary of executed instructions on the call stack.
20-
package var currentCallStack: [UInt64] {
60+
package var currentCallStack: [Int] {
61+
guard let instance = self.valueStack.currentInstance else { return [] }
62+
let isDebuggable = instance.isDebuggable
63+
print("isDebuggable is \(isDebuggable)")
64+
2165
return Execution.captureBacktrace(sp: self.valueStack, store: self.store).symbols.map {
22-
switch $0.debuggingAddress {
23-
case .iseq: fatalError()
24-
case .wasm(let pc): return pc
25-
}
66+
instance.iSeqToWasmMapping[$0.address]!
2667
}
2768
}
2869

Sources/WasmKit/Execution/Errors.swift

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,10 @@ import struct WasmParser.Import
55
/// The backtrace of the trap.
66
struct Backtrace: CustomStringConvertible, Sendable {
77
/// A symbol in the backtrace.
8-
struct Symbol {
8+
struct Symbol: @unchecked Sendable {
99
/// The name of the symbol.
1010
let name: String?
11-
let debuggingAddress: DebuggingAddress
12-
13-
/// Address of the symbol for debugging purposes.
14-
enum DebuggingAddress: CustomStringConvertible,
15-
// `Pc` is a pointer, which is not `Sendable` by default.
16-
@unchecked Sendable
17-
{
18-
case iseq(Pc)
19-
case wasm(UInt64)
20-
21-
var description: String {
22-
switch self {
23-
case .iseq(let pc): "iseq(\(Int(bitPattern: pc)))"
24-
case .wasm(let wasmAddress): "wasm(\(wasmAddress))"
25-
}
26-
}
27-
}
11+
let address: Pc
2812
}
2913

3014
/// The symbols in the backtrace.
@@ -35,7 +19,7 @@ struct Backtrace: CustomStringConvertible, Sendable {
3519
print("backtrace contains \(symbols.count) symbols")
3620
return symbols.enumerated().map { (index, symbol) in
3721
let name = symbol.name ?? "unknown"
38-
return " \(index): (\(symbol.debuggingAddress)) \(name)"
22+
return " \(index): (\(symbol.address)) \(name)"
3923
}.joined(separator: "\n")
4024
}
4125
}

Sources/WasmKit/Execution/Execution.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,14 @@ struct Execution: ~Copyable {
6969
static func captureBacktrace(sp: Sp, store: Store) -> Backtrace {
7070
var frames = FrameIterator(sp: sp)
7171
var symbols: [Backtrace.Symbol] = []
72+
7273
while let frame = frames.next() {
7374
guard let function = frame.function else {
74-
symbols.append(.init(name: nil, debuggingAddress: .iseq(frame.pc)))
75+
symbols.append(.init(name: nil, address: frame.pc))
7576
continue
7677
}
7778
let symbolName = store.nameRegistry.symbolicate(.wasm(function))
78-
symbols.append(.init(name: symbolName, debuggingAddress: .iseq(frame.pc)))
79+
symbols.append(.init(name: symbolName, address: frame.pc))
7980
}
8081
return Backtrace(symbols: symbols)
8182
}
@@ -251,7 +252,7 @@ extension Sp {
251252
nonmutating set { self[-1] = UInt64(UInt(bitPattern: newValue)) }
252253
}
253254

254-
fileprivate var currentInstance: InternalInstance? {
255+
var currentInstance: InternalInstance? {
255256
currentFunction?.instance
256257
}
257258
}

Sources/WasmKit/Execution/Instances.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ struct InstanceEntity /* : ~Copyable */ {
8484
var features: WasmFeatureSet
8585
var dataCount: UInt32?
8686
var isDebuggable: Bool
87-
var iSeqToWasmMapping: [Pc: UInt64]
87+
var iSeqToWasmMapping: [Pc: Int]
8888

8989
static var empty: InstanceEntity {
9090
InstanceEntity(

Sources/WasmKit/Translator.swift

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ struct InstructionTranslator: InstructionVisitor {
811811

812812
let allocator: ISeqAllocator
813813
let funcTypeInterner: Interner<FunctionType>
814-
let module: InternalInstance
814+
var module: InternalInstance
815815
private var iseqBuilder: ISeqBuilder
816816
var controlStack: ControlStack
817817
var valueStack: ValueStack
@@ -825,6 +825,14 @@ struct InstructionTranslator: InstructionVisitor {
825825
var constantSlots: ConstSlots
826826
let validator: InstructionValidator
827827

828+
// Wasm debugging support.
829+
830+
/// Current offset to an instruction in the original Wasm binary processed by this translator.
831+
var binaryOffset: Int = 0
832+
833+
/// Mapping from `self.iseqBuilder.instructions` to Wasm instructions
834+
var iSeqToWasmMapping = [Int: Int]()
835+
828836
init(
829837
allocator: ISeqAllocator,
830838
engineConfiguration: EngineConfiguration,
@@ -874,12 +882,14 @@ struct InstructionTranslator: InstructionVisitor {
874882
}
875883

876884
private mutating func emit(_ instruction: Instruction, resultRelink: ISeqBuilder.ResultRelink? = nil) {
885+
self.updateInstructionMapping()
877886
iseqBuilder.emit(instruction, resultRelink: resultRelink)
878887
}
879888

880889
@discardableResult
881890
private mutating func emitCopyStack(from source: VReg, to dest: VReg) -> Bool {
882891
guard source != dest else { return false }
892+
self.updateInstructionMapping()
883893
emit(.copyStack(Instruction.CopyStackOperand(source: LVReg(source), dest: LVReg(dest))))
884894
return true
885895
}
@@ -1065,6 +1075,7 @@ struct InstructionTranslator: InstructionVisitor {
10651075
emit(.onExit(functionIndex))
10661076
}
10671077
try visitReturnLike()
1078+
self.updateInstructionMapping()
10681079
iseqBuilder.emit(._return)
10691080
}
10701081
private mutating func markUnreachable() throws {
@@ -1084,9 +1095,15 @@ struct InstructionTranslator: InstructionVisitor {
10841095
let instructions = iseqBuilder.finalize()
10851096
// TODO: Figure out a way to avoid the copy here while keeping the execution performance.
10861097
let buffer = allocator.allocateInstructions(capacity: instructions.count)
1087-
for (idx, instruction) in instructions.enumerated() {
1088-
buffer[idx] = instruction
1098+
let initializedElementsIndex = buffer.initialize(fromContentsOf: instructions)
1099+
assert(initializedElementsIndex == instructions.endIndex)
1100+
1101+
for (iseq, wasm) in self.iSeqToWasmMapping {
1102+
self.module.withValue {
1103+
$0.iSeqToWasmMapping[iseq + buffer.baseAddress.unsafelyUnwrapped] = wasm
1104+
}
10891105
}
1106+
10901107
let constants = allocator.allocateConstants(self.constantSlots.values)
10911108
return InstructionSequence(
10921109
instructions: buffer,
@@ -1095,6 +1112,15 @@ struct InstructionTranslator: InstructionVisitor {
10951112
)
10961113
}
10971114

1115+
private mutating func updateInstructionMapping() {
1116+
// This is a hot path, so best to exclude the code altogether if the trait isn't enabled.
1117+
#if WasmDebuggingSupport
1118+
guard self.module.isDebuggable else { return }
1119+
1120+
self.iSeqToWasmMapping[self.iseqBuilder.insertingPC.offsetFromHead] = self.binaryOffset
1121+
#endif
1122+
}
1123+
10981124
// MARK: Main entry point
10991125

11001126
/// Translate a Wasm expression into a sequence of instructions.
@@ -1122,7 +1148,9 @@ struct InstructionTranslator: InstructionVisitor {
11221148
emit(.unreachable)
11231149
try markUnreachable()
11241150
}
1125-
mutating func visitNop() -> Output { emit(.nop) }
1151+
mutating func visitNop() -> Output {
1152+
emit(.nop)
1153+
}
11261154

11271155
mutating func visitBlock(blockType: WasmParser.BlockType) throws -> Output {
11281156
let blockType = try module.resolveBlockType(blockType)
@@ -1169,6 +1197,7 @@ struct InstructionTranslator: InstructionVisitor {
11691197
)
11701198
)
11711199
guard let condition = condition else { return }
1200+
self.updateInstructionMapping()
11721201
iseqBuilder.emitWithLabel(Instruction.brIfNot, endLabel) { iseqBuilder, selfPC, endPC in
11731202
let targetPC: MetaProgramCounter
11741203
if let elsePC = iseqBuilder.resolveLabel(elseLabel) {
@@ -1189,6 +1218,8 @@ struct InstructionTranslator: InstructionVisitor {
11891218
preserveOnStack(depth: valueStack.height - frame.stackHeight)
11901219
try controlStack.resetReachability()
11911220
iseqBuilder.resetLastEmission()
1221+
1222+
self.updateInstructionMapping()
11921223
iseqBuilder.emitWithLabel(Instruction.br, endLabel) { _, selfPC, endPC in
11931224
let offset = endPC.offsetFromHead - selfPC.offsetFromHead
11941225
return Int32(offset)
@@ -1284,6 +1315,8 @@ struct InstructionTranslator: InstructionVisitor {
12841315
currentFrame: try controlStack.currentFrame(),
12851316
currentHeight: valueStack.height
12861317
)
1318+
1319+
self.updateInstructionMapping()
12871320
iseqBuilder.emitWithLabel(makeInstruction, frame.continuation) { _, selfPC, continuation in
12881321
let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead
12891322
return make(Int32(relativeOffset), UInt32(copyCount), popCount)
@@ -1317,6 +1350,7 @@ struct InstructionTranslator: InstructionVisitor {
13171350
if frame.copyCount == 0 {
13181351
guard let condition else { return }
13191352
// Optimization where we don't need copying values when the branch taken
1353+
self.updateInstructionMapping()
13201354
iseqBuilder.emitWithLabel(Instruction.brIf, frame.continuation) { _, selfPC, continuation in
13211355
let relativeOffset = continuation.offsetFromHead - selfPC.offsetFromHead
13221356
return Instruction.BrIfOperand(
@@ -1349,11 +1383,13 @@ struct InstructionTranslator: InstructionVisitor {
13491383
// [0x06] (local.get 1 reg:2) <----|---------+
13501384
// [0x07] ... <-------+
13511385
let onBranchNotTaken = iseqBuilder.allocLabel()
1386+
self.updateInstructionMapping()
13521387
iseqBuilder.emitWithLabel(Instruction.brIfNot, onBranchNotTaken) { _, conditionCheckAt, continuation in
13531388
let relativeOffset = continuation.offsetFromHead - conditionCheckAt.offsetFromHead
13541389
return Instruction.BrIfOperand(condition: LVReg(condition), offset: Int32(relativeOffset))
13551390
}
13561391
try copyOnBranch(targetFrame: frame)
1392+
self.updateInstructionMapping()
13571393
try emitBranch(Instruction.br, relativeDepth: relativeDepth) { offset, copyCount, popCount in
13581394
return offset
13591395
}
@@ -1380,6 +1416,7 @@ struct InstructionTranslator: InstructionVisitor {
13801416
baseAddress: tableBuffer.baseAddress!,
13811417
count: UInt16(tableBuffer.count), index: index
13821418
)
1419+
self.updateInstructionMapping()
13831420
iseqBuilder.emit(.brTable(operand))
13841421
let brTableAt = iseqBuilder.insertingPC
13851422

@@ -1429,6 +1466,7 @@ struct InstructionTranslator: InstructionVisitor {
14291466
}
14301467
let emittedCopy = try copyOnBranch(targetFrame: frame)
14311468
if emittedCopy {
1469+
self.updateInstructionMapping()
14321470
iseqBuilder.emitWithLabel(Instruction.br, frame.continuation) { _, brAt, continuation in
14331471
let relativeOffset = continuation.offsetFromHead - brAt.offsetFromHead
14341472
return Int32(relativeOffset)

Sources/WasmKitGDBHandler/WasmKitGDBHandler.swift

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,12 @@
2525
enum Error: Swift.Error {
2626
case unknownTransferArguments
2727
case unknownReadMemoryArguments
28-
case entrypointFunctionNotFound
2928
}
3029

3130
private let wasmBinary: ByteBuffer
32-
private let module: Module
3331
private let moduleFilePath: FilePath
3432
private let logger: Logger
3533
private let debugger: Debugger
36-
private let instance: Instance
37-
private let entrypointFunction: Function
3834
private let functionsRLE: [(wasmAddress: Int, iSeqAddress: Int)] = []
3935

4036
package init(logger: Logger, moduleFilePath: FilePath) async throws {
@@ -44,20 +40,15 @@
4440
try await $0.readToEnd(maximumSizeAllowed: .unlimited)
4541
}
4642

47-
self.module = try parseWasm(bytes: .init(buffer: self.wasmBinary))
4843
self.moduleFilePath = moduleFilePath
49-
let store = Store(engine: Engine())
50-
self.debugger = Debugger(store: store)
5144

45+
let store = Store(engine: Engine())
5246
var imports = Imports()
5347
let wasi = try WASIBridgeToHost()
5448
wasi.link(to: &imports, store: store)
55-
self.instance = try module.instantiate(store: store, imports: imports)
5649

57-
guard case .function(let entrypointFunction) = self.instance.exports["_start"] else {
58-
throw Error.entrypointFunctionNotFound
59-
}
60-
self.entrypointFunction = entrypointFunction
50+
self.debugger = try Debugger(module: parseWasm(bytes: .init(buffer: self.wasmBinary)), store: store, imports: imports)
51+
try self.debugger.stopAtEntrypoint()
6152
}
6253

6354
package func handle(command: GDBHostCommand) throws -> GDBTargetResponse {

Sources/WasmParser/BinaryInstructionDecoder.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import WasmTypes
66

77
@usableFromInline
88
protocol BinaryInstructionDecoder {
9+
/// Current offset in the decoded Wasm binary.
10+
var offset: Int { get }
11+
912
/// Claim the next byte to be decoded
1013
@inlinable func claimNextByte() throws -> UInt8
1114

@@ -91,6 +94,8 @@ protocol BinaryInstructionDecoder {
9194

9295
@inlinable
9396
func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool {
97+
visitor.binaryOffset = decoder.offset
98+
9499
let opcode0 = try decoder.claimNextByte()
95100
switch opcode0 {
96101
case 0x00:

0 commit comments

Comments
 (0)