Go 1.15 support (#2011)

* proc: start variable visibility one line after their decl line

In most cases variables shouldn't be visible on their declaration line
because they won't be initialized there.
Function arguments are treated as an exception.

This fix is only applied to programs compiled with Go 1.15 or later as
previous versions of Go did not report the correct declaration line for
variables captured by closures.

Fixes #1134

* proc: silence go vet error

* Makefile: enable PIE tests on windows/Go 1.15

* core: support core files for PIEs on windows

* goversion: add Go 1.15 to supported versions

* proc: fix function call injection for Go 1.15

Go 1.15 changed the call injection protocol so that the runtime will
execute the injected call on a different (new) goroutine.

This commit changes the function call support in delve to:

1. correctly track down the call injection state after the runtime
   switches to a different goroutine.
2. correctly perform the escapeCheck when stack values can come from
   multiple goroutine stacks.

* proc: miscellaneous fixed for call injection under macOS with go 1.15

- create copy of SP in debugCallAXCompleteCall case because the code
  used to assume that regs doesn't change
- fix automatic address calculation for function arguments when an
  argument has a spurious DW_OP_piece at entry
This commit is contained in:
Alessandro Arzilli 2020-07-28 18:19:51 +02:00 committed by GitHub
parent 3d896ece07
commit f9c8f7f55b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 267 additions and 105 deletions

@ -2,14 +2,15 @@ package main
import ( import (
"fmt" "fmt"
"runtime"
) )
func main() { func main() {
a := 0 a := 0
runtime.Breakpoint()
a++ a++
b := 0 b := 0
runtime.Breakpoint() f1(a, b)
fmt.Println(a, b) }
func f1(a, b int) {
fmt.Printf("%d %d\n", a, b)
} }

@ -20,8 +20,8 @@ type Nest struct {
} }
func barfoo() { func barfoo() {
runtime.Breakpoint()
a1 := "bur" a1 := "bur"
runtime.Breakpoint()
fmt.Println(a1) fmt.Println(a1)
} }

