From f0a32c8e1b6571c8c1cd4ed89dcb9c784481f422 Mon Sep 17 00:00:00 2001 From: Alessandro Arzilli Date: Thu, 8 Jul 2021 17:47:53 +0200 Subject: [PATCH] Go 1.17 support branch (#2451) * proc: support new Go 1.17 panic/defer mechanism Go 1.17 will create wrappers for deferred calls that take arguments. Change defer reading code so that wrappers are automatically unwrapped. Also the deferred function is called directly by runtime.gopanic, without going through runtime.callN which means that sometimes when a panic happens the stack is either: 0. deferred function call 1. deferred call wrapper 2. runtime.gopanic or: 0. deferred function call 1. runtime.gopanic instead of always being: 0. deferred function call 1. runtime.callN 2. runtime.gopanic the isPanicCall check is changed accordingly. * test: miscellaneous minor test fixes for Go 1.17 * proc: resolve inlined calls when stepping out of runtime.breakpoint Calls to runtime.Breakpoint are inlined in Go 1.17 when inlining is enabled, resolve inlined calls in stepInstructionOut. * proc: add support for debugCallV2 with regabi This change adds support for the new debug call protocol which had to change for the new register ABI introduced in Go 1.17. Summary of changes: - Abstracts over the debug call version depending on the Go version found in the binary. - Uses R12 instead of RAX as the debug protocol register when the binary is from Go 1.17 or later. - Creates a variable directly from the DWARF entry for function arguments to support passing arguments however the ABI expects. - Computes a very conservative stack frame size for the call when injecting a call into a Go process whose version is >=1.17. Co-authored-by: Michael Anthony Knyszek Co-authored-by: Alessandro Arzilli * TeamCity: enable tests on go-tip * goversion: version compatibility bump * TeamCity: fix go-tip builds on macOS/arm64 Co-authored-by: Michael Anthony Knyszek --- _fixtures/fncall.go | 10 +- _scripts/test_linux.sh | 2 +- _scripts/test_mac.sh | 11 +- _scripts/test_windows.ps1 | 2 +- pkg/goversion/compat.go | 2 +- pkg/proc/bininfo.go | 38 +++- pkg/proc/breakpoints.go | 23 ++- pkg/proc/fncall.go | 299 +++++++++++++++++++++++------- pkg/proc/proc_general_test.go | 14 ++ pkg/proc/proc_test.go | 97 +++++++--- pkg/proc/stack.go | 31 +++- pkg/proc/target.go | 24 +++ pkg/proc/target_exec.go | 34 +++- pkg/proc/variables.go | 15 +- service/api/conversions.go | 8 +- service/dap/server.go | 2 +- service/dap/server_test.go | 29 ++- service/debugger/debugger.go | 24 ++- service/rpc1/server.go | 2 +- service/rpc2/server.go | 2 +- service/test/integration2_test.go | 11 -- service/test/variables_test.go | 16 +- 22 files changed, 532 insertions(+), 164 deletions(-) diff --git a/_fixtures/fncall.go b/_fixtures/fncall.go index 949e4010..6967e102 100644 --- a/_fixtures/fncall.go +++ b/_fixtures/fncall.go @@ -168,6 +168,14 @@ func (_ X2) CallMe(i int) int { return i * i } +func regabistacktest(s1, s2, s3, s4, s5 string, n uint8) (string, string, string, string, string, uint8) { + return s1 + s2, s2 + s3, s3 + s4, s4 + s5, s5 + s1, 2 * n +} + +func regabistacktest2(n1, n2, n3, n4, n5, n6, n7, n8, n9, n10 int) (int, int, int, int, int, int, int, int, int, int) { + return n1 + n2, n2 + n3, n3 + n4, n4 + n5, n5 + n6, n6 + n7, n7 + n8, n8 + n9, n9 + n10, n10 + n1 +} + func main() { one, two := 1, 2 intslice := []int{1, 2, 3} @@ -200,5 +208,5 @@ func main() { d.Method() d.Base.Method() x.CallMe() - fmt.Println(one, two, zero, call, call0, call2, callexit, callpanic, callbreak, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2, square, intcallpanic, onetwothree, curriedAdd, getAStruct, getAStructPtr, getVRcvrableFromAStruct, getPRcvrableFromAStructPtr, getVRcvrableFromAStructPtr, pa2, noreturncall, str, d, x, x2.CallMe(5), longstrs) + fmt.Println(one, two, zero, call, call0, call2, callexit, callpanic, callbreak, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2, square, intcallpanic, onetwothree, curriedAdd, getAStruct, getAStructPtr, getVRcvrableFromAStruct, getPRcvrableFromAStructPtr, getVRcvrableFromAStructPtr, pa2, noreturncall, str, d, x, x2.CallMe(5), longstrs, regabistacktest, regabistacktest2) } diff --git a/_scripts/test_linux.sh b/_scripts/test_linux.sh index 2125af5f..345daf3a 100755 --- a/_scripts/test_linux.sh +++ b/_scripts/test_linux.sh @@ -20,7 +20,7 @@ function getgo { } if [ "$version" = "gotip" ]; then - exit 0 + #exit 0 echo Building Go from tip getgo $(curl https://golang.org/VERSION?m=text) export GOROOT_BOOTSTRAP=$GOROOT diff --git a/_scripts/test_mac.sh b/_scripts/test_mac.sh index 9ca17d89..4a65f7e3 100644 --- a/_scripts/test_mac.sh +++ b/_scripts/test_mac.sh @@ -8,10 +8,17 @@ ARCH=$2 TMPDIR=$3 if [ "$GOVERSION" = "gotip" ]; then - exit 0 + #exit 0 bootstrapver=$(curl https://golang.org/VERSION?m=text) + cd $TMPDIR curl -sSL "https://storage.googleapis.com/golang/$bootstrapver.darwin-$ARCH.tar.gz" | tar -xz - git clone https://go.googlesource.com/go $TMPDIR/go-tip + cd - + if [ -x $TMPDIR/go-tip ]; then + cd $TMPDIR/go-tip + git pull origin + else + git clone https://go.googlesource.com/go $TMPDIR/go-tip + fi export GOROOT_BOOTSTRAP=$TMPDIR/go export GOROOT=$TMPDIR/go-tip cd $TMPDIR/go-tip/src diff --git a/_scripts/test_windows.ps1 b/_scripts/test_windows.ps1 index 7d814b67..7c4b45d3 100644 --- a/_scripts/test_windows.ps1 +++ b/_scripts/test_windows.ps1 @@ -32,7 +32,7 @@ function GetGo($version) { } if ($version -eq "gotip") { - Exit 0 + #Exit 0 $latest = Invoke-WebRequest -Uri https://golang.org/VERSION?m=text -UseBasicParsing | Select-Object -ExpandProperty Content GetGo $latest $env:GOROOT_BOOTSTRAP = $env:GOROOT diff --git a/pkg/goversion/compat.go b/pkg/goversion/compat.go index f800b38c..62c82c1c 100644 --- a/pkg/goversion/compat.go +++ b/pkg/goversion/compat.go @@ -8,7 +8,7 @@ var ( MinSupportedVersionOfGoMajor = 1 MinSupportedVersionOfGoMinor = 14 MaxSupportedVersionOfGoMajor = 1 - MaxSupportedVersionOfGoMinor = 16 + MaxSupportedVersionOfGoMinor = 17 goTooOldErr = fmt.Errorf("Version of Go is too old for this version of Delve (minimum supported version %d.%d, suppress this error with --check-go-version=false)", MinSupportedVersionOfGoMajor, MinSupportedVersionOfGoMinor) dlvTooOldErr = fmt.Errorf("Version of Delve is too old for this version of Go (maximum supported version %d.%d, suppress this error with --check-go-version=false)", MaxSupportedVersionOfGoMajor, MaxSupportedVersionOfGoMinor) ) diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index a7e0ea69..c2b1a5cc 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -100,6 +100,12 @@ type BinaryInfo struct { // function starts. inlinedCallLines map[fileLine][]uint64 + // dwrapUnwrapCache caches unwrapping of defer wrapper functions (dwrap) + dwrapUnwrapCache map[uint64]*Function + + // Go 1.17 register ABI is enabled. + regabi bool + logger *logrus.Entry } @@ -656,7 +662,8 @@ type Image struct { compileUnits []*compileUnit // compileUnits is sorted by increasing DWARF offset - dwarfTreeCache *simplelru.LRU + dwarfTreeCache *simplelru.LRU + runtimeMallocgcTree *godwarf.Tree // patched version of runtime.mallocgc's DIE // runtimeTypeToDIE maps between the offset of a runtime._type in // runtime.moduledata.types and the offset of the DIE in debug_info. This @@ -782,6 +789,9 @@ func (image *Image) LoadError() error { } func (image *Image) getDwarfTree(off dwarf.Offset) (*godwarf.Tree, error) { + if image.runtimeMallocgcTree != nil && off == image.runtimeMallocgcTree.Offset { + return image.runtimeMallocgcTree, nil + } if r, ok := image.dwarfTreeCache.Get(off); ok { return r.(*godwarf.Tree), nil } @@ -1681,6 +1691,9 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugInfoBytes, debugLineB if bi.inlinedCallLines == nil { bi.inlinedCallLines = make(map[fileLine][]uint64) } + if bi.dwrapUnwrapCache == nil { + bi.dwrapUnwrapCache = make(map[uint64]*Function) + } image.runtimeTypeToDIE = make(map[uint64]runtimeTypeDIE) @@ -1735,6 +1748,13 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugInfoBytes, debugLineB cu.optimized = goversion.ProducerAfterOrEqual(cu.producer, 1, 10) } else { cu.optimized = !strings.Contains(cu.producer[semicolon:], "-N") || !strings.Contains(cu.producer[semicolon:], "-l") + const regabi = " regabi" + if i := strings.Index(cu.producer[semicolon:], regabi); i > 0 { + i += semicolon + if i+len(regabi) >= len(cu.producer) || cu.producer[i+len(regabi)] == ' ' { + bi.regabi = true + } + } cu.producer = cu.producer[:semicolon] } } @@ -1775,6 +1795,22 @@ func (bi *BinaryInfo) loadDebugInfoMaps(image *Image, debugInfoBytes, debugLineB sort.Strings(bi.Sources) bi.Sources = uniq(bi.Sources) + if bi.regabi { + // prepare patch for runtime.mallocgc's DIE + fn := bi.LookupFunc["runtime.mallocgc"] + if fn != nil { + tree, err := image.getDwarfTree(fn.offset) + if err == nil { + tree.Children, err = regabiMallocgcWorkaround(bi) + if err != nil { + bi.logger.Errorf("could not patch runtime.mallogc: %v", err) + } else { + image.runtimeMallocgcTree = tree + } + } + } + } + if cont != nil { cont() } diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go index 761e6e2a..aed2c49b 100644 --- a/pkg/proc/breakpoints.go +++ b/pkg/proc/breakpoints.go @@ -194,7 +194,7 @@ func (bpstate *BreakpointState) checkCond(thread Thread) { var err error frames, err := ThreadStacktrace(thread, 2) if err == nil { - nextDeferOk = isPanicCall(frames) + nextDeferOk, _ = isPanicCall(frames) if !nextDeferOk { nextDeferOk, _ = isDeferReturnCall(frames, bpstate.DeferReturns) } @@ -239,8 +239,25 @@ func (bpstate *BreakpointState) checkHitCond(thread Thread) { } } -func isPanicCall(frames []Stackframe) bool { - return len(frames) >= 3 && frames[2].Current.Fn != nil && frames[2].Current.Fn.Name == "runtime.gopanic" +func isPanicCall(frames []Stackframe) (bool, int) { + // In Go prior to 1.17 the call stack for a panic is: + // 0. deferred function call + // 1. runtime.callN + // 2. runtime.gopanic + // in Go after 1.17 it is either: + // 0. deferred function call + // 1. deferred call wrapper + // 2. runtime.gopanic + // or: + // 0. deferred function call + // 1. runtime.gopanic + if len(frames) >= 3 && frames[2].Current.Fn != nil && frames[2].Current.Fn.Name == "runtime.gopanic" { + return true, 2 + } + if len(frames) >= 2 && frames[1].Current.Fn != nil && frames[1].Current.Fn.Name == "runtime.gopanic" { + return true, 1 + } + return false, 0 } func isDeferReturnCall(frames []Stackframe, deferReturns []uint64) (bool, uint64) { diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index 2e4fdf15..b17a0694 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -42,8 +42,12 @@ import ( const ( debugCallFunctionNamePrefix1 = "debugCall" debugCallFunctionNamePrefix2 = "runtime.debugCall" - debugCallFunctionName = "runtime.debugCallV1" + maxDebugCallVersion = 2 maxArgFrameSize = 65535 + + // maxRegArgBytes is extra padding for ABI1 call injections, equivalent to + // the maximum space occupied by register arguments. + maxRegArgBytes = 9*8 + 15*8 // TODO: Make this generic for other platforms. ) var ( @@ -163,7 +167,7 @@ func EvalExpressionWithCalls(t *Target, g *G, expr string, retLoadCfg LoadConfig return errFuncCallInProgress } - dbgcallfn := bi.LookupFunc[debugCallFunctionName] + dbgcallfn, _ := debugCallFunction(bi) if dbgcallfn == nil { return errFuncCallUnsupported } @@ -272,7 +276,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { return nil, errFuncCallUnsupportedBackend } - dbgcallfn := bi.LookupFunc[debugCallFunctionName] + dbgcallfn, dbgcallversion := debugCallFunction(bi) if dbgcallfn == nil { return nil, errFuncCallUnsupported } @@ -289,7 +293,11 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { if regs.SP()-256 <= stacklo { return nil, errNotEnoughStack } - if bi.Arch.RegistersToDwarfRegisters(0, regs).Reg(regnum.AMD64_Rax) == nil { //TODO(aarzilli): make this generic when call injection is supported on other architectures + protocolReg, ok := debugCallProtocolReg(dbgcallversion) + if !ok { + return nil, errFuncCallUnsupported + } + if bi.Arch.RegistersToDwarfRegisters(0, regs).Reg(protocolReg) == nil { return nil, errFuncCallUnsupportedBackend } @@ -347,7 +355,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) { scope.Regs.FrameBase = fboff + int64(scope.g.stack.hi) scope.Regs.CFA = scope.frameOffset + int64(scope.g.stack.hi) - finished := funcCallStep(scope, &fncall, g.Thread) + finished := funcCallStep(scope, &fncall, g.Thread, protocolReg, dbgcallfn.Name) if finished { break } @@ -498,22 +506,23 @@ func funcCallEvalFuncExpr(scope *EvalScope, fncall *functionCallState, allowCall } type funcCallArg struct { - name string - typ godwarf.Type - off int64 - isret bool + name string + typ godwarf.Type + off int64 + dwarfEntry *godwarf.Tree // non-nil if Go 1.17+ + isret bool } // funcCallEvalArgs evaluates the arguments of the function call, copying -// the into the argument frame starting at argFrameAddr. -func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr uint64) error { +// them into the argument frame starting at argFrameAddr. +func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, formalScope *EvalScope) error { if scope.g == nil { // this should never happen return errNoGoroutine } if fncall.receiver != nil { - err := funcCallCopyOneArg(scope, fncall, fncall.receiver, &fncall.formalArgs[0], argFrameAddr) + err := funcCallCopyOneArg(scope, fncall, fncall.receiver, &fncall.formalArgs[0], formalScope) if err != nil { return err } @@ -529,7 +538,7 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr } actualArg.Name = exprToString(fncall.expr.Args[i]) - err = funcCallCopyOneArg(scope, fncall, actualArg, formalArg, argFrameAddr) + err = funcCallCopyOneArg(scope, fncall, actualArg, formalArg, formalScope) if err != nil { return err } @@ -538,7 +547,7 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr return nil } -func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg *Variable, formalArg *funcCallArg, argFrameAddr uint64) error { +func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg *Variable, formalArg *funcCallArg, formalScope *EvalScope) error { if scope.callCtx.checkEscape { //TODO(aarzilli): only apply the escapeCheck to leaking parameters. if err := escapeCheck(actualArg, formalArg.name, scope.g.stack); err != nil { @@ -554,7 +563,16 @@ func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg * //TODO(aarzilli): autmoatic wrapping in interfaces for cases not handled // by convertToEface. - formalArgVar := newVariable(formalArg.name, uint64(formalArg.off+int64(argFrameAddr)), formalArg.typ, scope.BinInfo, scope.Mem) + var formalArgVar *Variable + if formalArg.dwarfEntry != nil { + var err error + formalArgVar, err = extractVarInfoFromEntry(scope.target, formalScope.BinInfo, formalScope.image(), formalScope.Regs, formalScope.Mem, formalArg.dwarfEntry) + if err != nil { + return err + } + } else { + formalArgVar = newVariable(formalArg.name, uint64(formalArg.off+int64(formalScope.Regs.CFA)), formalArg.typ, scope.BinInfo, scope.Mem) + } if err := scope.setValue(formalArgVar, actualArg, actualArg.Name); err != nil { return err } @@ -563,16 +581,26 @@ func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg * } func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize int64, formalArgs []funcCallArg, err error) { - const CFA = 0x1000 - dwarfTree, err := fn.cu.image.getDwarfTree(fn.offset) if err != nil { return 0, nil, fmt.Errorf("DWARF read error: %v", err) } - varEntries := reader.Variables(dwarfTree, fn.Entry, int(^uint(0)>>1), reader.VariablesSkipInlinedSubroutines) + producer := bi.Producer() + trustArgOrder := producer != "" && goversion.ProducerAfterOrEqual(bi.Producer(), 1, 12) - trustArgOrder := bi.Producer() != "" && goversion.ProducerAfterOrEqual(bi.Producer(), 1, 12) + if bi.regabi && fn.cu.optimized && fn.Name != "runtime.mallocgc" { + // Debug info for function arguments on optimized functions is currently + // too incomplete to attempt injecting calls to arbitrary optimized + // functions. + // Prior to regabi we could do this because the ABI was simple enough to + // manually encode it in Delve. + // Runtime.mallocgc is an exception, we specifically patch it's DIE to be + // correct for call injection purposes. + return 0, nil, fmt.Errorf("can not call optimized function %s when regabi is in use", fn.Name) + } + + varEntries := reader.Variables(dwarfTree, fn.Entry, int(^uint(0)>>1), reader.VariablesSkipInlinedSubroutines) // typechecks arguments, calculates argument frame size for _, entry := range varEntries { @@ -584,44 +612,37 @@ func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize i return 0, nil, err } typ = resolveTypedef(typ) - var off int64 - locprog, _, err := bi.locationExpr(entry, dwarf.AttrLocation, fn.Entry) - if err != nil { - err = fmt.Errorf("could not get argument location of %s: %v", argname, err) + var formalArg *funcCallArg + if bi.regabi { + formalArg, err = funcCallArgRegABI(fn, bi, entry, argname, typ, &argFrameSize) } else { - var pieces []op.Piece - off, pieces, err = op.ExecuteStackProgram(op.DwarfRegisters{CFA: CFA, FrameBase: CFA}, locprog, bi.Arch.PtrSize()) - if err != nil { - err = fmt.Errorf("unsupported location expression for argument %s: %v", argname, err) - } - if pieces != nil { - err = fmt.Errorf("unsupported location expression for argument %s (uses DW_OP_piece)", argname) - } - off -= CFA + formalArg, err = funcCallArgOldABI(fn, bi, entry, argname, typ, trustArgOrder, &argFrameSize) } if err != nil { - if !trustArgOrder { - return 0, nil, err - } - - // With Go version 1.12 or later we can trust that the arguments appear - // in the same order as declared, which means we can calculate their - // address automatically. - // With this we can call optimized functions (which sometimes do not have - // an argument address, due to a compiler bug) as well as runtime - // functions (which are always optimized). - off = argFrameSize - off = alignAddr(off, typ.Align()) + return 0, nil, err } - - if e := off + typ.Size(); e > argFrameSize { - argFrameSize = e + if !formalArg.isret || includeRet { + formalArgs = append(formalArgs, *formalArg) } + } - if isret, _ := entry.Val(dwarf.AttrVarParam).(bool); !isret || includeRet { - formalArgs = append(formalArgs, funcCallArg{name: argname, typ: typ, off: off, isret: isret}) - } + if bi.regabi { + // The argument frame size is computed conservatively, assuming that + // there's space for each argument on the stack even if its passed in + // registers. Unfortunately this isn't quite enough because the register + // assignment algorithm Go uses can result in an amount of additional + // space used due to alignment requirements, bounded by the number of argument registers. + // Because we currently don't have an easy way to obtain the frame size, + // let's be even more conservative. + // A safe lower-bound on the size of the argument frame includes space for + // each argument plus the total bytes of register arguments. + // This is derived from worst-case alignment padding of up to + // (pointer-word-bytes - 1) per argument passed in registers. + // See: https://github.com/go-delve/delve/pull/2451#discussion_r665761531 + // TODO: Make this generic for other platforms. + argFrameSize = alignAddr(argFrameSize, 8) + argFrameSize += maxRegArgBytes } sort.Slice(formalArgs, func(i, j int) bool { @@ -631,6 +652,56 @@ func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize i return argFrameSize, formalArgs, nil } +func funcCallArgOldABI(fn *Function, bi *BinaryInfo, entry reader.Variable, argname string, typ godwarf.Type, trustArgOrder bool, pargFrameSize *int64) (*funcCallArg, error) { + const CFA = 0x1000 + var off int64 + + locprog, _, err := bi.locationExpr(entry, dwarf.AttrLocation, fn.Entry) + if err != nil { + err = fmt.Errorf("could not get argument location of %s: %v", argname, err) + } else { + var pieces []op.Piece + off, pieces, err = op.ExecuteStackProgram(op.DwarfRegisters{CFA: CFA, FrameBase: CFA}, locprog, bi.Arch.PtrSize()) + if err != nil { + err = fmt.Errorf("unsupported location expression for argument %s: %v", argname, err) + } + if pieces != nil { + err = fmt.Errorf("unsupported location expression for argument %s (uses DW_OP_piece)", argname) + } + off -= CFA + } + if err != nil { + if !trustArgOrder { + return nil, err + } + + // With Go version 1.12 or later we can trust that the arguments appear + // in the same order as declared, which means we can calculate their + // address automatically. + // With this we can call optimized functions (which sometimes do not have + // an argument address, due to a compiler bug) as well as runtime + // functions (which are always optimized). + off = *pargFrameSize + off = alignAddr(off, typ.Align()) + } + + if e := off + typ.Size(); e > *pargFrameSize { + *pargFrameSize = e + } + + isret, _ := entry.Val(dwarf.AttrVarParam).(bool) + return &funcCallArg{name: argname, typ: typ, off: off, isret: isret}, nil +} + +func funcCallArgRegABI(fn *Function, bi *BinaryInfo, entry reader.Variable, argname string, typ godwarf.Type, pargFrameSize *int64) (*funcCallArg, error) { + // Conservatively calculate the full stack argument space for ABI0. + *pargFrameSize = alignAddr(*pargFrameSize, typ.Align()) + *pargFrameSize += typ.Size() + + isret, _ := entry.Val(dwarf.AttrVarParam).(bool) + return &funcCallArg{name: argname, typ: typ, dwarfEntry: entry.Tree, isret: isret}, nil +} + // alignAddr rounds up addr to a multiple of align. Align must be a power of 2. func alignAddr(addr, align int64) int64 { return (addr + int64(align-1)) &^ int64(align-1) @@ -686,15 +757,15 @@ func escapeCheckPointer(addr uint64, name string, stack stack) error { } const ( - debugCallAXPrecheckFailed = 8 - debugCallAXCompleteCall = 0 - debugCallAXReadReturn = 1 - debugCallAXReadPanic = 2 - debugCallAXRestoreRegisters = 16 + debugCallRegPrecheckFailed = 8 + debugCallRegCompleteCall = 0 + debugCallRegReadReturn = 1 + debugCallRegReadPanic = 2 + debugCallRegRestoreRegisters = 16 ) // funcCallStep executes one step of the function call injection protocol. -func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread) bool { +func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread, protocolReg uint64, debugCallName string) bool { p := callScope.callCtx.p bi := p.BinInfo() @@ -704,7 +775,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread return true } - rax := bi.Arch.RegistersToDwarfRegisters(0, regs).Uint64Val(regnum.AMD64_Rax) //TODO(aarzilli): make this generic when call injection is supported on other architectures + regval := bi.Arch.RegistersToDwarfRegisters(0, regs).Uint64Val(protocolReg) if logflags.FnCall() { loc, _ := thread.Location() @@ -716,11 +787,11 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread fnname = loc.Fn.Name } } - fncallLog("function call interrupt gid=%d (original) thread=%d rax=%#x (PC=%#x in %s)", callScope.g.ID, thread.ThreadID(), rax, pc, fnname) + fncallLog("function call interrupt gid=%d (original) thread=%d regval=%#x (PC=%#x in %s)", callScope.g.ID, thread.ThreadID(), regval, pc, fnname) } - switch rax { - case debugCallAXPrecheckFailed: + switch regval { + case debugCallRegPrecheckFailed: // get error from top of the stack and return it to user errvar, err := readTopstackVariable(p, thread, regs, "string", loadFullValue) if err != nil { @@ -730,7 +801,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread errvar.Name = "err" fncall.err = fmt.Errorf("%v", constant.StringVal(errvar.Value)) - case debugCallAXCompleteCall: + case debugCallRegCompleteCall: p.fncallForG[callScope.g.ID].startThreadID = 0 // evaluate arguments of the target function, copy them into its argument frame and call the function if fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0 { @@ -763,8 +834,15 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread cfa := regs.SP() oldpc := regs.PC() callOP(bi, thread, regs, fncall.fn.Entry) + formalScope, err := GoroutineScope(callScope.target, thread) + if formalScope != nil && formalScope.Regs.CFA != int64(cfa) { + // This should never happen, checking just to avoid hard to figure out disasters. + err = fmt.Errorf("mismatch in CFA %#x (calculated) %#x (expected)", formalScope.Regs.CFA, int64(cfa)) + } + if err == nil { + err = funcCallEvalArgs(callScope, fncall, formalScope) + } - err := funcCallEvalArgs(callScope, fncall, cfa) if err != nil { // rolling back the call, note: this works because we called regs.Copy() above setSP(thread, cfa) @@ -774,7 +852,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread break } - case debugCallAXRestoreRegisters: + case debugCallRegRestoreRegisters: // runtime requests that we restore the registers (all except pc and sp), // this is also the last step of the function call protocol. pc, sp := regs.PC(), regs.SP() @@ -787,12 +865,12 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread if err := setSP(thread, sp); err != nil { fncall.err = fmt.Errorf("could not restore SP: %v", err) } - if err := stepInstructionOut(p, thread, debugCallFunctionName, debugCallFunctionName); err != nil { - fncall.err = fmt.Errorf("could not step out of %s: %v", debugCallFunctionName, err) + if err := stepInstructionOut(p, thread, debugCallName, debugCallName); err != nil { + fncall.err = fmt.Errorf("could not step out of %s: %v", debugCallName, err) } return true - case debugCallAXReadReturn: + case debugCallRegReadReturn: // read return arguments from stack if fncall.panicvar != nil || fncall.lateCallFailure { break @@ -805,7 +883,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread // pretend we are still inside the function we called fakeFunctionEntryScope(retScope, fncall.fn, int64(regs.SP()), regs.SP()-uint64(bi.Arch.PtrSize())) - retScope.trustArgOrder = true + retScope.trustArgOrder = !bi.regabi fncall.retvars, err = retScope.Locals() if err != nil { @@ -828,7 +906,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread callScope.callCtx.stacks = append(callScope.callCtx.stacks, threadg.stack) } - case debugCallAXReadPanic: + case debugCallRegReadPanic: // read panic value from stack fncall.panicvar, err = readTopstackVariable(p, thread, regs, "interface {}", callScope.callCtx.retLoadCfg) if err != nil { @@ -838,9 +916,9 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread fncall.panicvar.Name = "~panic" default: - // Got an unknown AX value, this is probably bad but the safest thing + // Got an unknown protocol register value, this is probably bad but the safest thing // possible is to ignore it and hope it didn't matter. - fncallLog("unknown value of AX %#x", rax) + fncallLog("unknown value of protocol register %#x", regval) } return false @@ -877,6 +955,7 @@ func fakeFunctionEntryScope(scope *EvalScope, fn *Function, cfa int64, sp uint64 scope.Regs.CFA = cfa scope.Regs.Reg(scope.Regs.SPRegNum).Uint64Val = sp + scope.Regs.Reg(scope.Regs.PCRegNum).Uint64Val = fn.Entry fn.cu.image.dwarfReader.Seek(fn.offset) e, err := fn.cu.image.dwarfReader.Next() @@ -1016,3 +1095,83 @@ func findCallInjectionStateForThread(t *Target, thread Thread) (*G, *callInjecti return nil, nil, notfound() } + +// debugCallFunction searches for the debug call function in the binary and +// uses this search to detect the debug call version. +// Returns the debug call function and its version as an integer (the lowest +// valid version is 1) or nil and zero. +func debugCallFunction(bi *BinaryInfo) (*Function, int) { + for version := maxDebugCallVersion; version >= 1; version-- { + name := debugCallFunctionNamePrefix2 + "V" + strconv.Itoa(version) + fn, ok := bi.LookupFunc[name] + if ok && fn != nil { + return fn, version + } + } + return nil, 0 +} + +// debugCallProtocolReg returns the register ID (as defined in pkg/dwarf/regnum) +// of the register used in the debug call protocol, given the debug call version. +// Also returns a bool indicating whether the version is supported. +func debugCallProtocolReg(version int) (uint64, bool) { + // TODO(aarzilli): make this generic when call injection is supported on other architectures. + var protocolReg uint64 + switch version { + case 1: + protocolReg = regnum.AMD64_Rax + case 2: + protocolReg = regnum.AMD64_R12 + default: + return 0, false + } + return protocolReg, true +} + +type fakeEntry map[dwarf.Attr]interface{} + +func (e fakeEntry) Val(attr dwarf.Attr) interface{} { + return e[attr] +} + +func regabiMallocgcWorkaround(bi *BinaryInfo) ([]*godwarf.Tree, error) { + var err1 error + + t := func(name string) godwarf.Type { + if err1 != nil { + return nil + } + typ, err := bi.findType(name) + if err != nil { + err1 = err + return nil + } + return typ + } + + m := func(name string, typ godwarf.Type, reg int, isret bool) *godwarf.Tree { + if err1 != nil { + return nil + } + var e fakeEntry = map[dwarf.Attr]interface{}{ + dwarf.AttrName: name, + dwarf.AttrType: typ.Common().Offset, + dwarf.AttrLocation: []byte{byte(op.DW_OP_reg0) + byte(reg)}, + dwarf.AttrVarParam: isret, + } + + return &godwarf.Tree{ + Entry: e, + Tag: dwarf.TagFormalParameter, + } + } + + r := []*godwarf.Tree{ + m("size", t("uintptr"), regnum.AMD64_Rax, false), + m("typ", t("*runtime._type"), regnum.AMD64_Rbx, false), + m("needzero", t("bool"), regnum.AMD64_Rcx, false), + m("~r1", t("unsafe.Pointer"), regnum.AMD64_Rax, true), + } + + return r, err1 +} diff --git a/pkg/proc/proc_general_test.go b/pkg/proc/proc_general_test.go index 25ce0aeb..ce72ec0e 100644 --- a/pkg/proc/proc_general_test.go +++ b/pkg/proc/proc_general_test.go @@ -6,6 +6,7 @@ import ( "testing" "unsafe" + "github.com/go-delve/delve/pkg/goversion" protest "github.com/go-delve/delve/pkg/proc/test" ) @@ -118,3 +119,16 @@ func TestDwarfVersion(t *testing.T) { } } } + +func TestRegabiFlagSentinel(t *testing.T) { + // Detect if the regabi flag in the producer string gets removed + if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) || runtime.GOARCH != "amd64" { + t.Skip("irrelevant before Go 1.17 or on non-amd64 architectures") + } + fixture := protest.BuildFixture("math", 0) + bi := NewBinaryInfo(runtime.GOOS, runtime.GOARCH) + assertNoError(bi.LoadBinaryInfo(fixture.Path, 0, nil), t, "LoadBinaryInfo") + if !bi.regabi { + t.Errorf("regabi flag not set") + } +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index caeeab5a..1aff437d 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -423,7 +423,7 @@ func testseq(program string, contFunc contFunc, testcases []nextTest, initialLoc testseq2(t, program, initialLocation, seqTestcases) } -const traceTestseq2 = false +const traceTestseq2 = true func testseq2(t *testing.T, program string, initialLocation string, testcases []seqTest) { testseq2Args(".", []string{}, 0, t, program, initialLocation, testcases) @@ -1284,7 +1284,7 @@ func TestFrameEvaluation(t *testing.T) { continue } t.Logf("Goroutine %d %#v", g.ID, g.Thread) - logStacktrace(t, p.BinInfo(), frames) + logStacktrace(t, p, frames) for i := range frames { if frames[i].Call.Fn != nil && frames[i].Call.Fn.Name == "main.agoroutine" { frame = i @@ -2245,7 +2245,7 @@ func TestStepCall(t *testing.T) { func TestStepCallPtr(t *testing.T) { // Tests that Step works correctly when calling functions with a // function pointer. - if goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) { + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) && !(goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) && (runtime.GOARCH == "amd64")) { testseq("teststepprog", contStep, []nextTest{ {9, 10}, {10, 6}, @@ -2335,6 +2335,19 @@ func TestStepIgnorePrivateRuntime(t *testing.T) { // Tests that Step will ignore calls to private runtime functions // (such as runtime.convT2E in this case) switch { + case goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) && (runtime.GOARCH == "amd64"): + testseq("teststepprog", contStep, []nextTest{ + {21, 13}, + {13, 14}, + {14, 15}, + {15, 17}, + {17, 22}}, "", t) + case goversion.VersionAfterOrEqual(runtime.Version(), 1, 17): + testseq("teststepprog", contStep, []nextTest{ + {21, 14}, + {14, 15}, + {15, 17}, + {17, 22}}, "", t) case goversion.VersionAfterOrEqual(runtime.Version(), 1, 11): testseq("teststepprog", contStep, []nextTest{ {21, 14}, @@ -2598,7 +2611,7 @@ func TestStepOnCallPtrInstr(t *testing.T) { assertNoError(p.Step(), t, "Step()") - if goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) { + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 11) && !(goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) && (runtime.GOARCH == "amd64")) { assertLineNumber(p, t, 6, "Step continued to wrong line,") } else { assertLineNumber(p, t, 5, "Step continued to wrong line,") @@ -3153,7 +3166,7 @@ func TestIssue844(t *testing.T) { }) } -func logStacktrace(t *testing.T, bi *proc.BinaryInfo, frames []proc.Stackframe) { +func logStacktrace(t *testing.T, p *proc.Target, frames []proc.Stackframe) { for j := range frames { name := "?" if frames[j].Current.Fn != nil { @@ -3165,20 +3178,20 @@ func logStacktrace(t *testing.T, bi *proc.BinaryInfo, frames []proc.Stackframe) t.Logf("\t%#x %#x %#x %s at %s:%d\n", frames[j].Call.PC, frames[j].FrameOffset(), frames[j].FramePointerOffset(), name, filepath.Base(frames[j].Call.File), frames[j].Call.Line) if frames[j].TopmostDefer != nil { - f, l, fn := bi.PCToLine(frames[j].TopmostDefer.DeferredPC) + _, _, fn := frames[j].TopmostDefer.DeferredFunc(p) fnname := "" if fn != nil { fnname = fn.Name } - t.Logf("\t\ttopmost defer: %#x %s at %s:%d\n", frames[j].TopmostDefer.DeferredPC, fnname, f, l) + t.Logf("\t\ttopmost defer: %#x %s\n", frames[j].TopmostDefer.DwrapPC, fnname) } for deferIdx, _defer := range frames[j].Defers { - f, l, fn := bi.PCToLine(_defer.DeferredPC) + _, _, fn := _defer.DeferredFunc(p) fnname := "" if fn != nil { fnname = fn.Name } - t.Logf("\t\t%d defer: %#x %s at %s:%d\n", deferIdx, _defer.DeferredPC, fnname, f, l) + t.Logf("\t\t%d defer: %#x %s\n", deferIdx, _defer.DwrapPC, fnname) } } @@ -3299,7 +3312,7 @@ func TestCgoStacktrace(t *testing.T) { assertNoError(err, t, fmt.Sprintf("Stacktrace at iteration step %d", itidx)) t.Logf("iteration step %d", itidx) - logStacktrace(t, p.BinInfo(), frames) + logStacktrace(t, p, frames) m := stacktraceCheck(t, tc, frames) mismatch := (m == nil) @@ -3336,7 +3349,7 @@ func TestCgoStacktrace(t *testing.T) { if frames[j].Current.File != threadFrames[j].Current.File || frames[j].Current.Line != threadFrames[j].Current.Line { t.Logf("stack mismatch between goroutine stacktrace and thread stacktrace") t.Logf("thread stacktrace:") - logStacktrace(t, p.BinInfo(), threadFrames) + logStacktrace(t, p, threadFrames) mismatch = true break } @@ -3390,7 +3403,7 @@ func TestSystemstackStacktrace(t *testing.T) { assertNoError(err, t, "GetG") frames, err := g.Stacktrace(100, 0) assertNoError(err, t, "stacktrace") - logStacktrace(t, p.BinInfo(), frames) + logStacktrace(t, p, frames) m := stacktraceCheck(t, []string{"!runtime.startpanic_m", "runtime.gopanic", "main.main"}, frames) if m == nil { t.Fatal("see previous loglines") @@ -3423,7 +3436,7 @@ func TestSystemstackOnRuntimeNewstack(t *testing.T) { } frames, err := g.Stacktrace(100, 0) assertNoError(err, t, "stacktrace") - logStacktrace(t, p.BinInfo(), frames) + logStacktrace(t, p, frames) m := stacktraceCheck(t, []string{"!runtime.newstack", "main.main"}, frames) if m == nil { t.Fatal("see previous loglines") @@ -4047,7 +4060,7 @@ func TestReadDefer(t *testing.T) { frames, err := p.SelectedGoroutine().Stacktrace(10, proc.StacktraceReadDefers) assertNoError(err, t, "Stacktrace") - logStacktrace(t, p.BinInfo(), frames) + logStacktrace(t, p, frames) examples := []struct { frameIdx int @@ -4073,9 +4086,9 @@ func TestReadDefer(t *testing.T) { if d.Unreadable != nil { t.Fatalf("expected %q as %s of frame %d, got unreadable defer: %v", tgt, deferName, frameIdx, d.Unreadable) } - dfn := p.BinInfo().PCToFunc(d.DeferredPC) + _, _, dfn := d.DeferredFunc(p) if dfn == nil { - t.Fatalf("expected %q as %s of frame %d, got %#x", tgt, deferName, frameIdx, d.DeferredPC) + t.Fatalf("expected %q as %s of frame %d, got %#x", tgt, deferName, frameIdx, d.DwrapPC) } if dfn.Name != tgt { t.Fatalf("expected %q as %s of frame %d, got %q", tgt, deferName, frameIdx, dfn.Name) @@ -4113,6 +4126,19 @@ func TestNextUnknownInstr(t *testing.T) { } func TestReadDeferArgs(t *testing.T) { + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) { + // When regabi is enabled in Go 1.17 and later, reading arguments of + // deferred functions becomes significantly more complicated because of + // the autogenerated code used to unpack the argument frame stored in + // runtime._defer into registers. + // We either need to know how to do the translation, implementing the ABI1 + // rules in Delve, or have some assistence from the compiler (for example + // have the dwrap function contain entries for each of the captured + // variables with a location describing their offset from DX). + // Ultimately this feature is unimportant enough that we can leave it + // disabled for now. + t.Skip("unsupported") + } var tests = []struct { frame, deferCall int a, b int64 @@ -4128,8 +4154,12 @@ func TestReadDeferArgs(t *testing.T) { scope, err := proc.ConvertEvalScope(p, -1, test.frame, test.deferCall) assertNoError(err, t, fmt.Sprintf("ConvertEvalScope(-1, %d, %d)", test.frame, test.deferCall)) - if scope.Fn.Name != "main.f2" { - t.Fatalf("expected function \"main.f2\" got %q", scope.Fn.Name) + if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) { + // In Go 1.17 deferred function calls can end up inside a wrapper, and + // the scope for this evaluation needs to be the wrapper. + if scope.Fn.Name != "main.f2" { + t.Fatalf("expected function \"main.f2\" got %q", scope.Fn.Name) + } } avar, err := scope.EvalVariable("a", normalLoadConfig) @@ -4344,7 +4374,7 @@ func TestAncestors(t *testing.T) { astack, err := a.Stack(100) assertNoError(err, t, fmt.Sprintf("Ancestor %d stack", i)) t.Logf("ancestor %d\n", i) - logStacktrace(t, p.BinInfo(), astack) + logStacktrace(t, p, astack) for _, frame := range astack { if frame.Current.Fn != nil && frame.Current.Fn.Name == "main.main" { mainFound = true @@ -4480,7 +4510,7 @@ func TestCgoStacktrace2(t *testing.T) { p.Continue() frames, err := proc.ThreadStacktrace(p.CurrentThread(), 100) assertNoError(err, t, "Stacktrace()") - logStacktrace(t, p.BinInfo(), frames) + logStacktrace(t, p, frames) m := stacktraceCheck(t, []string{"C.sigsegv", "C.testfn", "main.main"}, frames) if m == nil { t.Fatal("see previous loglines") @@ -4591,7 +4621,7 @@ func TestIssue1795(t *testing.T) { assertNoError(p.Continue(), t, "Continue()") frames, err := proc.ThreadStacktrace(p.CurrentThread(), 40) assertNoError(err, t, "ThreadStacktrace()") - logStacktrace(t, p.BinInfo(), frames) + logStacktrace(t, p, frames) if err := checkFrame(frames[0], "regexp.(*Regexp).doExecute", "", 0, false); err != nil { t.Errorf("Wrong frame 0: %v", err) } @@ -5361,3 +5391,28 @@ func TestManualStopWhileStopped(t *testing.T) { } }) } + +func TestDwrapStartLocation(t *testing.T) { + // Tests that the start location of a goroutine is unwrapped in Go 1.17 and later. + withTestProcess("goroutinestackprog", t, func(p *proc.Target, fixture protest.Fixture) { + setFunctionBreakpoint(p, t, "main.stacktraceme") + assertNoError(p.Continue(), t, "Continue()") + gs, _, err := proc.GoroutinesInfo(p, 0, 0) + assertNoError(err, t, "GoroutinesInfo") + found := false + for _, g := range gs { + startLoc := g.StartLoc(p) + if startLoc.Fn == nil { + continue + } + t.Logf("%#v\n", startLoc.Fn.Name) + if startLoc.Fn.Name == "main.agoroutine" { + found = true + break + } + } + if !found { + t.Errorf("could not find any goroutine with a start location of main.agoroutine") + } + }) +} diff --git a/pkg/proc/stack.go b/pkg/proc/stack.go index 005bca9e..8460a26c 100644 --- a/pkg/proc/stack.go +++ b/pkg/proc/stack.go @@ -519,11 +519,11 @@ func (it *stackIterator) loadG0SchedSP() { // Defer represents one deferred call type Defer struct { - DeferredPC uint64 // Value of field _defer.fn.fn, the deferred function - DeferPC uint64 // PC address of instruction that added this defer - SP uint64 // Value of SP register when this function was deferred (this field gets adjusted when the stack is moved to match the new stack space) - link *Defer // Next deferred function - argSz int64 + DwrapPC uint64 // Value of field _defer.fn.fn, the deferred function or a wrapper to it in Go 1.17 or later + DeferPC uint64 // PC address of instruction that added this defer + SP uint64 // Value of SP register when this function was deferred (this field gets adjusted when the stack is moved to match the new stack space) + link *Defer // Next deferred function + argSz int64 variable *Variable Unreadable error @@ -584,7 +584,7 @@ func (d *Defer) load() { if fnvar.Addr != 0 { fnvar = fnvar.loadFieldNamed("fn") if fnvar.Unreadable == nil { - d.DeferredPC, _ = constant.Uint64Val(fnvar.Value) + d.DwrapPC, _ = constant.Uint64Val(fnvar.Value) } } @@ -627,11 +627,11 @@ func (d *Defer) EvalScope(t *Target, thread Thread) (*EvalScope, error) { } bi := thread.BinInfo() - scope.PC = d.DeferredPC - scope.File, scope.Line, scope.Fn = bi.PCToLine(d.DeferredPC) + scope.PC = d.DwrapPC + scope.File, scope.Line, scope.Fn = bi.PCToLine(d.DwrapPC) if scope.Fn == nil { - return nil, fmt.Errorf("could not find function at %#x", d.DeferredPC) + return nil, fmt.Errorf("could not find function at %#x", d.DwrapPC) } // The arguments are stored immediately after the defer header struct, i.e. @@ -664,3 +664,16 @@ func (d *Defer) EvalScope(t *Target, thread Thread) (*EvalScope, error) { return scope, nil } + +// DeferredFunc returns the deferred function, on Go 1.17 and later unwraps +// any defer wrapper. +func (d *Defer) DeferredFunc(p *Target) (file string, line int, fn *Function) { + bi := p.BinInfo() + fn = bi.PCToFunc(d.DwrapPC) + fn = p.dwrapUnwrap(fn) + if fn == nil { + return "", 0, nil + } + file, line = fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry) + return file, line, fn +} diff --git a/pkg/proc/target.go b/pkg/proc/target.go index 3f4cfcd5..ff8af354 100644 --- a/pkg/proc/target.go +++ b/pkg/proc/target.go @@ -465,3 +465,27 @@ func (t *Target) clearFakeMemory() { t.fakeMemoryRegistry = t.fakeMemoryRegistry[:0] t.fakeMemoryRegistryMap = make(map[string]*compositeMemory) } + +// dwrapUnwrap checks if fn is a dwrap wrapper function and unwraps it if it is. +func (t *Target) dwrapUnwrap(fn *Function) *Function { + if fn == nil { + return nil + } + if !strings.Contains(fn.Name, "·dwrap·") { + return fn + } + if unwrap := t.BinInfo().dwrapUnwrapCache[fn.Entry]; unwrap != nil { + return unwrap + } + text, err := disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), fn.Entry, fn.End, false) + if err != nil { + return fn + } + for _, instr := range text { + if instr.IsCall() && instr.DestLoc != nil && instr.DestLoc.Fn != nil && !instr.DestLoc.Fn.privateRuntime() { + t.BinInfo().dwrapUnwrapCache[fn.Entry] = instr.DestLoc.Fn + return instr.DestLoc.Fn + } + } + return fn +} diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go index 3c138b3d..ff9c681a 100644 --- a/pkg/proc/target_exec.go +++ b/pkg/proc/target_exec.go @@ -2,6 +2,7 @@ package proc import ( "bytes" + "debug/dwarf" "errors" "fmt" "go/ast" @@ -275,7 +276,22 @@ func stepInstructionOut(dbp *Target, curthread Thread, fnname1, fnname2 string) return err } loc, err := curthread.Location() - if err != nil || loc.Fn == nil || (loc.Fn.Name != fnname1 && loc.Fn.Name != fnname2) { + var locFnName string + if loc.Fn != nil { + locFnName = loc.Fn.Name + // Calls to runtime.Breakpoint are inlined in some versions of Go when + // inlining is enabled. Here we attempt to resolve any inlining. + dwarfTree, _ := loc.Fn.cu.image.getDwarfTree(loc.Fn.offset) + if dwarfTree != nil { + inlstack := reader.InlineStack(dwarfTree, loc.PC) + if len(inlstack) > 0 { + if locFnName2, ok := inlstack[0].Val(dwarf.AttrName).(string); ok { + locFnName = locFnName2 + } + } + } + } + if err != nil || loc.Fn == nil || (locFnName != fnname1 && locFnName != fnname2) { g, _ := GetG(curthread) selg := dbp.SelectedGoroutine() if g != nil && selg != nil && g.ID == selg.ID { @@ -902,8 +918,8 @@ func skipAutogeneratedWrappersOut(g *G, thread Thread, startTopframe, startRetfr func setDeferBreakpoint(p *Target, text []AsmInstruction, topframe Stackframe, sameGCond ast.Expr, stepInto bool) (uint64, error) { // Set breakpoint on the most recently deferred function (if any) var deferpc uint64 - if topframe.TopmostDefer != nil && topframe.TopmostDefer.DeferredPC != 0 { - deferfn := p.BinInfo().PCToFunc(topframe.TopmostDefer.DeferredPC) + if topframe.TopmostDefer != nil && topframe.TopmostDefer.DwrapPC != 0 { + _, _, deferfn := topframe.TopmostDefer.DeferredFunc(p) var err error deferpc, err = FirstPCAfterPrologue(p, deferfn, false) if err != nil { @@ -977,11 +993,15 @@ func stepOutReverse(p *Target, topframe, retframe Stackframe, sameGCond ast.Expr var callpc uint64 - if isPanicCall(frames) { - if len(frames) < 4 || frames[3].Current.Fn == nil { - return &ErrNoSourceForPC{frames[2].Current.PC} + if ok, panicFrame := isPanicCall(frames); ok { + if len(frames) < panicFrame+2 || frames[panicFrame+1].Current.Fn == nil { + if panicFrame < len(frames) { + return &ErrNoSourceForPC{frames[panicFrame].Current.PC} + } else { + return &ErrNoSourceForPC{frames[0].Current.PC} + } } - callpc, err = findCallInstrForRet(p, p.Memory(), frames[2].Ret, frames[3].Current.Fn) + callpc, err = findCallInstrForRet(p, p.Memory(), frames[panicFrame].Ret, frames[panicFrame+1].Current.Fn) if err != nil { return err } diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 82d894ef..8fa5242c 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -516,15 +516,20 @@ func (g *G) Go() Location { } // StartLoc returns the starting location of the goroutine. -func (g *G) StartLoc() Location { - f, l, fn := g.variable.bi.PCToLine(g.StartPC) - return Location{PC: g.StartPC, File: f, Line: l, Fn: fn} +func (g *G) StartLoc(tgt *Target) Location { + fn := g.variable.bi.PCToFunc(g.StartPC) + fn = tgt.dwrapUnwrap(fn) + if fn == nil { + return Location{PC: g.StartPC} + } + f, l := fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry) + return Location{PC: fn.Entry, File: f, Line: l, Fn: fn} } // System returns true if g is a system goroutine. See isSystemGoroutine in // $GOROOT/src/runtime/traceback.go. -func (g *G) System() bool { - loc := g.StartLoc() +func (g *G) System(tgt *Target) bool { + loc := g.StartLoc(tgt) if loc.Fn == nil { return false } diff --git a/service/api/conversions.go b/service/api/conversions.go index 7a2d7179..086a0767 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -284,7 +284,7 @@ func ConvertFunction(fn *proc.Function) *Function { } // ConvertGoroutine converts from proc.G to api.Goroutine. -func ConvertGoroutine(g *proc.G) *Goroutine { +func ConvertGoroutine(tgt *proc.Target, g *proc.G) *Goroutine { th := g.Thread tid := 0 if th != nil { @@ -298,7 +298,7 @@ func ConvertGoroutine(g *proc.G) *Goroutine { CurrentLoc: ConvertLocation(g.CurrentLoc), UserCurrentLoc: ConvertLocation(g.UserCurrent()), GoStatementLoc: ConvertLocation(g.Go()), - StartLoc: ConvertLocation(g.StartLoc()), + StartLoc: ConvertLocation(g.StartLoc(tgt)), ThreadID: tid, WaitSince: g.WaitSince, WaitReason: g.WaitReason, @@ -308,10 +308,10 @@ func ConvertGoroutine(g *proc.G) *Goroutine { } // ConvertGoroutines converts from []*proc.G to []*api.Goroutine. -func ConvertGoroutines(gs []*proc.G) []*Goroutine { +func ConvertGoroutines(tgt *proc.Target, gs []*proc.G) []*Goroutine { goroutines := make([]*Goroutine, len(gs)) for i := range gs { - goroutines[i] = ConvertGoroutine(gs[i]) + goroutines[i] = ConvertGoroutine(tgt, gs[i]) } return goroutines } diff --git a/service/dap/server.go b/service/dap/server.go index f9fe0fc4..696bf6cb 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1559,7 +1559,7 @@ func (s *Server) onStackTraceRequest(request *dap.StackTraceRequest) { // Determine if the goroutine is a system goroutine. isSystemGoroutine := true if g, _ := s.debugger.FindGoroutine(goroutineID); g != nil { - isSystemGoroutine = g.System() + isSystemGoroutine = g.System(s.debugger.Target()) } stackFrames := make([]dap.StackFrame, len(frames)) diff --git a/service/dap/server_test.go b/service/dap/server_test.go index 615a8f1a..f779cdae 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -2775,9 +2775,14 @@ func TestWorkingDir(t *testing.T) { client.VariablesRequest(1001) // Locals locals := client.ExpectVariablesResponse(t) checkChildren(t, locals, "Locals", 2) - checkVarExact(t, locals, 0, "pwd", "pwd", fmt.Sprintf("%q", wd), "string", noChildren) - checkVarExact(t, locals, 1, "err", "err", "error nil", "error", noChildren) - + for i := range locals.Body.Variables { + switch locals.Body.Variables[i].Name { + case "pwd": + checkVarExact(t, locals, i, "pwd", "pwd", fmt.Sprintf("%q", wd), "string", noChildren) + case "err": + checkVarExact(t, locals, i, "err", "err", "error nil", "error", noChildren) + } + } }, disconnect: false, }}) @@ -3210,7 +3215,7 @@ func TestEvaluateCallRequest(t *testing.T) { disconnect: false, }, { // Stop at runtime breakpoint execute: func() { - checkStop(t, client, 1, "main.main", 197) + checkStop(t, client, 1, "main.main", -1) // No return values client.EvaluateRequest("call call0(1, 2)", 1000, "this context will be ignored") @@ -3501,13 +3506,19 @@ func TestStepOutPreservesGoroutine(t *testing.T) { if len(bestg) > 0 { goroutineId = bestg[rand.Intn(len(bestg))] t.Logf("selected goroutine %d (best)\n", goroutineId) - } else { + } else if len(candg) > 0 { goroutineId = candg[rand.Intn(len(candg))] t.Logf("selected goroutine %d\n", goroutineId) } - client.StepOutRequest(goroutineId) - client.ExpectStepOutResponse(t) + + if goroutineId != 0 { + client.StepOutRequest(goroutineId) + client.ExpectStepOutResponse(t) + } else { + client.ContinueRequest(-1) + client.ExpectContinueResponse(t) + } switch e := client.ExpectMessage(t).(type) { case *dap.StoppedEvent: @@ -4490,7 +4501,7 @@ func TestSetVariableWithCall(t *testing.T) { execute: func() { tester := &helperForSetVariable{t, client} - checkStop(t, client, 1, "main.main", 197) + checkStop(t, client, 1, "main.main", -1) _ = tester.variables(1001) @@ -4499,7 +4510,7 @@ func TestSetVariableWithCall(t *testing.T) { tester.evaluateRegex("str", `.*in main.callstacktrace at.*`, noChildren) tester.failSetVariableAndStop(1001, "str", `callpanic()`, `callpanic panicked`) - checkStop(t, client, 1, "main.main", 197) + checkStop(t, client, 1, "main.main", -1) // breakpoint during a function call. tester.failSetVariableAndStop(1001, "str", `callbreak()`, "call stopped") diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 14ce3e93..7f20b91f 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -590,7 +590,7 @@ func (d *Debugger) state(retLoadCfg *proc.LoadConfig) (*api.DebuggerState, error ) if d.target.SelectedGoroutine() != nil { - goroutine = api.ConvertGoroutine(d.target.SelectedGoroutine()) + goroutine = api.ConvertGoroutine(d.target, d.target.SelectedGoroutine()) } exited := false @@ -1269,7 +1269,7 @@ func (d *Debugger) collectBreakpointInformation(state *api.DebuggerState) error if err != nil { return err } - bpi.Goroutine = api.ConvertGoroutine(g) + bpi.Goroutine = api.ConvertGoroutine(d.target, g) } if bp.Stacktrace > 0 { @@ -1536,7 +1536,7 @@ func (d *Debugger) FilterGoroutines(gs []*proc.G, filters []api.ListGoroutinesFi for _, g := range gs { ok := true for i := range filters { - if !matchGoroutineFilter(g, &filters[i]) { + if !matchGoroutineFilter(d.target, g, &filters[i]) { ok = false break } @@ -1548,7 +1548,7 @@ func (d *Debugger) FilterGoroutines(gs []*proc.G, filters []api.ListGoroutinesFi return r } -func matchGoroutineFilter(g *proc.G, filter *api.ListGoroutinesFilter) bool { +func matchGoroutineFilter(tgt *proc.Target, g *proc.G, filter *api.ListGoroutinesFilter) bool { var val bool switch filter.Kind { default: @@ -1562,7 +1562,7 @@ func matchGoroutineFilter(g *proc.G, filter *api.ListGoroutinesFilter) bool { case api.GoroutineGoLoc: val = matchGoroutineLocFilter(g.Go(), filter.Arg) case api.GoroutineStartLoc: - val = matchGoroutineLocFilter(g.StartLoc(), filter.Arg) + val = matchGoroutineLocFilter(g.StartLoc(tgt), filter.Arg) case api.GoroutineLabel: idx := strings.Index(filter.Arg, "=") if idx >= 0 { @@ -1573,7 +1573,7 @@ func matchGoroutineFilter(g *proc.G, filter *api.ListGoroutinesFilter) bool { case api.GoroutineRunning: val = g.Thread != nil case api.GoroutineUser: - val = !g.System() + val = !g.System(tgt) } if filter.Negated { val = !val @@ -1616,13 +1616,13 @@ func (d *Debugger) GroupGoroutines(gs []*proc.G, group *api.GoroutineGroupingOpt case api.GoroutineGoLoc: key = formatLoc(g.Go()) case api.GoroutineStartLoc: - key = formatLoc(g.StartLoc()) + key = formatLoc(g.StartLoc(d.target)) case api.GoroutineLabel: key = fmt.Sprintf("%s=%s", group.GroupByKey, g.Labels()[group.GroupByKey]) case api.GoroutineRunning: key = fmt.Sprintf("running=%v", g.Thread != nil) case api.GoroutineUser: - key = fmt.Sprintf("user=%v", !g.System()) + key = fmt.Sprintf("user=%v", !g.System(d.target)) } if len(groupMembers[key]) < group.MaxGroupMembers { groupMembers[key] = append(groupMembers[key], g) @@ -1764,12 +1764,12 @@ func (d *Debugger) convertStacktrace(rawlocs []proc.Stackframe, cfg *proc.LoadCo func (d *Debugger) convertDefers(defers []*proc.Defer) []api.Defer { r := make([]api.Defer, len(defers)) for i := range defers { - ddf, ddl, ddfn := d.target.BinInfo().PCToLine(defers[i].DeferredPC) + ddf, ddl, ddfn := defers[i].DeferredFunc(d.target) drf, drl, drfn := d.target.BinInfo().PCToLine(defers[i].DeferPC) r[i] = api.Defer{ DeferredLoc: api.ConvertLocation(proc.Location{ - PC: defers[i].DeferredPC, + PC: ddfn.Entry, File: ddf, Line: ddl, Fn: ddfn, @@ -2109,6 +2109,10 @@ func (d *Debugger) DumpCancel() error { return nil } +func (d *Debugger) Target() *proc.Target { + return d.target +} + func go11DecodeErrorCheck(err error) error { if _, isdecodeerr := err.(dwarf.DecodeError); !isdecodeerr { return err diff --git a/service/rpc1/server.go b/service/rpc1/server.go index 2844dc82..2c8d729e 100644 --- a/service/rpc1/server.go +++ b/service/rpc1/server.go @@ -288,7 +288,7 @@ func (s *RPCServer) ListGoroutines(arg interface{}, goroutines *[]*api.Goroutine } s.debugger.LockTarget() s.debugger.UnlockTarget() - *goroutines = api.ConvertGoroutines(gs) + *goroutines = api.ConvertGoroutines(s.debugger.Target(), gs) return nil } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index 2a0607aa..056900ed 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -636,7 +636,7 @@ func (s *RPCServer) ListGoroutines(arg ListGoroutinesIn, out *ListGoroutinesOut) gs, out.Groups, out.TooManyGroups = s.debugger.GroupGoroutines(gs, &arg.GoroutineGroupingOptions) s.debugger.LockTarget() defer s.debugger.UnlockTarget() - out.Goroutines = api.ConvertGoroutines(gs) + out.Goroutines = api.ConvertGoroutines(s.debugger.Target(), gs) out.Nextg = nextg return nil } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 5ee3151b..bbe5150c 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -1891,17 +1891,9 @@ func TestAcceptMulticlient(t *testing.T) { <-serverDone } -func mustHaveDebugCalls(t *testing.T, c service.Client) { - locs, err := c.FindLocation(api.EvalScope{GoroutineID: -1}, "runtime.debugCallV1", false, nil) - if len(locs) == 0 || err != nil { - t.Skip("function calls not supported on this version of go") - } -} - func TestClientServerFunctionCall(t *testing.T) { protest.MustSupportFunctionCalls(t, testBackend) withTestClient2("fncall", t, func(c service.Client) { - mustHaveDebugCalls(t, c) c.SetReturnValuesLoadConfig(&normalLoadConfig) state := <-c.Continue() assertNoError(state.Err, t, "Continue()") @@ -1935,7 +1927,6 @@ func TestClientServerFunctionCallBadPos(t *testing.T) { t.Skip("this is a safe point for Go 1.12") } withTestClient2("fncall", t, func(c service.Client) { - mustHaveDebugCalls(t, c) loc, err := c.FindLocation(api.EvalScope{GoroutineID: -1}, "fmt/print.go:649", false, nil) assertNoError(err, t, "could not find location") @@ -1959,7 +1950,6 @@ func TestClientServerFunctionCallBadPos(t *testing.T) { func TestClientServerFunctionCallPanic(t *testing.T) { protest.MustSupportFunctionCalls(t, testBackend) withTestClient2("fncall", t, func(c service.Client) { - mustHaveDebugCalls(t, c) c.SetReturnValuesLoadConfig(&normalLoadConfig) state := <-c.Continue() assertNoError(state.Err, t, "Continue()") @@ -1988,7 +1978,6 @@ func TestClientServerFunctionCallStacktrace(t *testing.T) { } protest.MustSupportFunctionCalls(t, testBackend) withTestClient2("fncall", t, func(c service.Client) { - mustHaveDebugCalls(t, c) c.SetReturnValuesLoadConfig(&api.LoadConfig{FollowPointers: false, MaxStringLen: 2048}) state := <-c.Continue() assertNoError(state.Err, t, "Continue()") diff --git a/service/test/variables_test.go b/service/test/variables_test.go index af6d4c78..1acbbacd 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -1283,12 +1283,12 @@ func TestCallFunction(t *testing.T) { {`strings.Join(s1, comma)`, nil, errors.New(`error evaluating "s1" as argument elems in function strings.Join: could not find symbol value for s1`)}, } - withTestProcess("fncall", t, func(p *proc.Target, fixture protest.Fixture) { - _, err := proc.FindFunctionLocation(p, "runtime.debugCallV1", 0) - if err != nil { - t.Skip("function calls not supported on this version of go") - } + var testcases117 = []testCaseCallFunction{ + {`regabistacktest("one", "two", "three", "four", "five", 4)`, []string{`:string:"onetwo"`, `:string:"twothree"`, `:string:"threefour"`, `:string:"fourfive"`, `:string:"fiveone"`, ":uint8:8"}, nil}, + {`regabistacktest2(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`, []string{":int:3", ":int:5", ":int:7", ":int:9", ":int:11", ":int:13", ":int:15", ":int:17", ":int:19", ":int:11"}, nil}, + } + withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, fixture protest.Fixture) { testCallFunctionSetBreakpoint(t, p, fixture) assertNoError(p.Continue(), t, "Continue()") @@ -1319,6 +1319,12 @@ func TestCallFunction(t *testing.T) { } } + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 17) { + for _, tc := range testcases117 { + testCallFunction(t, p, tc) + } + } + // LEAVE THIS AS THE LAST ITEM, IT BREAKS THE TARGET PROCESS!!! testCallFunction(t, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil}) })