From 6c4dd1cfd9d798d1c491cf94834594bc3bfbf5da Mon Sep 17 00:00:00 2001 From: Takeshi Yoneda Date: Mon, 5 Dec 2022 14:59:45 +0900 Subject: [PATCH] Adds support for DWARF based stack traces (#881) Signed-off-by: Takeshi Yoneda --- Makefile | 2 + experimental/dwarf.go | 47 ++++++++ experimental/dwarf_test.go | 61 +++++++++++ internal/engine/compiler/compiler.go | 3 + internal/engine/compiler/engine.go | 92 +++++++++++++++- internal/engine/compiler/engine_test.go | 100 +++++++++++++++--- internal/engine/compiler/impl_amd64.go | 34 ++++-- internal/engine/compiler/impl_arm64.go | 31 ++++-- internal/engine/interpreter/interpreter.go | 34 +++--- .../bench/decoder_bench_test.go | 2 +- .../integration_test/spectest/spectest.go | 8 +- internal/testing/dwarftestdata/data.go | 6 ++ .../testing/dwarftestdata/testdata/main.go | 20 ++++ .../testing/dwarftestdata/testdata/main.wasm | Bin 0 -> 40710 bytes internal/wasm/binary/code.go | 5 +- internal/wasm/binary/decoder.go | 47 +++++--- internal/wasm/binary/decoder_test.go | 27 +++-- internal/wasm/binary/section.go | 5 +- internal/wasm/module.go | 10 ++ internal/wasmdebug/debug.go | 13 ++- internal/wasmdebug/debug_test.go | 15 +-- internal/wasmdebug/dwarf.go | 32 ++++++ internal/wasmdebug/dwarf_test.go | 51 +++++++++ internal/wazeroir/compiler.go | 28 ++++- runtime.go | 4 +- 25 files changed, 588 insertions(+), 89 deletions(-) create mode 100644 experimental/dwarf.go create mode 100644 experimental/dwarf_test.go create mode 100644 internal/testing/dwarftestdata/data.go create mode 100644 internal/testing/dwarftestdata/testdata/main.go create mode 100755 internal/testing/dwarftestdata/testdata/main.wasm create mode 100644 internal/wasmdebug/dwarf.go create mode 100644 internal/wasmdebug/dwarf_test.go 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 0000000000000000000000000000000000000000..92282900f76a9cef42350149ca5d58f0562a9631 GIT binary patch literal 40710 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 literal 0 HcmV?d00001 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 {