@ -70,15 +70,7 @@ func NewMakeCommands() *cobra.Command {
Short: "Tests delve", Short: "Tests delve",
Long: `Tests delve. Long: `Tests delve.
Use the flags -s, -r and -b to specify which tests to run. Specifying nothing is equivalent to: Use the flags -s, -r and -b to specify which tests to run. Specifying nothing will run all tests relevant for the current environment (see testStandard).
go run _scripts/make.go test -s all -b default
go run _scripts/make.go test -s basic -b lldb # if lldb-server is installed and Go < 1.14
go run _scripts/make.go test -s basic -b rr # if rr is installed
go run _scripts/make.go test -s basic -m pie # only on linux
go run _scripts/make.go test -s core -m pie # only on linux
go run _scripts/make.go test -s
`, `,
Run: testCmd, Run: testCmd,
} }
@ -310,25 +302,7 @@ func testCmd(cmd *cobra.Command, args []string) {
os.Exit(1) os.Exit(1)
} }
fmt.Println("Testing default backend") testStandard()
testCmdIntl("all", "", "default", "normal")
if inpath("lldb-server") && !goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) {
fmt.Println("\nTesting LLDB backend")
testCmdIntl("basic", "", "lldb", "normal")
}
if inpath("rr") {
fmt.Println("\nTesting RR backend")
testCmdIntl("basic", "", "rr", "normal")
}
if runtime.GOOS == "linux" {
fmt.Println("\nTesting PIE buildmode, default backend")
testCmdIntl("basic", "", "default", "pie")
testCmdIntl("core", "", "default", "pie")
}
if runtime.GOOS == "linux" && inpath("rr") {
fmt.Println("\nTesting PIE buildmode, RR backend")
testCmdIntl("basic", "", "rr", "pie")
}
return return
} }
@ -347,6 +321,28 @@ func testCmd(cmd *cobra.Command, args []string) {
testCmdIntl(TestSet, TestRegex, TestBackend, TestBuildMode) testCmdIntl(TestSet, TestRegex, TestBackend, TestBuildMode)
} }
func testStandard() {
fmt.Println("Testing default backend")
testCmdIntl("all", "", "default", "normal")
if inpath("lldb-server") && !goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) {
fmt.Println("\nTesting LLDB backend")
testCmdIntl("basic", "", "lldb", "normal")
}
if inpath("rr") {
fmt.Println("\nTesting RR backend")
testCmdIntl("basic", "", "rr", "normal")
}
if runtime.GOOS == "linux" || (runtime.GOOS == "windows" && goversion.VersionAfterOrEqual(runtime.Version(), 1, 15)) {
fmt.Println("\nTesting PIE buildmode, default backend")
testCmdIntl("basic", "", "default", "pie")
testCmdIntl("core", "", "default", "pie")
}
if runtime.GOOS == "linux" && inpath("rr") {
fmt.Println("\nTesting PIE buildmode, RR backend")
testCmdIntl("basic", "", "rr", "pie")
}
}
func testCmdIntl(testSet, testRegex, testBackend, testBuildMode string) { func testCmdIntl(testSet, testRegex, testBackend, testBuildMode string) {
testPackages := testSetToPackages(testSet) testPackages := testSetToPackages(testSet)
if len(testPackages) == 0 { if len(testPackages) == 0 {

@ -11,29 +11,45 @@ type Variable struct {
Depth int Depth int
} }
// VariablesFlags specifies some configuration flags for the Variables function.
type VariablesFlags uint8
const (
VariablesOnlyVisible VariablesFlags = 1 << iota
VariablesSkipInlinedSubroutines
VariablesTrustDeclLine
)
// Variables returns a list of variables contained inside 'root'. // Variables returns a list of variables contained inside 'root'.
// If onlyVisible is true only variables visible at pc will be returned. // If onlyVisible is true only variables visible at pc will be returned.
// If skipInlinedSubroutines is true inlined subroutines will be skipped // If skipInlinedSubroutines is true inlined subroutines will be skipped
func Variables(root *godwarf.Tree, pc uint64, line int, onlyVisible, skipInlinedSubroutines bool) []Variable { func Variables(root *godwarf.Tree, pc uint64, line int, flags VariablesFlags) []Variable {
return variablesInternal(nil, root, 0, pc, line, onlyVisible, skipInlinedSubroutines) return variablesInternal(nil, root, 0, pc, line, flags)
} }
func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, line int, onlyVisible, skipInlinedSubroutines bool) []Variable { func variablesInternal(v []Variable, root *godwarf.Tree, depth int, pc uint64, line int, flags VariablesFlags) []Variable {
switch root.Tag { switch root.Tag {
case dwarf.TagInlinedSubroutine: case dwarf.TagInlinedSubroutine:
if skipInlinedSubroutines { if flags&VariablesSkipInlinedSubroutines != 0 {
return v return v
} }
fallthrough fallthrough
case dwarf.TagLexDwarfBlock, dwarf.TagSubprogram: case dwarf.TagLexDwarfBlock, dwarf.TagSubprogram:
if !onlyVisible || root.ContainsPC(pc) { if (flags&VariablesOnlyVisible == 0) || root.ContainsPC(pc) {
for _, child := range root.Children { for _, child := range root.Children {
v = variablesInternal(v, child, depth+1, pc, line, onlyVisible, skipInlinedSubroutines) v = variablesInternal(v, child, depth+1, pc, line, flags)
} }
} }
return v return v
default: default:
if declLine, ok := root.Val(dwarf.AttrDeclLine).(int64); !ok || line >= int(declLine) { o := 0
if root.Tag != dwarf.TagFormalParameter && (flags&VariablesTrustDeclLine != 0) {
// visibility for variables starts the line after declaration line,
// except for formal parameters, which are visible on the same line they
// are defined.
o = 1
}
if declLine, ok := root.Val(dwarf.AttrDeclLine).(int64); !ok || line >= int(declLine)+o {
return append(v, Variable{root, depth}) return append(v, Variable{root, depth})
} }
return v return v

@ -8,7 +8,7 @@ var (
MinSupportedVersionOfGoMajor = 1 MinSupportedVersionOfGoMajor = 1
MinSupportedVersionOfGoMinor = 12 MinSupportedVersionOfGoMinor = 12
MaxSupportedVersionOfGoMajor = 1 MaxSupportedVersionOfGoMajor = 1
MaxSupportedVersionOfGoMinor = 14 MaxSupportedVersionOfGoMinor = 15
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) 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) 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)
) )

@ -28,6 +28,8 @@ func arm64AsmDecode(asmInst *AsmInstruction, mem []byte, regs Registers, memrw M
asmInst.Kind = RetInstruction asmInst.Kind = RetInstruction
case arm64asm.B, arm64asm.BR: case arm64asm.B, arm64asm.BR:
asmInst.Kind = JmpInstruction asmInst.Kind = JmpInstruction
case arm64asm.BRK:
asmInst.Kind = HardBreakInstruction
} }
asmInst.DestLoc = resolveCallArgARM64(&inst, asmInst.Loc.PC, asmInst.AtPC, regs, memrw, bi) asmInst.DestLoc = resolveCallArgARM64(&inst, asmInst.Loc.PC, asmInst.AtPC, regs, memrw, bi)

@ -28,10 +28,16 @@ func readAMD64Minidump(minidumpPath, exePath string) (*process, error) {
memory.Add(m, uintptr(m.Addr), uintptr(len(m.Data))) memory.Add(m, uintptr(m.Addr), uintptr(len(m.Data)))
} }
entryPoint := uint64(0)
if len(mdmp.Modules) > 0 {
entryPoint = mdmp.Modules[0].BaseOfImage
}
p := &process{ p := &process{
mem: memory, mem: memory,
Threads: map[int]*thread{}, Threads: map[int]*thread{},
bi: proc.NewBinaryInfo("windows", "amd64"), bi: proc.NewBinaryInfo("windows", "amd64"),
entryPoint: entryPoint,
breakpoints: proc.NewBreakpointMap(), breakpoints: proc.NewBreakpointMap(),
pid: int(mdmp.Pid), pid: int(mdmp.Pid),
} }

