Skip to content

Commit ccbdb53

Browse files
authored
Add breakpoint VM instruction (#198)
Per a discussion, following options are available for enabling breakpoints in WasmKit: > One of the main difficulties in implementing breakpoint in WasmKit is that our internal instruction set is not a 1:1 map of Wasm instruction set, unlike IPInt in JSC. Therefore, we need a way to translate address between Wasm byte offset <-> internal ISeq. > >Option 1: Record address translation table during compiling wasm bytecode to internal ISeq when a first breakpoint is set to the function, then patch the internal ISeq to set breakpoint using the table. We only need one-time re-compilation to record the table, so later breakpoint settings will be trivial, but it adds non-trivial memory footprint to hold the table. > >Option 2: Patch the original wasm bytecode to replace the target instruction with a special reserved breakpoint instruction and compile to the internal ISeq. We don't need to hold the addr translation table but need to re-compile every time a new breakpoint is set. > > Option 3: Implement in-place interpreter along side with the current fast interpreter. This is the simplest approach and what WAMR and JSC are doing for their classic interpreter and IPInt respectively, but need to maintain two interpreters. We've decided to proceed with Option 2, where a special `breakpoint` instruction is added to the VM, which is translated from 0xFF Wasm binary code. This code can never be supplied by the user, as it isn't enabled in the parser. In a future PR we'll provide `public` methods on module instances like `func enableBreakpoint` that flips this instruction in in-memory module source and recompiles the corresponding function to VM bytecode with the new `breakpoint` instruction.
1 parent e0ce4e3 commit ccbdb53

File tree

12 files changed

+97
-22
lines changed

12 files changed

+97
-22
lines changed

Sources/WasmKit/Execution/DispatchInstruction.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ extension Execution {
212212
case 196: return self.execute_tableElementDrop(sp: &sp, pc: &pc, md: &md, ms: &ms)
213213
case 197: return self.execute_onEnter(sp: &sp, pc: &pc, md: &md, ms: &ms)
214214
case 198: return self.execute_onExit(sp: &sp, pc: &pc, md: &md, ms: &ms)
215+
case 199: return try self.execute_breakpoint(sp: &sp, pc: &pc, md: &md, ms: &ms)
215216
default: preconditionFailure("Unknown instruction!?")
216217

217218
}
@@ -1795,6 +1796,12 @@ extension Execution {
17951796
pc.pointee = pc.pointee.advanced(by: 1)
17961797
return next
17971798
}
1799+
@_silgen_name("wasmkit_execute_breakpoint") @inline(__always)
1800+
mutating func execute_breakpoint(sp: UnsafeMutablePointer<Sp>, pc: UnsafeMutablePointer<Pc>, md: UnsafeMutablePointer<Md>, ms: UnsafeMutablePointer<Ms>) throws -> CodeSlot {
1801+
let next: CodeSlot
1802+
(pc.pointee, next) = try self.breakpoint(sp: &sp.pointee, pc: pc.pointee)
1803+
return next
1804+
}
17981805
}
17991806

18001807
extension Instruction {

Sources/WasmKit/Execution/Execution.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,12 @@ extension Execution {
361361
}
362362
}
363363

364-
/// A ``Error`` thrown when the execution normally ends.
364+
/// An ``Error`` thrown when the execution normally ends.
365365
struct EndOfExecution: Error {}
366366

