Adds support for DWARF based stack traces (#881)

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Takeshi Yoneda
2022-12-05 14:59:45 +09:00
committed by GitHub
parent 6f30a42828
commit 6c4dd1cfd9
25 changed files with 588 additions and 89 deletions

View File

@@ -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

47
experimental/dwarf.go Normal file
View File

@@ -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
}

View File

@@ -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")
})
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -354,18 +354,27 @@ func ptrAsUint64(f *function) uint64 {
}
func TestCallEngine_deferredOnCall(t *testing.T) {
f1 := &function{source: &wasm.FunctionInstance{
f1 := &function{
source: &wasm.FunctionInstance{
Definition: newMockFunctionDefinition("1"),
Type: &wasm.FunctionType{ParamNumInUint64: 2},
}}
f2 := &function{source: &wasm.FunctionInstance{
},
parent: &code{sourceModule: &wasm.Module{}},
}
f2 := &function{
source: &wasm.FunctionInstance{
Definition: newMockFunctionDefinition("2"),
Type: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3},
}}
f3 := &function{source: &wasm.FunctionInstance{
},
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)
})
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -170,6 +170,7 @@ type callFrame struct {
}
type code struct {
source *wasm.Module
body []*interpreterOp
listener experimental.FunctionListener
hostFn interface{}
@@ -217,6 +218,7 @@ type interpreterOp struct {
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)

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
package dwarftestdata
import _ "embed"
//go:embed testdata/main.wasm
var DWARFWasm []byte

View File

@@ -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")
}

Binary file not shown.

View File

@@ -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.

View File

@@ -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,20 +71,37 @@ 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))
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, custom)
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 {
// 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)
}
}
} else {
m.NameSection, err = decodeNameSection(r, uint64(limit))
}
case wasm.SectionIDType:
m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
case wasm.SectionIDImport:
@@ -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)

View File

@@ -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)
})
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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,
},

View File

@@ -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)
}

View File

@@ -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)
})
}
}

View File

@@ -163,7 +163,7 @@ type compiler struct {
on bool
depth int
}
pc uint64
pc, currentOpPC uint64
result CompilationResult
// body holds the code for the function's body where Wasm instructions are stored.
@@ -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)

View File

@@ -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 {