@ -23,6 +23,7 @@ const (
CallInstruction CallInstruction
RetInstruction RetInstruction
JmpInstruction JmpInstruction
HardBreakInstruction
) )
// IsCall is true if instr is a call instruction. // IsCall is true if instr is a call instruction.
@ -40,6 +41,11 @@ func (instr *AsmInstruction) IsJmp() bool {
return instr.Kind == JmpInstruction return instr.Kind == JmpInstruction
} }
// IsHardBreak is true if instr is a hardcoded breakpoint instruction.
func (instr *AsmInstruction) IsHardBreak() bool {
return instr.Kind == HardBreakInstruction
}
type archInst interface { type archInst interface {
Text(flavour AssemblyFlavour, pc uint64, symLookup func(uint64) (string, uint64)) string Text(flavour AssemblyFlavour, pc uint64, symLookup func(uint64) (string, uint64)) string
OpcodeEquals(op uint64) bool OpcodeEquals(op uint64) bool

@ -215,7 +215,12 @@ func (scope *EvalScope) Locals() ([]*Variable, error) {
return nil, err return nil, err
} }
varEntries := reader.Variables(dwarfTree, scope.PC, scope.Line, true, false) variablesFlags := reader.VariablesOnlyVisible
if scope.BinInfo.Producer() != "" && goversion.ProducerAfterOrEqual(scope.BinInfo.Producer(), 1, 15) {
variablesFlags |= reader.VariablesTrustDeclLine
}
varEntries := reader.Variables(dwarfTree, scope.PC, scope.Line, variablesFlags)
vars := make([]*Variable, 0, len(varEntries)) vars := make([]*Variable, 0, len(varEntries))
depths := make([]int, 0, len(varEntries)) depths := make([]int, 0, len(varEntries))
for _, entry := range varEntries { for _, entry := range varEntries {
@ -224,7 +229,7 @@ func (scope *EvalScope) Locals() ([]*Variable, error) {
// skip variables that we can't parse yet // skip variables that we can't parse yet
continue continue
} }
if trustArgOrder && val.Unreadable != nil && val.Addr == 0 && entry.Tag == dwarf.TagFormalParameter { if trustArgOrder && ((val.Unreadable != nil && val.Addr == 0) || val.Flags&VariableFakeAddress != 0) && entry.Tag == dwarf.TagFormalParameter {
addr := afterLastArgAddr(vars) addr := afterLastArgAddr(vars)
if addr == 0 { if addr == 0 {
addr = uintptr(scope.Regs.CFA) addr = uintptr(scope.Regs.CFA)
@ -653,7 +658,7 @@ func (scope *EvalScope) evalToplevelTypeCast(t ast.Expr, cfg LoadConfig) (*Varia
return v, nil return v, nil
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr: case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint, reflect.Uintptr:
b, _ := constant.Int64Val(argv.Value) b, _ := constant.Int64Val(argv.Value)
s := string(b) s := string(rune(b))
v.Value = constant.MakeString(s) v.Value = constant.MakeString(s)
v.Len = int64(len(s)) v.Len = int64(len(s))
return v, nil return v, nil

@ -107,6 +107,14 @@ type callContext struct {
// continueRequest having cont == false and the return values in ret. // continueRequest having cont == false and the return values in ret.
continueRequest chan<- continueRequest continueRequest chan<- continueRequest
continueCompleted <-chan *G continueCompleted <-chan *G
// injectionThread is the thread to use for nested call injections if the
// original injection goroutine isn't running (because we are in Go 1.15)
injectionThread Thread
// stacks is a slice of known goroutine stacks used to check for
// inappropriate escapes
stacks []stack
} }
type continueRequest struct { type continueRequest struct {
@ -121,6 +129,7 @@ type callInjection struct {
// pkg/proc/fncall.go for a description of how this works. // pkg/proc/fncall.go for a description of how this works.
continueCompleted chan<- *G continueCompleted chan<- *G
continueRequest <-chan continueRequest continueRequest <-chan continueRequest
startThreadID int
} }
func (callCtx *callContext) doContinue() *G { func (callCtx *callContext) doContinue() *G {
@ -178,8 +187,9 @@ func EvalExpressionWithCalls(t *Target, g *G, expr string, retLoadCfg LoadConfig
} }
t.fncallForG[g.ID] = &callInjection{ t.fncallForG[g.ID] = &callInjection{
continueCompleted, continueCompleted: continueCompleted,
continueRequest, continueRequest: continueRequest,
startThreadID: 0,
} }
go scope.EvalExpression(expr, retLoadCfg) go scope.EvalExpression(expr, retLoadCfg)
@ -193,7 +203,7 @@ func EvalExpressionWithCalls(t *Target, g *G, expr string, retLoadCfg LoadConfig
} }
func finishEvalExpressionWithCalls(t *Target, g *G, contReq continueRequest, ok bool) error { func finishEvalExpressionWithCalls(t *Target, g *G, contReq continueRequest, ok bool) error {
fncallLog("stashing return values for %d in thread=%d\n", g.ID, g.Thread.ThreadID()) fncallLog("stashing return values for %d in thread=%d", g.ID, g.Thread.ThreadID())
var err error var err error
if !ok { if !ok {
err = errors.New("internal error EvalExpressionWithCalls didn't return anything") err = errors.New("internal error EvalExpressionWithCalls didn't return anything")
@ -239,6 +249,23 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) {
if scope.callCtx == nil { if scope.callCtx == nil {
return nil, errFuncCallNotAllowed return nil, errFuncCallNotAllowed
} }
thread := scope.g.Thread
stacklo := scope.g.stack.lo
if thread == nil {
// We are doing a nested function call and using Go 1.15, the original
// injection goroutine was suspended and now we are using a different
// goroutine, evaluation still happend on the original goroutine but we
// need to use a different thread to do the nested call injection.
thread = scope.callCtx.injectionThread
g2, err := GetG(thread)
if err != nil {
return nil, err
}
stacklo = g2.stack.lo
}
if thread == nil {
return nil, errGoroutineNotRunning
}
p := scope.callCtx.p p := scope.callCtx.p
bi := scope.BinInfo bi := scope.BinInfo
@ -252,7 +279,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) {
} }
// check that there are at least 256 bytes free on the stack // check that there are at least 256 bytes free on the stack
regs, err := scope.g.Thread.Registers() regs, err := thread.Registers()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -260,7 +287,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if regs.SP()-256 <= scope.g.stack.lo { if regs.SP()-256 <= stacklo {
return nil, errNotEnoughStack return nil, errNotEnoughStack
} }
_, err = regs.Get(int(x86asm.RAX)) _, err = regs.Get(int(x86asm.RAX))
@ -278,22 +305,37 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) {
return nil, err return nil, err
} }
if err := callOP(bi, scope.g.Thread, regs, dbgcallfn.Entry); err != nil { if err := callOP(bi, thread, regs, dbgcallfn.Entry); err != nil {
return nil, err return nil, err
} }
// write the desired argument frame size at SP-(2*pointer_size) (the extra pointer is the saved PC) // write the desired argument frame size at SP-(2*pointer_size) (the extra pointer is the saved PC)
if err := writePointer(bi, scope.g.Thread, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(fncall.argFrameSize)); err != nil { if err := writePointer(bi, thread, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(fncall.argFrameSize)); err != nil {
return nil, err return nil, err
} }
fncallLog("function call initiated %v frame size %d", fncall.fn, fncall.argFrameSize) fncallLog("function call initiated %v frame size %d goroutine %d (thread %d)", fncall.fn, fncall.argFrameSize, scope.g.ID, thread.ThreadID())
p.fncallForG[scope.g.ID].startThreadID = thread.ThreadID()
spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(scope.g.stack.hi) spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(scope.g.stack.hi)
bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(scope.g.stack.hi) bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(scope.g.stack.hi)
fboff := scope.Regs.FrameBase - int64(scope.g.stack.hi) fboff := scope.Regs.FrameBase - int64(scope.g.stack.hi)
for { for {
scope.g = scope.callCtx.doContinue() scope.callCtx.injectionThread = nil
g := scope.callCtx.doContinue()
// Go 1.15 will move call injection execution to a different goroutine,
// but we want to keep evaluation on the original goroutine.
if g.ID == scope.g.ID {
scope.g = g
} else {
// We are in Go 1.15 and we switched to a new goroutine, the original
// goroutine is now parked and therefore does not have a thread
// associated.
scope.g.Thread = nil
scope.g.Status = Gwaiting
scope.callCtx.injectionThread = g.Thread
}
// adjust the value of registers inside scope // adjust the value of registers inside scope
pcreg, bpreg, spreg := scope.Regs.Reg(scope.Regs.PCRegNum), scope.Regs.Reg(scope.Regs.BPRegNum), scope.Regs.Reg(scope.Regs.SPRegNum) pcreg, bpreg, spreg := scope.Regs.Reg(scope.Regs.PCRegNum), scope.Regs.Reg(scope.Regs.BPRegNum), scope.Regs.Reg(scope.Regs.SPRegNum)
@ -306,7 +348,7 @@ func evalFunctionCall(scope *EvalScope, node *ast.CallExpr) (*Variable, error) {
scope.Regs.FrameBase = fboff + int64(scope.g.stack.hi) scope.Regs.FrameBase = fboff + int64(scope.g.stack.hi)
scope.Regs.CFA = scope.frameOffset + int64(scope.g.stack.hi) scope.Regs.CFA = scope.frameOffset + int64(scope.g.stack.hi)
finished := funcCallStep(scope, &fncall) finished := funcCallStep(scope, &fncall, g.Thread)
if finished { if finished {
break break
} }
@ -500,9 +542,14 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr
func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg *Variable, formalArg *funcCallArg, argFrameAddr uint64) error { func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg *Variable, formalArg *funcCallArg, argFrameAddr uint64) error {
if scope.callCtx.checkEscape { if scope.callCtx.checkEscape {
//TODO(aarzilli): only apply the escapeCheck to leaking parameters. //TODO(aarzilli): only apply the escapeCheck to leaking parameters.
if err := escapeCheck(actualArg, formalArg.name, scope.g); err != nil { if err := escapeCheck(actualArg, formalArg.name, scope.g.stack); err != nil {
return fmt.Errorf("cannot use %s as argument %s in function %s: %v", actualArg.Name, formalArg.name, fncall.fn.Name, err) return fmt.Errorf("cannot use %s as argument %s in function %s: %v", actualArg.Name, formalArg.name, fncall.fn.Name, err)
} }
for _, stack := range scope.callCtx.stacks {
if err := escapeCheck(actualArg, formalArg.name, stack); err != nil {
return fmt.Errorf("cannot use %s as argument %s in function %s: %v", actualArg.Name, formalArg.name, fncall.fn.Name, err)
}
}
} }
//TODO(aarzilli): autmoatic wrapping in interfaces for cases not handled //TODO(aarzilli): autmoatic wrapping in interfaces for cases not handled
@ -524,7 +571,7 @@ func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize i
return 0, nil, fmt.Errorf("DWARF read error: %v", err) return 0, nil, fmt.Errorf("DWARF read error: %v", err)
} }
varEntries := reader.Variables(dwarfTree, fn.Entry, int(^uint(0)>>1), false, true) varEntries := reader.Variables(dwarfTree, fn.Entry, int(^uint(0)>>1), reader.VariablesSkipInlinedSubroutines)
trustArgOrder := bi.Producer() != "" && goversion.ProducerAfterOrEqual(bi.Producer(), 1, 12) trustArgOrder := bi.Producer() != "" && goversion.ProducerAfterOrEqual(bi.Producer(), 1, 12)
@ -590,7 +637,7 @@ func alignAddr(addr, align int64) int64 {
return (addr + int64(align-1)) &^ int64(align-1) return (addr + int64(align-1)) &^ int64(align-1)
} }
func escapeCheck(v *Variable, name string, g *G) error { func escapeCheck(v *Variable, name string, stack stack) error {
switch v.Kind { switch v.Kind {
case reflect.Ptr: case reflect.Ptr:
var w *Variable var w *Variable
@ -600,31 +647,31 @@ func escapeCheck(v *Variable, name string, g *G) error {
} else { } else {
w = v.maybeDereference() w = v.maybeDereference()
} }
return escapeCheckPointer(w.Addr, name, g) return escapeCheckPointer(w.Addr, name, stack)
case reflect.Chan, reflect.String, reflect.Slice: case reflect.Chan, reflect.String, reflect.Slice:
return escapeCheckPointer(v.Base, name, g) return escapeCheckPointer(v.Base, name, stack)
case reflect.Map: case reflect.Map:
sv := v.clone() sv := v.clone()
sv.RealType = resolveTypedef(&(v.RealType.(*godwarf.MapType).TypedefType)) sv.RealType = resolveTypedef(&(v.RealType.(*godwarf.MapType).TypedefType))
sv = sv.maybeDereference() sv = sv.maybeDereference()
return escapeCheckPointer(sv.Addr, name, g) return escapeCheckPointer(sv.Addr, name, stack)
case reflect.Struct: case reflect.Struct:
t := v.RealType.(*godwarf.StructType) t := v.RealType.(*godwarf.StructType)
for _, field := range t.Field { for _, field := range t.Field {
fv, _ := v.toField(field) fv, _ := v.toField(field)
if err := escapeCheck(fv, fmt.Sprintf("%s.%s", name, field.Name), g); err != nil { if err := escapeCheck(fv, fmt.Sprintf("%s.%s", name, field.Name), stack); err != nil {
return err return err
} }
} }
case reflect.Array: case reflect.Array:
for i := int64(0); i < v.Len; i++ { for i := int64(0); i < v.Len; i++ {
sv, _ := v.sliceAccess(int(i)) sv, _ := v.sliceAccess(int(i))
if err := escapeCheck(sv, fmt.Sprintf("%s[%d]", name, i), g); err != nil { if err := escapeCheck(sv, fmt.Sprintf("%s[%d]", name, i), stack); err != nil {
return err return err
} }
} }
case reflect.Func: case reflect.Func:
if err := escapeCheckPointer(uintptr(v.funcvalAddr()), name, g); err != nil { if err := escapeCheckPointer(uintptr(v.funcvalAddr()), name, stack); err != nil {
return err return err
} }
} }
@ -632,8 +679,8 @@ func escapeCheck(v *Variable, name string, g *G) error {
return nil return nil
} }
func escapeCheckPointer(addr uintptr, name string, g *G) error { func escapeCheckPointer(addr uintptr, name string, stack stack) error {
if uint64(addr) >= g.stack.lo && uint64(addr) < g.stack.hi { if uint64(addr) >= stack.lo && uint64(addr) < stack.hi {
return fmt.Errorf("stack object passed to escaping pointer: %s", name) return fmt.Errorf("stack object passed to escaping pointer: %s", name)
} }
return nil return nil
@ -648,21 +695,15 @@ const (
) )
// funcCallStep executes one step of the function call injection protocol. // funcCallStep executes one step of the function call injection protocol.
func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool { func funcCallStep(callScope *EvalScope, fncall *functionCallState, thread Thread) bool {
p := callScope.callCtx.p p := callScope.callCtx.p
bi := p.BinInfo() bi := p.BinInfo()
thread := callScope.g.Thread
regs, err := thread.Registers() regs, err := thread.Registers()
if err != nil { if err != nil {
fncall.err = err fncall.err = err
return true return true
} }
regs, err = regs.Copy()
if err != nil {
fncall.err = err
return true
}
rax, _ := regs.Get(int(x86asm.RAX)) rax, _ := regs.Get(int(x86asm.RAX))
@ -676,7 +717,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
fnname = loc.Fn.Name fnname = loc.Fn.Name
} }
} }
fncallLog("function call interrupt gid=%d 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 rax=%#x (PC=%#x in %s)", callScope.g.ID, thread.ThreadID(), rax, pc, fnname)
} }
switch rax { switch rax {
@ -691,6 +732,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
fncall.err = fmt.Errorf("%v", constant.StringVal(errvar.Value)) fncall.err = fmt.Errorf("%v", constant.StringVal(errvar.Value))
case debugCallAXCompleteCall: case debugCallAXCompleteCall:
p.fncallForG[callScope.g.ID].startThreadID = 0
// evaluate arguments of the target function, copy them into its argument frame and call the function // 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 { if fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0 {
// if we couldn't figure out which function we are calling before // if we couldn't figure out which function we are calling before
@ -719,13 +761,15 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
// address of the function pointer itself. // address of the function pointer itself.
thread.SetDX(fncall.closureAddr) thread.SetDX(fncall.closureAddr)
} }
cfa := regs.SP()
oldpc := regs.PC()
callOP(bi, thread, regs, fncall.fn.Entry) callOP(bi, thread, regs, fncall.fn.Entry)
err := funcCallEvalArgs(callScope, fncall, regs.SP()) err := funcCallEvalArgs(callScope, fncall, cfa)
if err != nil { if err != nil {
// rolling back the call, note: this works because we called regs.Copy() above // rolling back the call, note: this works because we called regs.Copy() above
thread.SetSP(regs.SP()) thread.SetSP(cfa)
thread.SetPC(regs.PC()) thread.SetPC(oldpc)
fncall.err = err fncall.err = err
fncall.lateCallFailure = true fncall.lateCallFailure = true
break break
@ -777,6 +821,13 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
v.Flags |= VariableFakeAddress v.Flags |= VariableFakeAddress
} }
// Store the stack span of the currently running goroutine (which in Go >=
// 1.15 might be different from the original injection goroutine) so that
// later on we can use it to perform the escapeCheck
if threadg, _ := GetG(thread); threadg != nil {
callScope.callCtx.stacks = append(callScope.callCtx.stacks, threadg.stack)
}
case debugCallAXReadPanic: case debugCallAXReadPanic:
// read panic value from stack // read panic value from stack
fncall.panicvar, err = readTopstackVariable(thread, regs, "interface {}", callScope.callCtx.retLoadCfg) fncall.panicvar, err = readTopstackVariable(thread, regs, "interface {}", callScope.callCtx.retLoadCfg)
@ -886,11 +937,18 @@ func allocString(scope *EvalScope, v *Variable) error {
return err return err
} }
func isCallInjectionStop(loc *Location) bool { func isCallInjectionStop(t *Target, thread Thread, loc *Location) bool {
if loc.Fn == nil { if loc.Fn == nil {
return false return false
} }
return strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) || strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2) if !strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) && !strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2) {
return false
}
text, err := disassembleCurrentInstruction(t, thread, -1)
if err != nil || len(text) <= 0 {
return false
}
return text[0].IsHardBreak()
} }
// callInjectionProtocol is the function called from Continue to progress // callInjectionProtocol is the function called from Continue to progress
@ -906,19 +964,16 @@ func callInjectionProtocol(t *Target, threads []Thread) (done bool, err error) {
if err != nil { if err != nil {
continue continue
} }
if !isCallInjectionStop(loc) { if !isCallInjectionStop(t, thread, loc) {
continue continue
} }
g, err := GetG(thread) g, callinj, err := findCallInjectionStateForThread(t, thread)
if err != nil { if err != nil {
return done, fmt.Errorf("could not determine running goroutine for thread %#x currently executing the function call injection protocol: %v", thread.ThreadID(), err) return false, err
} }
callinj := t.fncallForG[g.ID]
if callinj == nil || callinj.continueCompleted == nil { fncallLog("step for injection on goroutine %d (current) thread=%d (location %s)", g.ID, thread.ThreadID(), loc.Fn.Name)
return false, fmt.Errorf("could not recover call injection state for goroutine %d", g.ID)
}
fncallLog("step for injection on goroutine %d thread=%d (location %s)", g.ID, thread.ThreadID(), loc.Fn.Name)
callinj.continueCompleted <- g callinj.continueCompleted <- g
contReq, ok := <-callinj.continueRequest contReq, ok := <-callinj.continueRequest
if !contReq.cont { if !contReq.cont {
@ -931,3 +986,36 @@ func callInjectionProtocol(t *Target, threads []Thread) (done bool, err error) {
} }
return done, nil return done, nil
} }
func findCallInjectionStateForThread(t *Target, thread Thread) (*G, *callInjection, error) {
g, err := GetG(thread)
if err != nil {
return nil, nil, fmt.Errorf("could not determine running goroutine for thread %#x currently executing the function call injection protocol: %v", thread.ThreadID(), err)
}
fncallLog("findCallInjectionStateForThread thread=%d goroutine=%d", thread.ThreadID(), g.ID)
notfound := func() error {
return fmt.Errorf("could not recover call injection state for goroutine %d (thread %d)", g.ID, thread.ThreadID())
}
callinj := t.fncallForG[g.ID]
if callinj != nil {
if callinj.continueCompleted == nil {
return nil, nil, notfound()
}
return g, callinj, nil
}
// In Go 1.15 and later the call injection protocol will switch to a
// different goroutine.
// Here we try to recover the injection goroutine by checking the injection
// thread.
for goid, callinj := range t.fncallForG {
if callinj != nil && callinj.continueCompleted != nil && callinj.startThreadID != 0 && callinj.startThreadID == thread.ThreadID() {
t.fncallForG[g.ID] = callinj
fncallLog("goroutine %d is the goroutine executing the call injection started in goroutine %d", g.ID, goid)
return g, callinj, nil
}
}
return nil, nil, notfound()
}