367+
/// An ``Error`` thrown when a breakpoint is triggered.
368+
struct Breakpoint: Error {}
369+
367370
/// The entry point for the execution of the WebAssembly function.
368371
@inline(never)
369372
mutating func execute(

Sources/WasmKit/Execution/Function.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ struct InternalFunction: Equatable, Hashable {
124124
_storage = bitPattern
125125
}
126126

127+
/// Returns `true` if the function is defined as a Wasm function in its original module.
128+
/// Returns `false` if the function is implemented by the host.
127129
var isWasm: Bool {
128130
_storage & 0b1 == 0
129131
}
@@ -241,7 +243,7 @@ struct WasmFunctionEntity {
241243
switch code {
242244
case .uncompiled(let code):
243245
return try compile(store: store, code: code)
244-
case .compiled(let iseq):
246+
case .compiled(let iseq), .compiledAndPatchable(_, let iseq):
245247
return iseq
246248
}
247249
}
@@ -260,7 +262,7 @@ struct WasmFunctionEntity {
260262
locals: code.locals,
261263
functionIndex: index,
262264
codeSize: code.expression.count,
263-
intercepting: engine.interceptor != nil
265+
isIntercepting: engine.interceptor != nil
264266
)
265267
let iseq = try code.withValue { code in
266268
try translator.translate(code: code)
@@ -281,7 +283,8 @@ extension EntityHandle<WasmFunctionEntity> {
281283
$0.code = .compiled(iseq)
282284
return iseq
283285
}
284-
case .compiled(let iseq): return iseq
286+
case .compiled(let iseq), .compiledAndPatchable(_, let iseq):
287+
return iseq
285288
}
286289
}
287290
}
@@ -313,6 +316,7 @@ struct InstructionSequence {
313316
enum CodeBody {
314317
case uncompiled(InternalUncompiledCode)
315318
case compiled(InstructionSequence)
319+
case compiledAndPatchable(InternalUncompiledCode, InstructionSequence)
316320
}
317321

318322
extension Reference {

Sources/WasmKit/Execution/Instructions/Control.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,8 @@ extension Execution {
223223
Function(handle: function, store: store.value)
224224
)
225225
}
226+
227+
mutating func breakpoint(sp: inout Sp, pc: Pc) throws -> (Pc, CodeSlot) {
228+
throw Breakpoint()
229+
}
226230
}

Sources/WasmKit/Execution/Instructions/Instruction.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,10 @@ enum Instruction: Equatable {
413413
case onEnter(Instruction.OnEnterOperand)
414414
/// Intercept the exit of a function
415415
case onExit(Instruction.OnExitOperand)
416+
/// Stop the VM on this instruction as a breakpoint
417+
///
418+
/// This instruction is used in debugging scenarios.
419+
case breakpoint
416420
}
417421

418422
extension Instruction {
@@ -1271,6 +1275,7 @@ extension Instruction {
12711275
case .tableElementDrop: return 196
12721276
case .onEnter: return 197
12731277
case .onExit: return 198
1278+
case .breakpoint: return 199
12741279
}
12751280
}
12761281
}
@@ -1481,6 +1486,7 @@ extension Instruction {
14811486
case 196: return .tableElementDrop(Instruction.TableElementDropOperand.load(from: &pc))
14821487
case 197: return .onEnter(Instruction.OnEnterOperand.load(from: &pc))
14831488
case 198: return .onExit(Instruction.OnExitOperand.load(from: &pc))
1489+
case 199: return .breakpoint
14841490
default: fatalError("Unknown instruction opcode: \(opcode)")
14851491
}
14861492
}
@@ -1694,6 +1700,7 @@ extension Instruction {
16941700
case 196: return "tableElementDrop"
16951701
case 197: return "onEnter"
16961702
case 198: return "onExit"
1703+
case 199: return "breakpoint"
16971704
default: fatalError("Unknown instruction index: \(opcode)")
16981705
}
16991706
}

