Adds support for DWARF based stack traces (#881)
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
2
Makefile
2
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
|
||||
|
||||
47
experimental/dwarf.go
Normal file
47
experimental/dwarf.go
Normal 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
|
||||
}
|
||||
61
experimental/dwarf_test.go
Normal file
61
experimental/dwarf_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
6
internal/testing/dwarftestdata/data.go
Normal file
6
internal/testing/dwarftestdata/data.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package dwarftestdata
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed testdata/main.wasm
|
||||
var DWARFWasm []byte
|
||||
20
internal/testing/dwarftestdata/testdata/main.go
vendored
Normal file
20
internal/testing/dwarftestdata/testdata/main.go
vendored
Normal 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")
|
||||
}
|
||||
BIN
internal/testing/dwarftestdata/testdata/main.wasm
vendored
Executable file
BIN
internal/testing/dwarftestdata/testdata/main.wasm
vendored
Executable file
Binary file not shown.
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
32
internal/wasmdebug/dwarf.go
Normal file
32
internal/wasmdebug/dwarf.go
Normal 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)
|
||||
}
|
||||
51
internal/wasmdebug/dwarf_test.go
Normal file
51
internal/wasmdebug/dwarf_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user