@ -15,6 +15,7 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"sort"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -3507,6 +3508,30 @@ func TestIssue1008(t *testing.T) {
}) })
} }
func testDeclLineCount(t *testing.T, p *proc.Target, lineno int, tgtvars []string) {
sort.Strings(tgtvars)
assertLineNumber(p, t, lineno, "Program did not continue to correct next location")
scope, err := proc.GoroutineScope(p.CurrentThread())
assertNoError(err, t, fmt.Sprintf("GoroutineScope (:%d)", lineno))
vars, err := scope.Locals()
assertNoError(err, t, fmt.Sprintf("Locals (:%d)", lineno))
if len(vars) != len(tgtvars) {
t.Fatalf("wrong number of variables %d (:%d)", len(vars), lineno)
}
outvars := make([]string, len(vars))
for i, v := range vars {
outvars[i] = v.Name
}
sort.Strings(outvars)
for i := range outvars {
if tgtvars[i] != outvars[i] {
t.Fatalf("wrong variables, got: %q expected %q\n", outvars, tgtvars)
}
}
}
func TestDeclLine(t *testing.T) { func TestDeclLine(t *testing.T) {
ver, _ := goversion.Parse(runtime.Version()) ver, _ := goversion.Parse(runtime.Version())
if ver.Major > 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 10, Rev: -1}) { if ver.Major > 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 10, Rev: -1}) {
@ -3514,24 +3539,34 @@ func TestDeclLine(t *testing.T) {
} }
withTestProcess("decllinetest", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("decllinetest", t, func(p *proc.Target, fixture protest.Fixture) {
setFileBreakpoint(p, t, fixture.Source, 8)
setFileBreakpoint(p, t, fixture.Source, 9)
setFileBreakpoint(p, t, fixture.Source, 10)
setFileBreakpoint(p, t, fixture.Source, 11)
setFileBreakpoint(p, t, fixture.Source, 14)
assertNoError(p.Continue(), t, "Continue") assertNoError(p.Continue(), t, "Continue")
scope, err := proc.GoroutineScope(p.CurrentThread()) if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
assertNoError(err, t, "GoroutineScope (1)") testDeclLineCount(t, p, 8, []string{})
vars, err := scope.LocalVariables(normalLoadConfig) } else {
assertNoError(err, t, "LocalVariables (1)") testDeclLineCount(t, p, 8, []string{"a"})
if len(vars) != 1 {
t.Fatalf("wrong number of variables %d", len(vars))
} }
assertNoError(p.Continue(), t, "Continue") assertNoError(p.Continue(), t, "Continue")
scope, err = proc.GoroutineScope(p.CurrentThread()) testDeclLineCount(t, p, 9, []string{"a"})
assertNoError(err, t, "GoroutineScope (2)")
scope.LocalVariables(normalLoadConfig) assertNoError(p.Continue(), t, "Continue")
vars, err = scope.LocalVariables(normalLoadConfig) if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
assertNoError(err, t, "LocalVariables (2)") testDeclLineCount(t, p, 10, []string{"a"})
if len(vars) != 2 { } else {
t.Fatalf("wrong number of variables %d", len(vars)) testDeclLineCount(t, p, 10, []string{"a", "b"})
} }
assertNoError(p.Continue(), t, "Continue")
testDeclLineCount(t, p, 11, []string{"a", "b"})
assertNoError(p.Continue(), t, "Continue")
testDeclLineCount(t, p, 14, []string{"a", "b"})
}) })
} }

