diff --git a/Makefile b/Makefile index 3cd82bae..9fbf5b17 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,8 @@ build.examples.tinygo: $(tinygo_sources) @for f in $^; do \ tinygo build -o $$(echo $$f | sed -e 's/\.go/\.wasm/') -scheduler=none --no-debug --target=wasi $$f; \ done + # Need DWARF sections. + tinygo build -o internal/testing/dwarftestdata/testdata/main.wasm -scheduler=none --target=wasi internal/testing/dwarftestdata/testdata/main.go # We use zig to build C as it is easy to install and embeds a copy of zig-cc. c_sources := imports/wasi_snapshot_preview1/example/testdata/zig-cc/cat.c imports/wasi_snapshot_preview1/testdata/zig-cc/ls.c diff --git a/experimental/dwarf.go b/experimental/dwarf.go new file mode 100644 index 00000000..4409b217 --- /dev/null +++ b/experimental/dwarf.go @@ -0,0 +1,47 @@ +package experimental + +import ( + "context" +) + +type enableDWARFBasedStackTraceKey struct{} + +// WithDWARFBasedStackTrace enables the DWARF based stack traces in the face of runtime errors. +// This only takes into effect when the original Wasm binary has the DWARF "custom sections" +// that are often stripped depending on the optimization options of the compilers. +// +// For example, when this is not enabled, the stack trace message looks like: +// +// wasm stack trace: +// .runtime._panic(i32) +// .myFunc() +// .main.main() +// .runtime.run() +// ._start() +// +// and when it is enabled: +// +// wasm stack trace: +// .runtime._panic(i32) +// 0x16e2: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_tinygowasm.go:73:6 +// .myFunc() +// 0x190b: /Users/XXXXX/wazero/internal/testing/dwarftestdata/testdata/main.go:19:7 +// .main.main() +// 0x18ed: /Users/XXXXX/wazero/internal/testing/dwarftestdata/testdata/main.go:4:3 +// .runtime.run() +// 0x18cc: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/scheduler_none.go:26:10 +// ._start() +// 0x18b6: /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_wasm_wasi.go:22:5 +// +// which contains the source code information. +// +// See https://github.com/tetratelabs/wazero/pull/881 for more context. +func WithDWARFBasedStackTrace(ctx context.Context) context.Context { + return context.WithValue(ctx, enableDWARFBasedStackTraceKey{}, struct{}{}) +} + +// DWARFBasedStackTraceEnabled returns true if the given context has the option enabling the DWARF +// based stack trace, and false otherwise. +func DWARFBasedStackTraceEnabled(ctx context.Context) bool { + return ctx.Value(enableDWARFBasedStackTraceKey{}) != nil +} diff --git a/experimental/dwarf_test.go b/experimental/dwarf_test.go new file mode 100644 index 00000000..18f9a0d8 --- /dev/null +++ b/experimental/dwarf_test.go @@ -0,0 +1,61 @@ +package experimental_test + +import ( + "context" + _ "embed" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/dwarftestdata" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +func TestWithDWARFBasedStackTrace(t *testing.T) { + ctx := context.Background() + require.False(t, experimental.DWARFBasedStackTraceEnabled(ctx)) + ctx = experimental.WithDWARFBasedStackTrace(ctx) + require.True(t, experimental.DWARFBasedStackTraceEnabled(ctx)) + + type testCase struct { + name string + r wazero.Runtime + } + + tests := []testCase{{name: "interpreter", r: wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())}} + + if platform.CompilerSupported() { + tests = append(tests, testCase{ + name: "compiler", r: wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler()), + }) + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + r := tc.r + defer r.Close(ctx) // This closes everything this Runtime created. + + wasi_snapshot_preview1.MustInstantiate(ctx, r) + + compiled, err := r.CompileModule(ctx, dwarftestdata.DWARFWasm) + require.NoError(t, err) + + // Use context.Background to ensure that DWARF is a compile-time option. + _, err = r.InstantiateModule(context.Background(), compiled, wazero.NewModuleConfig()) + require.Error(t, err) + + errStr := err.Error() + require.Contains(t, errStr, "src/runtime/runtime_tinygowasm.go:73:6") + require.Contains(t, errStr, "wazero/internal/testing/dwarftestdata/testdata/main.go:19:7") + require.Contains(t, errStr, "wazero/internal/testing/dwarftestdata/testdata/main.go:14:3") + require.Contains(t, errStr, "wazero/internal/testing/dwarftestdata/testdata/main.go:9:3") + require.Contains(t, errStr, "wazero/internal/testing/dwarftestdata/testdata/main.go:4:3") + require.Contains(t, errStr, "wazero/internal/testing/dwarftestdata/testdata/main.go:4:3") + require.Contains(t, errStr, "src/runtime/scheduler_none.go:26:10") + require.Contains(t, errStr, "src/runtime/runtime_wasm_wasi.go:22:5") + }) + } +} diff --git a/internal/engine/compiler/compiler.go b/internal/engine/compiler/compiler.go index e14113d9..49a74680 100644 --- a/internal/engine/compiler/compiler.go +++ b/internal/engine/compiler/compiler.go @@ -323,4 +323,7 @@ type compiler interface { pushRuntimeValueLocationOnRegister(reg asm.Register, vt runtimeValueType) (ret *runtimeValueLocation) // pushRuntimeValueLocationOnRegister pushes a new vector value's runtimeValueLocation on a register `reg`. pushVectorRuntimeValueLocationOnRegister(reg asm.Register) (lowerBitsLocation *runtimeValueLocation) + // compileNOP compiles NOP instruction and returns the corresponding asm.Node in the assembled native code. + // This is used to emit DWARF based stack traces. + compileNOP() asm.Node } diff --git a/internal/engine/compiler/engine.go b/internal/engine/compiler/engine.go index c1ac85fc..3c4afd4a 100644 --- a/internal/engine/compiler/engine.go +++ b/internal/engine/compiler/engine.go @@ -6,11 +6,13 @@ import ( "fmt" "reflect" "runtime" + "sort" "sync" "unsafe" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/asm" "github.com/tetratelabs/wazero/internal/compilationcache" "github.com/tetratelabs/wazero/internal/platform" "github.com/tetratelabs/wazero/internal/version" @@ -263,6 +265,19 @@ type ( sourceModule *wasm.Module // listener holds a listener to notify when this function is called. listener experimental.FunctionListener + + sourceOffsetMap *sourceOffsetMap + } + + // sourceOffsetMap holds the information to retrieve the original offset in the Wasm binary from the + // offset in the native binary. + sourceOffsetMap struct { + // irOperationOffsetsInNativeBinary is index-correlated with irOperationSourceOffsetsInWasmBinary, + // and maps each index (corresponding to each IR Operation) to the offset in the compiled native code. + irOperationOffsetsInNativeBinary []uint64 + // irOperationSourceOffsetsInWasmBinary is index-correlated with irOperationOffsetsInNativeBinary. + // See wazeroir.CompilationResult irOperationOffsetsInNativeBinary. + irOperationSourceOffsetsInWasmBinary []uint64 } ) @@ -716,15 +731,28 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) { // Unwinds call frames from the values stack, starting from the // current function `ce.fn`, and the current stack base pointer `ce.stackBasePointerInBytes`. fn := ce.fn + pc := uint64(ce.returnAddress) stackBasePointer := int(ce.stackBasePointerInBytes >> 3) for { - def := fn.source.Definition - builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes()) + source := fn.source + def := source.Definition - callFrameOffset := callFrameOffset(fn.source.Type) + // sourceInfo holds the source code information corresponding to the frame. + // It is not empty only when the DWARF is enabled. + var sourceInfo string + if p := fn.parent; p.codeSegment != nil { + if p.sourceOffsetMap != nil { + offset := fn.getSourceOffsetInWasmBinary(pc) + sourceInfo = wasmdebug.GetSourceInfo(p.sourceModule.DWARF, offset) + } + } + builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes(), sourceInfo) + + callFrameOffset := callFrameOffset(source.Type) if stackBasePointer != 0 { frame := *(*callFrame)(unsafe.Pointer(&ce.stack[stackBasePointer+callFrameOffset])) fn = frame.function + pc = uint64(frame.returnAddress) stackBasePointer = int(frame.returnStackBasePointerInBytes >> 3) } else { // base == 0 means that this was the last call frame stacked. break @@ -739,6 +767,36 @@ func (ce *callEngine) deferredOnCall(recovered interface{}) (err error) { return } +// getSourceOffsetInWasmBinary returns the corresponding offset in the original Wasm binary's code section +// for the given pc (which is an absolute address in the memory). +// If needPreviousInstr equals true, this returns the previous instruction's offset for the given pc. +func (f *function) getSourceOffsetInWasmBinary(pc uint64) uint64 { + srcMap := f.parent.sourceOffsetMap + if srcMap == nil { + return 0 + } + n := len(srcMap.irOperationOffsetsInNativeBinary) + 1 + + // Calculate the offset in the compiled native binary. + pcOffsetInNativeBinary := pc - uint64(f.codeInitialAddress) + + // Then, do the binary search on the list of offsets in the native binary for all the IR operations. + // This returns the index of the *next* IR operation of the one corresponding to the origin of this pc. + // See sort.Search. + index := sort.Search(n, func(i int) bool { + if i == n-1 { + return true + } + return srcMap.irOperationOffsetsInNativeBinary[i] >= pcOffsetInNativeBinary + }) + + if index == n || index == 0 { // This case, somehow pc is not found in the source offset map. + return 0 + } else { + return srcMap.irOperationSourceOffsetsInWasmBinary[index-1] + } +} + func NewEngine(ctx context.Context, enabledFeatures api.CoreFeatures) wasm.Engine { return newEngine(ctx, enabledFeatures) } @@ -983,8 +1041,21 @@ func compileWasmFunction(_ api.CoreFeatures, ir *wazeroir.CompilationResult, wit return nil, fmt.Errorf("failed to emit preamble: %w", err) } + needSourceOffsets := len(ir.IROperationSourceOffsetsInWasmBinary) > 0 + var irOpBegins []asm.Node + if needSourceOffsets { + irOpBegins = make([]asm.Node, len(ir.Operations)) + } + var skip bool - for _, op := range ir.Operations { + for i, op := range ir.Operations { + if needSourceOffsets { + // If this compilation requires source offsets for DWARF based back trace, + // we emit a NOP node at the beginning of each IR operation to get the + // binary offset of the beginning of the corresponding compiled native code. + irOpBegins[i] = compiler.compileNOP() + } + // Compiler determines whether skip the entire label. // For example, if the label doesn't have any caller, // we don't need to generate native code at all as we never reach the region. @@ -1289,5 +1360,16 @@ func compileWasmFunction(_ api.CoreFeatures, ir *wazeroir.CompilationResult, wit return nil, fmt.Errorf("failed to compile: %w", err) } - return &code{codeSegment: c, stackPointerCeil: stackPointerCeil}, nil + ret := &code{codeSegment: c, stackPointerCeil: stackPointerCeil} + if needSourceOffsets { + offsetInNativeBin := make([]uint64, len(irOpBegins)) + for i, nop := range irOpBegins { + offsetInNativeBin[i] = nop.OffsetInBinary() + } + ret.sourceOffsetMap = &sourceOffsetMap{ + irOperationSourceOffsetsInWasmBinary: ir.IROperationSourceOffsetsInWasmBinary, + irOperationOffsetsInNativeBinary: offsetInNativeBin, + } + } + return ret, nil } diff --git a/internal/engine/compiler/engine_test.go b/internal/engine/compiler/engine_test.go index a211c905..3e11d1f5 100644 --- a/internal/engine/compiler/engine_test.go +++ b/internal/engine/compiler/engine_test.go @@ -354,18 +354,27 @@ func ptrAsUint64(f *function) uint64 { } func TestCallEngine_deferredOnCall(t *testing.T) { - f1 := &function{source: &wasm.FunctionInstance{ - Definition: newMockFunctionDefinition("1"), - Type: &wasm.FunctionType{ParamNumInUint64: 2}, - }} - f2 := &function{source: &wasm.FunctionInstance{ - Definition: newMockFunctionDefinition("2"), - Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3}, - }} - f3 := &function{source: &wasm.FunctionInstance{ - Definition: newMockFunctionDefinition("3"), - Type: &wasm.FunctionType{ResultNumInUint64: 1}, - }} + f1 := &function{ + source: &wasm.FunctionInstance{ + Definition: newMockFunctionDefinition("1"), + Type: &wasm.FunctionType{ParamNumInUint64: 2}, + }, + parent: &code{sourceModule: &wasm.Module{}}, + } + f2 := &function{ + source: &wasm.FunctionInstance{ + Definition: newMockFunctionDefinition("2"), + Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3}, + }, + parent: &code{sourceModule: &wasm.Module{}}, + } + f3 := &function{ + source: &wasm.FunctionInstance{ + Definition: newMockFunctionDefinition("3"), + Type: &wasm.FunctionType{ResultNumInUint64: 1}, + }, + parent: &code{sourceModule: &wasm.Module{}}, + } ce := &callEngine{ stack: []uint64{ @@ -630,3 +639,70 @@ func (m mockListener) Before(ctx context.Context, def api.FunctionDefinition, pa func (m mockListener) After(ctx context.Context, def api.FunctionDefinition, err error, resultValues []uint64) { m.after(ctx, def, err, resultValues) } + +func TestFunction_getSourceOffsetInWasmBinary(t *testing.T) { + tests := []struct { + name string + pc, exp uint64 + codeInitialAddress uintptr + srcMap *sourceOffsetMap + }{ + {name: "source map nil", srcMap: nil}, // This can happen when this code is from compilation cache. + {name: "not found", srcMap: &sourceOffsetMap{}}, + { + name: "first IR", + pc: 4000, + codeInitialAddress: 3999, + srcMap: &sourceOffsetMap{ + irOperationOffsetsInNativeBinary: []uint64{ + 0 /*4000-3999=1 exists here*/, 5, 8, 15, + }, + irOperationSourceOffsetsInWasmBinary: []uint64{ + 10, 100, 800, 12344, + }, + }, + exp: 10, + }, + { + name: "middle", + pc: 100, + codeInitialAddress: 90, + srcMap: &sourceOffsetMap{ + irOperationOffsetsInNativeBinary: []uint64{ + 0, 5, 8 /*100-90=10 exists here*/, 15, + }, + irOperationSourceOffsetsInWasmBinary: []uint64{ + 10, 100, 800, 12344, + }, + }, + exp: 800, + }, + { + name: "last", + pc: 9999, + codeInitialAddress: 8999, + srcMap: &sourceOffsetMap{ + irOperationOffsetsInNativeBinary: []uint64{ + 0, 5, 8, 15, /*9999-8999=1000 exists here*/ + }, + irOperationSourceOffsetsInWasmBinary: []uint64{ + 10, 100, 800, 12344, + }, + }, + exp: 12344, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + f := function{ + parent: &code{sourceOffsetMap: tc.srcMap}, + codeInitialAddress: tc.codeInitialAddress, + } + + actual := f.getSourceOffsetInWasmBinary(tc.pc) + require.Equal(t, tc.exp, actual) + }) + } +} diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go index 03c01e2b..b45b0f38 100644 --- a/internal/engine/compiler/impl_amd64.go +++ b/internal/engine/compiler/impl_amd64.go @@ -77,6 +77,11 @@ func (c *amd64Compiler) String() string { return c.locationStack.String() } +// compileNOP implements compiler.compileNOP for the amd64 architecture. +func (c *amd64Compiler) compileNOP() asm.Node { + return c.assembler.CompileStandAlone(amd64.NOP) +} + type amd64Compiler struct { assembler amd64.Assembler ir *wazeroir.CompilationResult @@ -4655,15 +4660,6 @@ func (c *amd64Compiler) compileCallGoFunction(compilerStatus nativeCallStatusCod return err } - // Read the return address, and write it to callEngine.exitContext.returnAddress. - returnAddressReg, ok := c.locationStack.takeFreeRegister(registerTypeGeneralPurpose) - if !ok { - panic("BUG: cannot take free register") - } - c.assembler.CompileReadInstructionAddress(returnAddressReg, amd64.RET) - c.assembler.CompileRegisterToMemory(amd64.MOVQ, - returnAddressReg, amd64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset) - c.compileExitFromNativeCode(compilerStatus) return nil } @@ -4732,6 +4728,26 @@ func (c *amd64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) { c.assembler.CompileConstToMemory(amd64.MOVQ, int64(c.locationStack.sp), amd64ReservedRegisterForCallEngine, callEngineStackContextStackPointerOffset) + switch status { + case nativeCallStatusCodeReturned: + case nativeCallStatusCodeCallGoHostFunction, nativeCallStatusCodeCallBuiltInFunction: + // Read the return address, and write it to callEngine.exitContext.returnAddress. + returnAddressReg, ok := c.locationStack.takeFreeRegister(registerTypeGeneralPurpose) + if !ok { + panic("BUG: cannot take free register") + } + c.assembler.CompileReadInstructionAddress(returnAddressReg, amd64.RET) + c.assembler.CompileRegisterToMemory(amd64.MOVQ, + returnAddressReg, amd64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset) + default: + // This case, the execution traps, so take tmpReg and store the instruction address onto callEngine.returnAddress + // so that the stack trace can contain the top frame's source position. + tmpReg := amd64.RegR15 + c.assembler.CompileReadInstructionAddress(tmpReg, amd64.MOVQ) + c.assembler.CompileRegisterToMemory(amd64.MOVQ, + tmpReg, amd64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset) + } + c.assembler.CompileStandAlone(amd64.RET) } diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go index c58232b5..ec9035f0 100644 --- a/internal/engine/compiler/impl_arm64.go +++ b/internal/engine/compiler/impl_arm64.go @@ -91,6 +91,11 @@ func isZeroRegister(r asm.Register) bool { return r == arm64.RegRZR } +// compileNOP implements compiler.compileNOP for the arm64 architecture. +func (c *arm64Compiler) compileNOP() asm.Node { + return c.assembler.CompileStandAlone(arm64.NOP) +} + // compile implements compiler.compile for the arm64 architecture. func (c *arm64Compiler) compile() (code []byte, stackPointerCeil uint64, err error) { // c.stackPointerCeil tracks the stack pointer ceiling (max seen) value across all runtimeValueLocationStack(s) @@ -341,6 +346,25 @@ func (c *arm64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) { arm64ReservedRegisterForCallEngine, callEngineExitContextNativeCallStatusCodeOffset) } + switch status { + case nativeCallStatusCodeReturned: + case nativeCallStatusCodeCallGoHostFunction, nativeCallStatusCodeCallBuiltInFunction: + // Read the return address, and write it to callEngine.exitContext.returnAddress. + c.assembler.CompileReadInstructionAddress(arm64ReservedRegisterForTemporary, arm64.RET) + c.assembler.CompileRegisterToMemory( + arm64.STRD, arm64ReservedRegisterForTemporary, + arm64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset, + ) + default: + // This case, the execution traps, store the instruction address onto callEngine.returnAddress + // so that the stack trace can contain the top frame's source position. + c.assembler.CompileReadInstructionAddress(arm64ReservedRegisterForTemporary, arm64.STRD) + c.assembler.CompileRegisterToMemory( + arm64.STRD, arm64ReservedRegisterForTemporary, + arm64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset, + ) + } + // The return address to the Go code is stored in archContext.compilerReturnAddress which // is embedded in ce. We load the value to the tmpRegister, and then // invoke RET with that register. @@ -2770,13 +2794,6 @@ func (c *arm64Compiler) compileCallGoFunction(compilerStatus nativeCallStatusCod ) } - // Read the return address, and write it to callEngine.exitContext.returnAddress. - c.assembler.CompileReadInstructionAddress(arm64ReservedRegisterForTemporary, arm64.RET) - c.assembler.CompileRegisterToMemory( - arm64.STRD, arm64ReservedRegisterForTemporary, - arm64ReservedRegisterForCallEngine, callEngineExitContextReturnAddressOffset, - ) - c.compileExitFromNativeCode(compilerStatus) return nil } diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go index 511d9ae3..cdd5d471 100644 --- a/internal/engine/interpreter/interpreter.go +++ b/internal/engine/interpreter/interpreter.go @@ -170,6 +170,7 @@ type callFrame struct { } type code struct { + source *wasm.Module body []*interpreterOp listener experimental.FunctionListener hostFn interface{} @@ -212,11 +213,12 @@ func (c *code) instantiate(f *wasm.FunctionInstance) *function { // only relevant when in context of its kind. type interpreterOp struct { // kind determines how to interpret the other fields in this struct. - kind wazeroir.OperationKind - b1, b2 byte - b3 bool - us []uint64 - rs []*wazeroir.InclusiveRange + kind wazeroir.OperationKind + b1, b2 byte + b3 bool + us []uint64 + rs []*wazeroir.InclusiveRange + sourcePC uint64 } // interpreter mode doesn't maintain call frames in the stack, so pass the zero size to the IR. @@ -242,19 +244,19 @@ func (e *engine) CompileModule(ctx context.Context, module *wasm.Module, listene // If this is the host function, there's nothing to do as the runtime representation of // host function in interpreter is its Go function itself as opposed to Wasm functions, // which need to be compiled down to wazeroir. + var compiled *code if ir.GoFunc != nil { - funcs[i] = &code{hostFn: ir.GoFunc, listener: lsn} - continue + compiled = &code{hostFn: ir.GoFunc, listener: lsn} } else { - compiled, err := e.lowerIR(ir) + compiled, err = e.lowerIR(ir) if err != nil { def := module.FunctionDefinitionSection[uint32(i)+module.ImportFuncCount()] return fmt.Errorf("failed to lower func[%s] to wazeroir: %w", def.DebugName(), err) } compiled.listener = lsn - funcs[i] = compiled } - + compiled.source = module + funcs[i] = compiled } e.addCodes(module, funcs) return nil @@ -289,12 +291,16 @@ func (e *engine) NewModuleEngine(name string, module *wasm.Module, importedFunct // lowerIR lowers the wazeroir operations to engine friendly struct. func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) { + hasSourcePCs := len(ir.IROperationSourceOffsetsInWasmBinary) > 0 ops := ir.Operations ret := &code{} labelAddress := map[string]uint64{} onLabelAddressResolved := map[string][]func(addr uint64){} - for _, original := range ops { + for i, original := range ops { op := &interpreterOp{kind: original.Kind()} + if hasSourcePCs { + op.sourcePC = ir.IROperationSourceOffsetsInWasmBinary[i] + } switch o := original.(type) { case *wazeroir.OperationUnreachable: case *wazeroir.OperationLabel: @@ -848,7 +854,11 @@ func (ce *callEngine) recoverOnCall(v interface{}) (err error) { for i := 0; i < frameCount; i++ { frame := ce.popFrame() def := frame.f.source.Definition - builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes()) + var sourceInfo string + if frame.f.body != nil { + sourceInfo = wasmdebug.GetSourceInfo(frame.f.parent.source.DWARF, frame.f.body[frame.pc].sourcePC) + } + builder.AddFrame(def.DebugName(), def.ParamTypes(), def.ResultTypes(), sourceInfo) } err = builder.FromRecovered(v) diff --git a/internal/integration_test/bench/decoder_bench_test.go b/internal/integration_test/bench/decoder_bench_test.go index ecec8441..c2c2ed5b 100644 --- a/internal/integration_test/bench/decoder_bench_test.go +++ b/internal/integration_test/bench/decoder_bench_test.go @@ -12,7 +12,7 @@ func BenchmarkCodec(b *testing.B) { b.Run("binary.DecodeModule", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - if _, err := binary.DecodeModule(caseWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false); err != nil { + if _, err := binary.DecodeModule(caseWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, false); err != nil { b.Fatal(err) } } diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index daa16d6e..3fa17f0e 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -325,7 +325,7 @@ var spectestWasm []byte // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25 func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, enabledFeatures api.CoreFeatures) { - mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false) + mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, false) require.NoError(t, err) // (global (export "global_i32") i32 (i32.const 666)) @@ -420,7 +420,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func( case "module": buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) - mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages, false, false) + mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages, false, false, false) require.NoError(t, err, msg) require.NoError(t, mod.Validate(enabledFeatures)) mod.AssignModuleID(buf) @@ -561,7 +561,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func( // // In practice, such a module instance can be used for invoking functions without any issue. In addition, we have to // retain functions after the expected "instantiation" failure, so in wazero we choose to not raise error in that case. - mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false, false) + mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false, false, false) require.NoError(t, err, msg) err = mod.Validate(s.EnabledFeatures) @@ -590,7 +590,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func( } func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, buf []byte, msg string) { - mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false, false) + mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false, false, false) if err != nil { return } diff --git a/internal/testing/dwarftestdata/data.go b/internal/testing/dwarftestdata/data.go new file mode 100644 index 00000000..2065c2c4 --- /dev/null +++ b/internal/testing/dwarftestdata/data.go @@ -0,0 +1,6 @@ +package dwarftestdata + +import _ "embed" + +//go:embed testdata/main.wasm +var DWARFWasm []byte diff --git a/internal/testing/dwarftestdata/testdata/main.go b/internal/testing/dwarftestdata/testdata/main.go new file mode 100644 index 00000000..d809d92b --- /dev/null +++ b/internal/testing/dwarftestdata/testdata/main.go @@ -0,0 +1,20 @@ +package main + +func main() { + a() +} + +//export a +func a() { + b() +} + +//export b +func b() { + c() +} + +//export c +func c() { + panic("NOOOOOOOOOOOOOOO") +} diff --git a/internal/testing/dwarftestdata/testdata/main.wasm b/internal/testing/dwarftestdata/testdata/main.wasm new file mode 100755 index 00000000..92282900 Binary files /dev/null and b/internal/testing/dwarftestdata/testdata/main.wasm differ diff --git a/internal/wasm/binary/code.go b/internal/wasm/binary/code.go index 09511024..e1c315f8 100644 --- a/internal/wasm/binary/code.go +++ b/internal/wasm/binary/code.go @@ -10,7 +10,7 @@ import ( "github.com/tetratelabs/wazero/internal/wasm" ) -func decodeCode(r *bytes.Reader) (*wasm.Code, error) { +func decodeCode(r *bytes.Reader, codeSectionStart uint64) (*wasm.Code, error) { ss, _, err := leb128.DecodeUint32(r) if err != nil { return nil, fmt.Errorf("get the size of code: %w", err) @@ -67,6 +67,7 @@ func decodeCode(r *bytes.Reader) (*wasm.Code, error) { } } + bodyOffsetInCodeSection := codeSectionStart - uint64(r.Len()) body := make([]byte, remaining) if _, err = io.ReadFull(r, body); err != nil { return nil, fmt.Errorf("read body: %w", err) @@ -76,7 +77,7 @@ func decodeCode(r *bytes.Reader) (*wasm.Code, error) { return nil, fmt.Errorf("expr not end with OpcodeEnd") } - return &wasm.Code{Body: body, LocalTypes: localTypes}, nil + return &wasm.Code{Body: body, LocalTypes: localTypes, BodyOffsetInCodeSection: bodyOffsetInCodeSection}, nil } // encodeCode returns the wasm.Code encoded in WebAssembly 1.0 (20191205) Binary Format. diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index 04f90895..21a3230b 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -2,6 +2,7 @@ package binary import ( "bytes" + "debug/dwarf" "errors" "fmt" "io" @@ -18,7 +19,7 @@ func DecodeModule( enabledFeatures api.CoreFeatures, memoryLimitPages uint32, memoryCapacityFromMax, - storeCustomSections bool, + dwarfEnabled, storeCustomSections bool, ) (*wasm.Module, error) { r := bytes.NewReader(binary) @@ -36,6 +37,7 @@ func DecodeModule( memorySizer := newMemorySizer(memoryLimitPages, memoryCapacityFromMax) m := &wasm.Module{} + var info, line, str, abbrev, ranges []byte // For DWARF Data. for { // TODO: except custom sections, all others are required to be in order, but we aren't checking yet. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A0%E2%93%AA @@ -69,19 +71,36 @@ func DecodeModule( // Now, either decode the NameSection or CustomSection limit := sectionSize - nameSize - if name == "name" { - m.NameSection, err = decodeNameSection(r, uint64(limit)) - } else if storeCustomSections { - custom, err := decodeCustomSection(r, name, uint64(limit)) - if err != nil { - return nil, fmt.Errorf("failed to read custom section name[%s]: %w", name, err) + + var c *wasm.CustomSection + if name != "name" { + if storeCustomSections || dwarfEnabled { + c, err = decodeCustomSection(r, name, uint64(limit)) + if err != nil { + return nil, fmt.Errorf("failed to read custom section name[%s]: %w", name, err) + } + m.CustomSections = append(m.CustomSections, c) + if dwarfEnabled { + switch name { + case ".debug_info": + info = c.Data + case ".debug_line": + line = c.Data + case ".debug_str": + str = c.Data + case ".debug_abbrev": + abbrev = c.Data + case ".debug_ranges": + ranges = c.Data + } + } + } else { + if _, err = io.CopyN(io.Discard, r, int64(limit)); err != nil { + return nil, fmt.Errorf("failed to skip name[%s]: %w", name, err) + } } - m.CustomSections = append(m.CustomSections, custom) } else { - // Note: Not Seek because it doesn't err when given an offset past EOF. Rather, it leads to undefined state. - if _, err = io.CopyN(io.Discard, r, int64(limit)); err != nil { - return nil, fmt.Errorf("failed to skip name[%s]: %w", name, err) - } + m.NameSection, err = decodeNameSection(r, uint64(limit)) } case wasm.SectionIDType: m.TypeSection, err = decodeTypeSection(enabledFeatures, r) @@ -131,6 +150,10 @@ func DecodeModule( } } + if dwarfEnabled { + m.DWARF, _ = dwarf.New(abbrev, nil, nil, info, line, nil, ranges, str) + } + functionCount, codeCount := m.SectionElementCount(wasm.SectionIDFunction), m.SectionElementCount(wasm.SectionIDCode) if functionCount != codeCount { return nil, fmt.Errorf("function and code section have inconsistent lengths: %d != %d", functionCount, codeCount) diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go index 2af370a4..17ef12a0 100644 --- a/internal/wasm/binary/decoder_test.go +++ b/internal/wasm/binary/decoder_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/dwarftestdata" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" ) @@ -81,7 +82,7 @@ func TestDecodeModule(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false) + m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false) require.NoError(t, e) require.Equal(t, tc.input, m) }) @@ -92,7 +93,7 @@ func TestDecodeModule(t *testing.T) { wasm.SectionIDCustom, 0xf, // 15 bytes in this section 0x04, 'm', 'e', 'm', 'e', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false) + m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false) require.NoError(t, e) require.Equal(t, &wasm.Module{}, m) }) @@ -102,7 +103,7 @@ func TestDecodeModule(t *testing.T) { wasm.SectionIDCustom, 0xf, // 15 bytes in this section 0x04, 'm', 'e', 'm', 'e', 1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - m, e := DecodeModule(input, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, true) + m, e := DecodeModule(input, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, true) require.NoError(t, e) require.Equal(t, &wasm.Module{ CustomSections: []*wasm.CustomSection{ @@ -124,7 +125,7 @@ func TestDecodeModule(t *testing.T) { subsectionIDModuleName, 0x07, // 7 bytes in this subsection 0x06, // the Module name simple is 6 bytes long 's', 'i', 'm', 'p', 'l', 'e') - m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false) + m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false) require.NoError(t, e) require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m) }) @@ -139,7 +140,7 @@ func TestDecodeModule(t *testing.T) { subsectionIDModuleName, 0x07, // 7 bytes in this subsection 0x06, // the Module name simple is 6 bytes long 's', 'i', 'm', 'p', 'l', 'e') - m, e := DecodeModule(input, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, true) + m, e := DecodeModule(input, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, true) require.NoError(t, e) require.Equal(t, &wasm.Module{ NameSection: &wasm.NameSection{ModuleName: "simple"}, @@ -152,10 +153,22 @@ func TestDecodeModule(t *testing.T) { }, m) }) + t.Run("DWARF enabled", func(t *testing.T) { + m, err := DecodeModule(dwarftestdata.DWARFWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, true, true) + require.NoError(t, err) + require.NotNil(t, m.DWARF) + }) + + t.Run("DWARF disabled", func(t *testing.T) { + m, err := DecodeModule(dwarftestdata.DWARFWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, false, true) + require.NoError(t, err) + require.Nil(t, m.DWARF) + }) + t.Run("data count section disabled", func(t *testing.T) { input := append(append(Magic, version...), wasm.SectionIDDataCount, 1, 0) - _, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false) + _, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false) require.EqualError(t, e, `data count section not supported as feature "bulk-memory-operations" is disabled`) }) } @@ -205,7 +218,7 @@ func TestDecodeModule_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, e := DecodeModule(tc.input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false) + _, e := DecodeModule(tc.input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false, false, false) require.EqualError(t, e, tc.expectedErr) }) } diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index aec8f7e9..d0a87cbc 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -163,6 +163,7 @@ func decodeElementSection(r *bytes.Reader, enabledFeatures api.CoreFeatures) ([] } func decodeCodeSection(r *bytes.Reader) ([]*wasm.Code, error) { + codeSectionStart := uint64(r.Len()) vs, _, err := leb128.DecodeUint32(r) if err != nil { return nil, fmt.Errorf("get size of vector: %w", err) @@ -170,9 +171,11 @@ func decodeCodeSection(r *bytes.Reader) ([]*wasm.Code, error) { result := make([]*wasm.Code, vs) for i := uint32(0); i < vs; i++ { - if result[i], err = decodeCode(r); err != nil { + c, err := decodeCode(r, codeSectionStart) + if err != nil { return nil, fmt.Errorf("read %d-th code segment: %v", i, err) } + result[i] = c } return result, nil } diff --git a/internal/wasm/module.go b/internal/wasm/module.go index dccf42f2..8665cc90 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -3,6 +3,7 @@ package wasm import ( "bytes" "crypto/sha256" + "debug/dwarf" "errors" "fmt" "io" @@ -184,6 +185,11 @@ type Module struct { // MemoryDefinitionSection is a wazero-specific section built on Validate. MemoryDefinitionSection []*MemoryDefinition + + // DWARF is the DWARF data used for DWARF base stack trace. This is created from the multiple custom sections + // as described in https://yurydelendik.github.io/webassembly-dwarf/, though it is not specified in the Wasm + // specification: https://github.com/WebAssembly/debugging/issues/1 + DWARF *dwarf.Data } // ModuleID represents sha256 hash value uniquely assigned to Module. @@ -831,6 +837,10 @@ type Code struct { // Note: This has no serialization format, so is not encodable. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#host-functions%E2%91%A2 GoFunc interface{} + + // BodyOffsetInCodeSection is the offset of the beginning of the body in the code section. + // This is used for DWARF based stack trace where a program counter represents an offset in code section. + BodyOffsetInCodeSection uint64 } type DataSegment struct { diff --git a/internal/wasmdebug/debug.go b/internal/wasmdebug/debug.go index e6859861..c2ba3e94 100644 --- a/internal/wasmdebug/debug.go +++ b/internal/wasmdebug/debug.go @@ -95,9 +95,10 @@ type ErrorBuilder interface { // * funcName should be from FuncName // * paramTypes should be from wasm.FunctionType // * resultTypes should be from wasm.FunctionType + // * sourceInfo is the source code information for this frame and can be empty. // // Note: paramTypes and resultTypes are present because signature misunderstanding, mismatch or overflow are common. - AddFrame(funcName string, paramTypes, resultTypes []api.ValueType) + AddFrame(funcName string, paramTypes, resultTypes []api.ValueType, sourceInfo string) // FromRecovered returns an error with the wasm stack trace appended to it. FromRecovered(recovered interface{}) error @@ -143,8 +144,10 @@ func (s *stackTrace) FromRecovered(recovered interface{}) error { } // AddFrame implements ErrorBuilder.Format -func (s *stackTrace) AddFrame(funcName string, paramTypes, resultTypes []api.ValueType) { - // Format as best as we can, considering we don't yet have source and line numbers, - // TODO: include DWARF symbols. See #58 - s.frames = append(s.frames, signature(funcName, paramTypes, resultTypes)) +func (s *stackTrace) AddFrame(funcName string, paramTypes, resultTypes []api.ValueType, sourceInfo string) { + sig := signature(funcName, paramTypes, resultTypes) + s.frames = append(s.frames, sig) + if sourceInfo != "" { + s.frames = append(s.frames, "\t"+sourceInfo) + } } diff --git a/internal/wasmdebug/debug_test.go b/internal/wasmdebug/debug_test.go index c543a27f..e0b58c7e 100644 --- a/internal/wasmdebug/debug_test.go +++ b/internal/wasmdebug/debug_test.go @@ -79,7 +79,7 @@ func TestErrorBuilder(t *testing.T) { { name: "one", build: func(builder ErrorBuilder) error { - builder.AddFrame("x.y", nil, nil) + builder.AddFrame("x.y", nil, nil, "") return builder.FromRecovered(argErr) }, expectedErr: `invalid argument (recovered by wazero) @@ -90,8 +90,8 @@ wasm stack trace: { name: "two", build: func(builder ErrorBuilder) error { - builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}) - builder.AddFrame("x.y", nil, nil) + builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}, "") + builder.AddFrame("x.y", nil, nil, "") return builder.FromRecovered(argErr) }, expectedErr: `invalid argument (recovered by wazero) @@ -103,8 +103,8 @@ wasm stack trace: { name: "runtime.Error", build: func(builder ErrorBuilder) error { - builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}) - builder.AddFrame("x.y", nil, nil) + builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}, "") + builder.AddFrame("x.y", nil, nil, "") return builder.FromRecovered(rteErr) }, expectedErr: `index out of bounds (recovered by wazero) @@ -116,13 +116,14 @@ wasm stack trace: { name: "wasmruntime.Error", build: func(builder ErrorBuilder) error { - builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}) - builder.AddFrame("x.y", nil, nil) + builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}, "/opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_tinygowasm.go:73:6") + builder.AddFrame("x.y", nil, nil, "") return builder.FromRecovered(wasmruntime.ErrRuntimeStackOverflow) }, expectedErr: `wasm error: stack overflow wasm stack trace: wasi_snapshot_preview1.fd_write(i32,i32,i32,i32) i32 + /opt/homebrew/Cellar/tinygo/0.26.0/src/runtime/runtime_tinygowasm.go:73:6 x.y()`, expectUnwrap: wasmruntime.ErrRuntimeStackOverflow, }, diff --git a/internal/wasmdebug/dwarf.go b/internal/wasmdebug/dwarf.go new file mode 100644 index 00000000..ad927366 --- /dev/null +++ b/internal/wasmdebug/dwarf.go @@ -0,0 +1,32 @@ +package wasmdebug + +import ( + "debug/dwarf" + "fmt" +) + +// GetSourceInfo returns the source information for the given instructionOffset which is an offset in +// the code section of the original Wasm binary. Returns empty string if the info is not found. +func GetSourceInfo(d *dwarf.Data, instructionOffset uint64) string { + if d == nil { + return "" + } + + r := d.Reader() + entry, err := r.SeekPC(instructionOffset) + if err != nil { + return "" + } + + lineReader, err := d.LineReader(entry) + if err != nil { + return "" + } + + var le dwarf.LineEntry + err = lineReader.SeekPC(instructionOffset, &le) + if err != nil { + return "" + } + return fmt.Sprintf("%#x: %s:%d:%d", le.Address, le.File.Name, le.Line, le.Column) +} diff --git a/internal/wasmdebug/dwarf_test.go b/internal/wasmdebug/dwarf_test.go new file mode 100644 index 00000000..1b52692d --- /dev/null +++ b/internal/wasmdebug/dwarf_test.go @@ -0,0 +1,51 @@ +package wasmdebug_test + +import ( + "math" + "testing" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/testing/dwarftestdata" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" + "github.com/tetratelabs/wazero/internal/wasm/binary" + "github.com/tetratelabs/wazero/internal/wasmdebug" +) + +func TestGetSourceInfo(t *testing.T) { + mod, err := binary.DecodeModule(dwarftestdata.DWARFWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false, true, false) + require.NoError(t, err) + require.NotNil(t, mod.DWARF) + + // Get the offsets of functions named "a", "b" and "c" in dwarftestdata.DWARFWasm. + var a, b, c uint64 + for _, exp := range mod.ExportSection { + switch exp.Name { + case "a": + a = mod.CodeSection[exp.Index-mod.ImportFuncCount()].BodyOffsetInCodeSection + case "b": + b = mod.CodeSection[exp.Index-mod.ImportFuncCount()].BodyOffsetInCodeSection + case "c": + c = mod.CodeSection[exp.Index-mod.ImportFuncCount()].BodyOffsetInCodeSection + } + } + + tests := []struct { + offset uint64 + exp string + }{ + // Unknown offset returns empty string. + {offset: math.MaxUint64, exp: ""}, + // The first instruction should point to the first line of each function in internal/testing/dwarftestdata/testdata/main.go + {offset: a, exp: "wazero/internal/testing/dwarftestdata/testdata/main.go:9:3"}, + {offset: b, exp: "wazero/internal/testing/dwarftestdata/testdata/main.go:14:3"}, + {offset: c, exp: "wazero/internal/testing/dwarftestdata/testdata/main.go:19:7"}, + } + + for _, tc := range tests { + t.Run(tc.exp, func(t *testing.T) { + actual := wasmdebug.GetSourceInfo(mod.DWARF, tc.offset) + require.Contains(t, actual, tc.exp) + }) + } +} diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go index d7634668..3c401893 100644 --- a/internal/wazeroir/compiler.go +++ b/internal/wazeroir/compiler.go @@ -163,8 +163,8 @@ type compiler struct { on bool depth int } - pc uint64 - result CompilationResult + pc, currentOpPC uint64 + result CompilationResult // body holds the code for the function's body where Wasm instructions are stored. body []byte @@ -182,6 +182,11 @@ type compiler struct { funcs []uint32 // globals holds the global types for all declard globas in the module where the targe function exists. globals []*wasm.GlobalType + + // needSourceOffset is true if this module requires DWARF based stack trace. + needSourceOffset bool + // bodyOffsetInCodeSection is the offset of the body of this function in the original Wasm binary's code section. + bodyOffsetInCodeSection uint64 } //lint:ignore U1000 for debugging only. @@ -213,6 +218,11 @@ type CompilationResult struct { // Operations holds wazeroir operations compiled from Wasm instructions in a Wasm function. Operations []Operation + // IROperationSourceOffsetsInWasmBinary is index-correlated with Operation and maps each operation to the corresponding source instruction's + // offset in the original WebAssembly binary. + // Non nil only when the given Wasm module has the DWARF section. + IROperationSourceOffsetsInWasmBinary []uint64 + // LabelCallers maps Label.String() to the number of callers to that label. // Here "callers" means that the call-sites which jumps to the label with br, br_if or br_table // instructions. @@ -249,7 +259,7 @@ type CompilationResult struct { HasElementInstances bool } -func CompileFunctions(_ context.Context, enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 int, module *wasm.Module) ([]*CompilationResult, error) { +func CompileFunctions(ctx context.Context, enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 int, module *wasm.Module) ([]*CompilationResult, error) { functions, globals, mem, tables, err := module.AllDeclarations() if err != nil { return nil, err @@ -286,7 +296,8 @@ func CompileFunctions(_ context.Context, enabledFeatures api.CoreFeatures, callF } continue } - r, err := compile(enabledFeatures, callFrameStackSizeInUint64, sig, code.Body, code.LocalTypes, module.TypeSection, functions, globals) + r, err := compile(enabledFeatures, callFrameStackSizeInUint64, sig, code.Body, + code.LocalTypes, module.TypeSection, functions, globals, code.BodyOffsetInCodeSection, module.DWARF != nil) if err != nil { def := module.FunctionDefinitionSection[uint32(funcIndex)+module.ImportFuncCount()] return nil, fmt.Errorf("failed to lower func[%s] to wazeroir: %w", def.DebugName(), err) @@ -316,6 +327,8 @@ func compile(enabledFeatures api.CoreFeatures, localTypes []wasm.ValueType, types []*wasm.FunctionType, functions []uint32, globals []*wasm.GlobalType, + bodyOffsetInCodeSection uint64, + needSourceOffset bool, ) (*CompilationResult, error) { c := compiler{ enabledFeatures: enabledFeatures, @@ -328,6 +341,8 @@ func compile(enabledFeatures api.CoreFeatures, globals: globals, funcs: functions, types: types, + needSourceOffset: needSourceOffset, + bodyOffsetInCodeSection: bodyOffsetInCodeSection, } c.initializeStack() @@ -360,6 +375,7 @@ func compile(enabledFeatures api.CoreFeatures, // and emit the results into c.results. func (c *compiler) handleInstruction() error { op := c.body[c.pc] + c.currentOpPC = c.pc if false { var instName string if op == wasm.OpcodeVecPrefix { @@ -3032,6 +3048,10 @@ func (c *compiler) emit(ops ...Operation) { } } c.result.Operations = append(c.result.Operations, op) + if c.needSourceOffset { + c.result.IROperationSourceOffsetsInWasmBinary = append(c.result.IROperationSourceOffsetsInWasmBinary, + c.currentOpPC+c.bodyOffsetInCodeSection) + } if false { fmt.Printf("emitting ") formatOperation(os.Stdout, op) diff --git a/runtime.go b/runtime.go index 91878126..4c217663 100644 --- a/runtime.go +++ b/runtime.go @@ -172,7 +172,9 @@ func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledMod return nil, errors.New("invalid binary") } - internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, r.memoryLimitPages, r.memoryCapacityFromMax, false) + dwarfEnabled := experimentalapi.DWARFBasedStackTraceEnabled(ctx) + internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, + r.memoryLimitPages, r.memoryCapacityFromMax, dwarfEnabled, false) if err != nil { return nil, err } else if err = internal.Validate(r.enabledFeatures); err != nil {