Sources/WasmKit/Translator.swift

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,9 @@ struct InstructionTranslator: InstructionVisitor {
820820
/// The index of the function in the module
821821
let functionIndex: FunctionIndex
822822
/// Whether a call to this function should be intercepted
823-
let intercepting: Bool
823+
let isIntercepting: Bool
824+
/// Whether Wasm debugging facilities are currently enabled.
825+
let isDebugging: Bool
824826
var constantSlots: ConstSlots
825827
let validator: InstructionValidator
826828

@@ -833,7 +835,8 @@ struct InstructionTranslator: InstructionVisitor {
833835
locals: [WasmTypes.ValueType],
834836
functionIndex: FunctionIndex,
835837
codeSize: Int,
836-
intercepting: Bool
838+
isIntercepting: Bool,
839+
isDebugging: Bool = false
837840
) throws {
838841
self.allocator = allocator
839842
self.funcTypeInterner = funcTypeInterner
@@ -849,7 +852,8 @@ struct InstructionTranslator: InstructionVisitor {
849852
self.valueStack = ValueStack(stackLayout: stackLayout)
850853
self.locals = Locals(types: type.parameters + locals)
851854
self.functionIndex = functionIndex
852-
self.intercepting = intercepting
855+
self.isIntercepting = isIntercepting
856+
self.isDebugging = isDebugging
853857
self.constantSlots = ConstSlots(stackLayout: stackLayout)
854858
self.validator = InstructionValidator(context: module)
855859

@@ -1059,7 +1063,7 @@ struct InstructionTranslator: InstructionVisitor {
10591063
return emittedCopy
10601064
}
10611065
private mutating func translateReturn() throws {
1062-
if intercepting {
1066+
if isIntercepting {
10631067
// Emit `onExit` instruction before every `return` instruction
10641068
emit(.onExit(functionIndex))
10651069
}
@@ -1098,7 +1102,7 @@ struct InstructionTranslator: InstructionVisitor {
10981102

10991103
/// Translate a Wasm expression into a sequence of instructions.
11001104
mutating func translate(code: Code) throws -> InstructionSequence {
1101-
if intercepting {
1105+
if isIntercepting {
11021106
// Emit `onEnter` instruction at the beginning of the function
11031107
emit(.onEnter(functionIndex))
11041108
}
@@ -2239,6 +2243,16 @@ struct InstructionTranslator: InstructionVisitor {
22392243
return .tableSize(Instruction.TableSizeOperand(tableIndex: table, result: LVReg(result)))
22402244
}
22412245
}
2246+
2247+
mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool {
2248+
guard self.isDebugging && opcode.count == 1 && opcode[0] == 0xFF else {
2249+
return false
2250+
}
2251+
2252+
emit(.breakpoint)
2253+
2254+
return true
2255+
}
22422256
}
22432257

22442258
struct TranslationError: Error, CustomStringConvertible {

Sources/WasmParser/BinaryInstructionDecoder.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import WasmTypes
88
protocol BinaryInstructionDecoder {
99
/// Claim the next byte to be decoded
1010
@inlinable func claimNextByte() throws -> UInt8
11-
/// Visit unknown instruction
12-
@inlinable func visitUnknown(_ opcode: [UInt8]) throws
11+
12+
func throwUnknown(_ opcode: [UInt8]) throws -> Never
1313
/// Decode `block` immediates
1414
@inlinable mutating func visitBlock() throws -> BlockType
1515
/// Decode `loop` immediates
@@ -87,8 +87,9 @@ protocol BinaryInstructionDecoder {
8787
/// Decode `table.size` immediates
8888
@inlinable mutating func visitTableSize() throws -> UInt32
8989
}
90+
9091
@inlinable
91-
func parseBinaryInstruction<V: InstructionVisitor, D: BinaryInstructionDecoder>(visitor: inout V, decoder: inout D) throws -> Bool {
92+
func parseBinaryInstruction(visitor: inout some InstructionVisitor, decoder: inout some BinaryInstructionDecoder) throws -> Bool {
9293
let opcode0 = try decoder.claimNextByte()
9394
switch opcode0 {
9495
case 0x00:
@@ -562,10 +563,10 @@ func parseBinaryInstruction<V: InstructionVisitor, D: BinaryInstructionDecoder>(
562563
let (table) = try decoder.visitTableFill()
563564
try visitor.visitTableFill(table: table)
564565
default:
565-
try decoder.visitUnknown([opcode0, opcode1])
566+
if try !visitor.visitUnknown([opcode0, opcode1]) { try decoder.throwUnknown([opcode0, opcode1]) }
566567
}
567568
default:
568-
try decoder.visitUnknown([opcode0])
569+
if try !visitor.visitUnknown([opcode0]) { try decoder.throwUnknown([opcode0]) }
569570
}
570571
return false
571572
}

Sources/WasmParser/InstructionVisitor.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,8 @@ public protocol InstructionVisitor {
398398
mutating func visitTableGrow(table: UInt32) throws
399399
/// Visiting `table.size` instruction.
400400
mutating func visitTableSize(table: UInt32) throws
401+
/// Returns: `true` if the parser should silently proceed parsing.
402+
mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool
401403
}
402404

403405
extension InstructionVisitor {
@@ -514,5 +516,6 @@ extension InstructionVisitor {
514516
public mutating func visitTableSet(table: UInt32) throws {}
515517
public mutating func visitTableGrow(table: UInt32) throws {}
516518
public mutating func visitTableSize(table: UInt32) throws {}
519+
public mutating func visitUnknown(_ opcode: [UInt8]) throws -> Bool { false }
517520
}
518521

Sources/WasmParser/WasmParser.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -606,10 +606,14 @@ extension Parser: BinaryInstructionDecoder {
606606
return 0
607607
}
608608

609-
@inlinable func visitUnknown(_ opcode: [UInt8]) throws {
609+
@inlinable func throwUnknown(_ opcode: [UInt8]) throws -> Never {
610610
throw makeError(.illegalOpcode(opcode))
611611
}
612612

613+
@inlinable func visitUnknown(_ opcode: [UInt8]) throws -> Bool {
614+
try throwUnknown(opcode)
615+
}
616+
613617
@inlinable mutating func visitBlock() throws -> BlockType { try parseResultType() }
614618
@inlinable mutating func visitLoop() throws -> BlockType { try parseResultType() }
615619
@inlinable mutating func visitIf() throws -> BlockType { try parseResultType() }
@@ -744,6 +748,7 @@ extension Parser: BinaryInstructionDecoder {
744748
return try stream.consumeAny()
745749
}
746750

751+
/// Returns: `true` if the parsed instruction is the block end instruction.
747752
@inline(__always)
748753
@inlinable
749754
mutating func parseInstruction<V: InstructionVisitor>(visitor v: inout V) throws -> Bool {

Sources/_CWasmKit/include/DirectThreadedCode.inc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,13 @@ SWIFT_CC(swiftasync) static inline void wasmkit_tc_onExit(Sp sp, Pc pc, Md md, M
12621262
INLINE_CALL next = wasmkit_execute_onExit(&sp, &pc, &md, &ms, state, &error);
12631263
return ((wasmkit_tc_exec)next)(sp, pc, md, ms, state);
12641264
}
1265+
SWIFT_CC(swiftasync) static inline void wasmkit_tc_breakpoint(Sp sp, Pc pc, Md md, Ms ms, SWIFT_CONTEXT void *state) {
1266+
SWIFT_CC(swift) uint64_t wasmkit_execute_breakpoint(Sp *sp, Pc *pc, Md *md, Ms *ms, SWIFT_CONTEXT void *state, SWIFT_ERROR_RESULT void **error);
1267+
void * _Nullable error = NULL; uint64_t next;
1268+
INLINE_CALL next = wasmkit_execute_breakpoint(&sp, &pc, &md, &ms, state, &error);
1269+
if (error) return wasmkit_execution_state_set_error(error, sp, state);
1270+
return ((wasmkit_tc_exec)next)(sp, pc, md, ms, state);
1271+
}
12651272
static const uintptr_t wasmkit_tc_exec_handlers[] = {
12661273
(uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_copyStack),
12671274
(uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_globalGet),
@@ -1462,4 +1469,5 @@ static const uintptr_t wasmkit_tc_exec_handlers[] = {
14621469
(uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_tableElementDrop),
14631470
(uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_onEnter),
14641471
(uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_onExit),
1472+
(uintptr_t)((wasmkit_tc_exec)&wasmkit_tc_breakpoint),
14651473
};

0 commit comments

Comments
 (0)