@ -157,7 +157,7 @@ func (dbp *Target) Continue() error {
return err return err
} }
if dbp.GetDirection() == Forward { if dbp.GetDirection() == Forward {
text, err := disassembleCurrentInstruction(dbp, curthread) text, err := disassembleCurrentInstruction(dbp, curthread, 0)
if err != nil { if err != nil {
return err return err
} }
@ -248,12 +248,12 @@ func pickCurrentThread(dbp *Target, trapthread Thread, threads []Thread) error {
return dbp.SwitchThread(trapthread.ThreadID()) return dbp.SwitchThread(trapthread.ThreadID())
} }
func disassembleCurrentInstruction(p Process, thread Thread) ([]AsmInstruction, error) { func disassembleCurrentInstruction(p Process, thread Thread, off int64) ([]AsmInstruction, error) {
regs, err := thread.Registers() regs, err := thread.Registers()
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc := regs.PC() pc := regs.PC() + uint64(off)
return disassemble(thread, regs, p.Breakpoints(), p.BinInfo(), pc, pc+uint64(p.BinInfo().Arch.MaxInstructionLength()), true) return disassemble(thread, regs, p.Breakpoints(), p.BinInfo(), pc, pc+uint64(p.BinInfo().Arch.MaxInstructionLength()), true)
} }

@ -135,6 +135,8 @@ func BuildFixture(name string, flags BuildFlags) Fixture {
} }
if flags&BuildModePIE != 0 { if flags&BuildModePIE != 0 {
buildFlags = append(buildFlags, "-buildmode=pie") buildFlags = append(buildFlags, "-buildmode=pie")
} else {
buildFlags = append(buildFlags, "-buildmode=exe")
} }
if flags&BuildModePlugin != 0 { if flags&BuildModePlugin != 0 {
buildFlags = append(buildFlags, "-buildmode=plugin") buildFlags = append(buildFlags, "-buildmode=plugin")

@ -30,6 +30,8 @@ func x86AsmDecode(asmInst *AsmInstruction, mem []byte, regs Registers, memrw Mem
asmInst.Kind = CallInstruction asmInst.Kind = CallInstruction
case x86asm.RET, x86asm.LRET: case x86asm.RET, x86asm.LRET:
asmInst.Kind = RetInstruction asmInst.Kind = RetInstruction
case x86asm.INT:
asmInst.Kind = HardBreakInstruction
} }
asmInst.DestLoc = resolveCallArgX86(&inst, asmInst.Loc.PC, asmInst.AtPC, regs, memrw, bi) asmInst.DestLoc = resolveCallArgX86(&inst, asmInst.Loc.PC, asmInst.AtPC, regs, memrw, bi)

@ -687,7 +687,7 @@ func TestListCmd(t *testing.T) {
withTestTerminal("testvariables", t, func(term *FakeTerminal) { withTestTerminal("testvariables", t, func(term *FakeTerminal) {
term.MustExec("continue") term.MustExec("continue")
term.MustExec("continue") term.MustExec("continue")
listIsAt(t, term, "list", 24, 19, 29) listIsAt(t, term, "list", 25, 20, 30)
listIsAt(t, term, "list 69", 69, 64, 70) listIsAt(t, term, "list 69", 69, 64, 70)
listIsAt(t, term, "frame 1 list", 62, 57, 67) listIsAt(t, term, "frame 1 list", 62, 57, 67)
listIsAt(t, term, "frame 1 list 69", 69, 64, 70) listIsAt(t, term, "frame 1 list 69", 69, 64, 70)

@ -417,7 +417,7 @@ func Test1ClientServer_switchThread(t *testing.T) {
func Test1ClientServer_infoLocals(t *testing.T) { func Test1ClientServer_infoLocals(t *testing.T) {
withTestClient1("testnextprog", t, func(c *rpc1.RPCClient) { withTestClient1("testnextprog", t, func(c *rpc1.RPCClient) {
fp := testProgPath(t, "testnextprog") fp := testProgPath(t, "testnextprog")
_, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 23}) _, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 24})
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }

@ -548,7 +548,7 @@ func TestClientServer_infoLocals(t *testing.T) {
protest.AllowRecording(t) protest.AllowRecording(t)
withTestClient2("testnextprog", t, func(c service.Client) { withTestClient2("testnextprog", t, func(c service.Client) {
fp := testProgPath(t, "testnextprog") fp := testProgPath(t, "testnextprog")
_, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 23}) _, err := c.CreateBreakpoint(&api.Breakpoint{File: fp, Line: 24})
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }
@ -1771,6 +1771,9 @@ func TestClientServerFunctionCallStacktrace(t *testing.T) {
if runtime.GOARCH == "arm64" { if runtime.GOARCH == "arm64" {
t.Skip("arm64 does not support FunctionCall for now") t.Skip("arm64 does not support FunctionCall for now")
} }
if goversion.VersionAfterOrEqual(runtime.Version(), 1, 15) {
t.Skip("Go 1.15 executes function calls in a different goroutine so the stack trace will not contain main.main or runtime.main")
}
protest.MustSupportFunctionCalls(t, testBackend) protest.MustSupportFunctionCalls(t, testBackend)
withTestClient2("fncall", t, func(c service.Client) { withTestClient2("fncall", t, func(c service.Client) {
mustHaveDebugCalls(t, c) mustHaveDebugCalls(t, c)