proc: allow simultaneous call injection to multiple goroutines (#1591)

* proc: allow simultaneous call injection to multiple goroutines

Changes the call injection code so that we can have multiple call
injections going on at the same time as long as they happen on distinct
goroutines.

* proc: fix EvalExpressionWithCalls for constant expressions

The lack of address of constant expressions would confuse EvalExpressionWithCalls

Fixes #1577
This commit is contained in:
Alessandro Arzilli 2019-06-30 19:44:30 +02:00 committed by Derek Parker
parent 55eed318fd
commit dd4fd5dc9c
14 changed files with 196 additions and 129 deletions

@ -80,7 +80,7 @@ func uintExprCheck(t *testing.T, scope *proc.EvalScope, expr string, tgt uint64)
} }
func dwarfExprCheck(t *testing.T, mem proc.MemoryReadWriter, regs op.DwarfRegisters, bi *proc.BinaryInfo, testCases map[string]uint16, fn *proc.Function) *proc.EvalScope { func dwarfExprCheck(t *testing.T, mem proc.MemoryReadWriter, regs op.DwarfRegisters, bi *proc.BinaryInfo, testCases map[string]uint16, fn *proc.Function) *proc.EvalScope {
scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: fn}, Regs: regs, Mem: mem, Gvar: nil, BinInfo: bi} scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: fn}, Regs: regs, Mem: mem, BinInfo: bi}
for name, value := range testCases { for name, value := range testCases {
uintExprCheck(t, scope, name, uint64(value)) uintExprCheck(t, scope, name, uint64(value))
} }
@ -213,7 +213,7 @@ func TestDwarfExprLoclist(t *testing.T) {
mem := newFakeMemory(defaultCFA, uint16(before), uint16(after)) mem := newFakeMemory(defaultCFA, uint16(before), uint16(after))
regs := linutil.AMD64Registers{Regs: &linutil.AMD64PtraceRegs{}} regs := linutil.AMD64Registers{Regs: &linutil.AMD64PtraceRegs{}}
scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: mainfn}, Regs: dwarfRegisters(bi, &regs), Mem: mem, Gvar: nil, BinInfo: bi} scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: mainfn}, Regs: dwarfRegisters(bi, &regs), Mem: mem, BinInfo: bi}
uintExprCheck(t, scope, "a", before) uintExprCheck(t, scope, "a", before)
scope.PC = 0x40800 scope.PC = 0x40800
@ -247,7 +247,7 @@ func TestIssue1419(t *testing.T) {
mem := newFakeMemory(defaultCFA) mem := newFakeMemory(defaultCFA)
scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: mainfn}, Regs: op.DwarfRegisters{}, Mem: mem, Gvar: nil, BinInfo: bi} scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: mainfn}, Regs: op.DwarfRegisters{}, Mem: mem, BinInfo: bi}
va, err := scope.EvalExpression("a", normalLoadConfig) va, err := scope.EvalExpression("a", normalLoadConfig)
assertNoError(err, t, "EvalExpression(a)") assertNoError(err, t, "EvalExpression(a)")

@ -214,10 +214,10 @@ func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
// try to interpret the selector as a package variable // try to interpret the selector as a package variable
if maybePkg, ok := node.X.(*ast.Ident); ok { if maybePkg, ok := node.X.(*ast.Ident); ok {
if maybePkg.Name == "runtime" && node.Sel.Name == "curg" { if maybePkg.Name == "runtime" && node.Sel.Name == "curg" {
if scope.Gvar == nil { if scope.g == nil {
return nilVariable, nil return nilVariable, nil
} }
return scope.Gvar.clone(), nil return scope.g.variable.clone(), nil
} else if maybePkg.Name == "runtime" && node.Sel.Name == "frameoff" { } else if maybePkg.Name == "runtime" && node.Sel.Name == "frameoff" {
return newConstant(constant.MakeInt64(scope.frameOffset), scope.Mem), nil return newConstant(constant.MakeInt64(scope.frameOffset), scope.Mem), nil
} else if v, err := scope.findGlobal(maybePkg.Name + "." + node.Sel.Name); err == nil { } else if v, err := scope.findGlobal(maybePkg.Name + "." + node.Sel.Name); err == nil {

@ -11,6 +11,7 @@ import (
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
"strings"
"github.com/go-delve/delve/pkg/dwarf/godwarf" "github.com/go-delve/delve/pkg/dwarf/godwarf"
"github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/dwarf/op"
@ -105,7 +106,7 @@ type callContext struct {
// To signal that evaluation is completed a value will be written to // To signal that evaluation is completed a value will be written to
// 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 struct{} continueCompleted <-chan *G
} }
type continueRequest struct { type continueRequest struct {
@ -114,9 +115,9 @@ type continueRequest struct {
ret *Variable ret *Variable
} }
func (callCtx *callContext) doContinue() { func (callCtx *callContext) doContinue() *G {
callCtx.continueRequest <- continueRequest{cont: true} callCtx.continueRequest <- continueRequest{cont: true}
<-callCtx.continueCompleted return <-callCtx.continueCompleted
} }
func (callCtx *callContext) doReturn(ret *Variable, err error) { func (callCtx *callContext) doReturn(ret *Variable, err error) {
@ -129,12 +130,21 @@ func (callCtx *callContext) doReturn(ret *Variable, err error) {
// EvalExpressionWithCalls is like EvalExpression but allows function calls in 'expr'. // EvalExpressionWithCalls is like EvalExpression but allows function calls in 'expr'.
// Because this can only be done in the current goroutine, unlike // Because this can only be done in the current goroutine, unlike
// EvalExpression, EvalExpressionWithCalls is not a method of EvalScope. // EvalExpression, EvalExpressionWithCalls is not a method of EvalScope.
func EvalExpressionWithCalls(p Process, expr string, retLoadCfg LoadConfig, checkEscape bool) error { func EvalExpressionWithCalls(p Process, g *G, expr string, retLoadCfg LoadConfig, checkEscape bool) error {
bi := p.BinInfo() bi := p.BinInfo()
if !p.Common().fncallEnabled { if !p.Common().fncallEnabled {
return errFuncCallUnsupportedBackend return errFuncCallUnsupportedBackend
} }
if p.Common().continueCompleted != nil {
// check that the target goroutine is running
if g == nil {
return errNoGoroutine
}
if g.Status != Grunning || g.Thread == nil {
return errGoroutineNotRunning
}
if callinj := p.Common().fncallForG[g.ID]; callinj != nil && callinj.continueCompleted != nil {
return errFuncCallInProgress return errFuncCallInProgress
} }
@ -143,22 +153,13 @@ func EvalExpressionWithCalls(p Process, expr string, retLoadCfg LoadConfig, chec
return errFuncCallUnsupported return errFuncCallUnsupported
} }
// check that the selected goroutine is running scope, err := GoroutineScope(g.Thread)
g := p.SelectedGoroutine()
if g == nil {
return errNoGoroutine
}
if g.Status != Grunning || g.Thread == nil {
return errGoroutineNotRunning
}
scope, err := GoroutineScope(p.CurrentThread())
if err != nil { if err != nil {
return err return err
} }
continueRequest := make(chan continueRequest) continueRequest := make(chan continueRequest)
continueCompleted := make(chan struct{}) continueCompleted := make(chan *G)
scope.callCtx = &callContext{ scope.callCtx = &callContext{
p: p, p: p,
@ -168,8 +169,10 @@ func EvalExpressionWithCalls(p Process, expr string, retLoadCfg LoadConfig, chec
continueCompleted: continueCompleted, continueCompleted: continueCompleted,
} }
p.Common().continueRequest = continueRequest p.Common().fncallForG[g.ID] = &callInjection{
p.Common().continueCompleted = continueCompleted continueCompleted,
continueRequest,
}
go scope.EvalExpression(expr, retLoadCfg) go scope.EvalExpression(expr, retLoadCfg)
@ -178,35 +181,35 @@ func EvalExpressionWithCalls(p Process, expr string, retLoadCfg LoadConfig, chec
return Continue(p) return Continue(p)
} }
return finishEvalExpressionWithCalls(p, contReq, ok) return finishEvalExpressionWithCalls(p, g, contReq, ok)
} }
func finishEvalExpressionWithCalls(p Process, contReq continueRequest, ok bool) error { func finishEvalExpressionWithCalls(p Process, g *G, contReq continueRequest, ok bool) error {
fncallLog("stashing return values for %d in thread=%d\n", 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")
} else if contReq.err != nil { } else if contReq.err != nil {
if fpe, ispanic := contReq.err.(fncallPanicErr); ispanic { if fpe, ispanic := contReq.err.(fncallPanicErr); ispanic {
p.CurrentThread().Common().returnValues = []*Variable{fpe.panicVar} g.Thread.Common().returnValues = []*Variable{fpe.panicVar}
} else { } else {
err = contReq.err err = contReq.err
} }
} else if contReq.ret == nil { } else if contReq.ret == nil {
p.CurrentThread().Common().returnValues = nil g.Thread.Common().returnValues = nil
} else if contReq.ret.Addr == 0 && contReq.ret.DwarfType == nil { } else if contReq.ret.Addr == 0 && contReq.ret.DwarfType == nil && contReq.ret.Kind == reflect.Invalid {
// this is a variable returned by a function call with multiple return values // this is a variable returned by a function call with multiple return values
r := make([]*Variable, len(contReq.ret.Children)) r := make([]*Variable, len(contReq.ret.Children))
for i := range contReq.ret.Children { for i := range contReq.ret.Children {
r[i] = &contReq.ret.Children[i] r[i] = &contReq.ret.Children[i]
} }
p.CurrentThread().Common().returnValues = r g.Thread.Common().returnValues = r
} else { } else {
p.CurrentThread().Common().returnValues = []*Variable{contReq.ret} g.Thread.Common().returnValues = []*Variable{contReq.ret}
} }
p.Common().continueRequest = nil close(p.Common().fncallForG[g.ID].continueCompleted)
close(p.Common().continueCompleted) delete(p.Common().fncallForG, g.ID)
p.Common().continueCompleted = nil
return err return err
} }
@ -240,22 +243,13 @@ func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error)
return nil, errFuncCallUnsupported return nil, errFuncCallUnsupported
} }
// check that the selected goroutine is running
g := p.SelectedGoroutine()
if g == nil {
return nil, errNoGoroutine
}
if g.Status != Grunning || g.Thread == nil {
return nil, errGoroutineNotRunning
}
// 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 := g.Thread.Registers(true) regs, err := scope.g.Thread.Registers(true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
regs = regs.Copy() regs = regs.Copy()
if regs.SP()-256 <= g.stacklo { if regs.SP()-256 <= scope.g.stacklo {
return nil, errNotEnoughStack return nil, errNotEnoughStack
} }
_, err = regs.Get(int(x86asm.RAX)) _, err = regs.Get(int(x86asm.RAX))
@ -273,42 +267,39 @@ func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error)
return nil, err return nil, err
} }
if err := callOP(bi, g.Thread, regs, dbgcallfn.Entry); err != nil { if err := callOP(bi, scope.g.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, g.Thread, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(fncall.argFrameSize)); err != nil { if err := writePointer(bi, scope.g.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\n", fncall.fn, fncall.argFrameSize) fncallLog("function call initiated %v frame size %d", fncall.fn, fncall.argFrameSize)
spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(g.stackhi) spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(scope.g.stackhi)
bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(g.stackhi) bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(scope.g.stackhi)
fboff := scope.Regs.FrameBase - int64(g.stackhi) fboff := scope.Regs.FrameBase - int64(scope.g.stackhi)
for { for {
scope.callCtx.doContinue() scope.g = scope.callCtx.doContinue()
g = p.SelectedGoroutine() // adjust the value of registers inside scope
if g != nil { for regnum := range scope.Regs.Regs {
// adjust the value of registers inside scope switch uint64(regnum) {
for regnum := range scope.Regs.Regs { case scope.Regs.PCRegNum, scope.Regs.SPRegNum, scope.Regs.BPRegNum:
switch uint64(regnum) { // leave these alone
case scope.Regs.PCRegNum, scope.Regs.SPRegNum, scope.Regs.BPRegNum: default:
// leave these alone // every other register is dirty and unrecoverable
default: scope.Regs.Regs[regnum] = nil
// every other register is dirty and unrecoverable
scope.Regs.Regs[regnum] = nil
}
} }
scope.Regs.Regs[scope.Regs.SPRegNum].Uint64Val = uint64(spoff + int64(g.stackhi))
scope.Regs.Regs[scope.Regs.BPRegNum].Uint64Val = uint64(bpoff + int64(g.stackhi))
scope.Regs.FrameBase = fboff + int64(g.stackhi)
scope.Regs.CFA = scope.frameOffset + int64(g.stackhi)
} }
scope.Regs.Regs[scope.Regs.SPRegNum].Uint64Val = uint64(spoff + int64(scope.g.stackhi))
scope.Regs.Regs[scope.Regs.BPRegNum].Uint64Val = uint64(bpoff + int64(scope.g.stackhi))
scope.Regs.FrameBase = fboff + int64(scope.g.stackhi)
scope.Regs.CFA = scope.frameOffset + int64(scope.g.stackhi)
finished := funcCallStep(scope, &fncall) finished := funcCallStep(scope, &fncall)
if finished { if finished {
break break
@ -464,14 +455,13 @@ type funcCallArg struct {
// funcCallEvalArgs evaluates the arguments of the function call, copying // funcCallEvalArgs evaluates the arguments of the function call, copying
// the into the argument frame starting at argFrameAddr. // the into the argument frame starting at argFrameAddr.
func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr uint64) error { func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr uint64) error {
g := scope.callCtx.p.SelectedGoroutine() if scope.g == nil {
if g == nil {
// this should never happen // this should never happen
return errNoGoroutine return errNoGoroutine
} }
if fncall.receiver != nil { if fncall.receiver != nil {
err := funcCallCopyOneArg(g, scope, fncall, fncall.receiver, &fncall.formalArgs[0], argFrameAddr) err := funcCallCopyOneArg(scope, fncall, fncall.receiver, &fncall.formalArgs[0], argFrameAddr)
if err != nil { if err != nil {
return err return err
} }
@ -487,7 +477,7 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr
} }
actualArg.Name = exprToString(fncall.expr.Args[i]) actualArg.Name = exprToString(fncall.expr.Args[i])
err = funcCallCopyOneArg(g, scope, fncall, actualArg, formalArg, argFrameAddr) err = funcCallCopyOneArg(scope, fncall, actualArg, formalArg, argFrameAddr)
if err != nil { if err != nil {
return err return err
} }
@ -496,10 +486,10 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr
return nil return nil
} }
func funcCallCopyOneArg(g *G, 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, g); err != nil { if err := escapeCheck(actualArg, formalArg.name, scope.g); 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)
} }
} }
@ -631,7 +621,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
p := callScope.callCtx.p p := callScope.callCtx.p
bi := p.BinInfo() bi := p.BinInfo()
thread := p.CurrentThread() thread := callScope.g.Thread
regs, err := thread.Registers(false) regs, err := thread.Registers(false)
if err != nil { if err != nil {
fncall.err = err fncall.err = err
@ -651,7 +641,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
fnname = loc.Fn.Name fnname = loc.Fn.Name
} }
} }
fncallLog("function call interrupt rax=%#x (PC=%#x in %s)\n", rax, pc, fnname) fncallLog("function call interrupt gid=%d thread=%d rax=%#x (PC=%#x in %s)", callScope.g.ID, thread.ThreadID(), rax, pc, fnname)
} }
switch rax { switch rax {
@ -869,3 +859,49 @@ func (scope *EvalScope) allocString(v *Variable) error {
_, err = scope.Mem.WriteMemory(v.Base, []byte(constant.StringVal(v.Value))) _, err = scope.Mem.WriteMemory(v.Base, []byte(constant.StringVal(v.Value)))
return err return err
} }
func isCallInjectionStop(loc *Location) bool {
if loc.Fn == nil {
return false
}
return strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) || strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2)
}
// callInjectionProtocol is the function called from Continue to progress
// the injection protocol for all threads.
// Returns true if a call injection terminated
func callInjectionProtocol(p Process, threads []Thread) (done bool, err error) {
if len(p.Common().fncallForG) == 0 {
// we aren't injecting any calls, no need to check the threads.
return false, nil
}
for _, thread := range threads {
loc, err := thread.Location()
if err != nil {
continue
}
if !isCallInjectionStop(loc) {
continue
}
g, err := GetG(thread)
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)
}
callinj := p.Common().fncallForG[g.ID]
if callinj == nil || callinj.continueCompleted == nil {
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
contReq, ok := <-callinj.continueRequest
if !contReq.cont {
err := finishEvalExpressionWithCalls(p, g, contReq, ok)
if err != nil {
return done, err
}
done = true
}
}
return done, nil
}

@ -117,17 +117,21 @@ type CommonProcess struct {
allGCache []*G allGCache []*G
fncallEnabled bool fncallEnabled bool
fncallForG map[int]*callInjection
}
type callInjection struct {
// if continueCompleted is not nil it means we are in the process of // if continueCompleted is not nil it means we are in the process of
// executing an injected function call, see comments throughout // executing an injected function call, see comments throughout
// pkg/proc/fncall.go for a description of how this works. // pkg/proc/fncall.go for a description of how this works.
continueCompleted chan<- struct{} continueCompleted chan<- *G
continueRequest <-chan continueRequest continueRequest <-chan continueRequest
} }
// NewCommonProcess returns a struct with fields common across // NewCommonProcess returns a struct with fields common across
// all process implementations. // all process implementations.
func NewCommonProcess(fncallEnabled bool) CommonProcess { func NewCommonProcess(fncallEnabled bool) CommonProcess {
return CommonProcess{fncallEnabled: fncallEnabled} return CommonProcess{fncallEnabled: fncallEnabled, fncallForG: make(map[int]*callInjection)}
} }
// ClearAllGCache clears the cached contents of the cache for runtime.allgs. // ClearAllGCache clears the cached contents of the cache for runtime.allgs.

@ -8,7 +8,6 @@ import (
"go/token" "go/token"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
) )
// ErrNotExecutable is returned after attempting to execute a non-executable file // ErrNotExecutable is returned after attempting to execute a non-executable file
@ -190,6 +189,11 @@ func Continue(dbp Process) error {
threads := dbp.ThreadList() threads := dbp.ThreadList()
callInjectionDone, err := callInjectionProtocol(dbp, threads)
if err != nil {
return err
}
if err := pickCurrentThread(dbp, trapthread, threads); err != nil { if err := pickCurrentThread(dbp, trapthread, threads); err != nil {
return err return err
} }
@ -209,6 +213,7 @@ func Continue(dbp Process) error {
if err != nil || loc.Fn == nil { if err != nil || loc.Fn == nil {
return conditionErrors(threads) return conditionErrors(threads)
} }
g, _ := GetG(curthread)
switch { switch {
case loc.Fn.Name == "runtime.breakpoint": case loc.Fn.Name == "runtime.breakpoint":
@ -220,22 +225,8 @@ func Continue(dbp Process) error {
return err return err
} }
return conditionErrors(threads) return conditionErrors(threads)
case strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) || strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2): case g == nil || dbp.Common().fncallForG[g.ID] == nil:
continueCompleted := dbp.Common().continueCompleted // a hardcoded breakpoint somewhere else in the code (probably cgo)
if continueCompleted == nil {
return conditionErrors(threads)
}
continueCompleted <- struct{}{}
contReq, ok := <-dbp.Common().continueRequest
if !contReq.cont {
// only stop execution if the expression evaluation with calls finished
err := finishEvalExpressionWithCalls(dbp, contReq, ok)
if err != nil {
return err
}
return conditionErrors(threads)
}
default:
return conditionErrors(threads) return conditionErrors(threads)
} }
case curbp.Active && curbp.Internal: case curbp.Active && curbp.Internal:
@ -285,6 +276,11 @@ func Continue(dbp Process) error {
default: default:
// not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat // not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat
} }
if callInjectionDone {
// a call injection was finished, don't let a breakpoint with a failed
// condition or a step breakpoint shadow this.
return conditionErrors(threads)
}
} }
} }
@ -334,8 +330,10 @@ func stepInstructionOut(dbp Process, curthread Thread, fnname1, fnname2 string)
} }
loc, err := curthread.Location() loc, err := curthread.Location()
if err != nil || loc.Fn == nil || (loc.Fn.Name != fnname1 && loc.Fn.Name != fnname2) { if err != nil || loc.Fn == nil || (loc.Fn.Name != fnname1 && loc.Fn.Name != fnname2) {
if g := dbp.SelectedGoroutine(); g != nil { g, _ := GetG(curthread)
g.CurrentLoc = *loc selg := dbp.SelectedGoroutine()
if g != nil && selg != nil && g.ID == selg.ID {
selg.CurrentLoc = *loc
} }
return curthread.SetCurrentBreakpoint() return curthread.SetCurrentBreakpoint()
} }
@ -706,11 +704,6 @@ func ConvertEvalScope(dbp Process, gid, frame, deferCall int) (*EvalScope, error
// Otherwise all memory between frames[0].Regs.SP() and frames[0].Regs.CFA // Otherwise all memory between frames[0].Regs.SP() and frames[0].Regs.CFA
// will be cached. // will be cached.
func FrameToScope(bi *BinaryInfo, thread MemoryReadWriter, g *G, frames ...Stackframe) *EvalScope { func FrameToScope(bi *BinaryInfo, thread MemoryReadWriter, g *G, frames ...Stackframe) *EvalScope {
var gvar *Variable
if g != nil {
gvar = g.variable
}
// Creates a cacheMem that will preload the entire stack frame the first // Creates a cacheMem that will preload the entire stack frame the first
// time any local variable is read. // time any local variable is read.
// Remember that the stack grows downward in memory. // Remember that the stack grows downward in memory.
@ -725,7 +718,7 @@ func FrameToScope(bi *BinaryInfo, thread MemoryReadWriter, g *G, frames ...Stack
thread = cacheMemory(thread, uintptr(minaddr), int(maxaddr-minaddr)) thread = cacheMemory(thread, uintptr(minaddr), int(maxaddr-minaddr))
} }
s := &EvalScope{Location: frames[0].Call, Regs: frames[0].Regs, Mem: thread, Gvar: gvar, BinInfo: bi, frameOffset: frames[0].FrameOffset()} s := &EvalScope{Location: frames[0].Call, Regs: frames[0].Regs, Mem: thread, g: g, BinInfo: bi, frameOffset: frames[0].FrameOffset()}
s.PC = frames[0].lastpc s.PC = frames[0].lastpc
return s return s
} }

@ -4144,7 +4144,7 @@ func TestIssue1374(t *testing.T) {
setFileBreakpoint(p, t, fixture, 7) setFileBreakpoint(p, t, fixture, 7)
assertNoError(proc.Continue(p), t, "First Continue") assertNoError(proc.Continue(p), t, "First Continue")
assertLineNumber(p, t, 7, "Did not continue to correct location (first continue),") assertLineNumber(p, t, 7, "Did not continue to correct location (first continue),")
assertNoError(proc.EvalExpressionWithCalls(p, "getNum()", normalLoadConfig, true), t, "Call") assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "getNum()", normalLoadConfig, true), t, "Call")
err := proc.Continue(p) err := proc.Continue(p)
if _, isexited := err.(proc.ErrProcessExited); !isexited { if _, isexited := err.(proc.ErrProcessExited); !isexited {
regs, _ := p.CurrentThread().Registers(false) regs, _ := p.CurrentThread().Registers(false)
@ -4325,18 +4325,33 @@ func TestAncestors(t *testing.T) {
}) })
} }
func testCallConcurrentCheckReturns(p proc.Process, t *testing.T, gid1 int) bool { func testCallConcurrentCheckReturns(p proc.Process, t *testing.T, gid1, gid2 int) int {
found := 0
for _, thread := range p.ThreadList() { for _, thread := range p.ThreadList() {
g, _ := proc.GetG(thread) g, _ := proc.GetG(thread)
if g == nil || g.ID != gid1 { if g == nil || (g.ID != gid1 && g.ID != gid2) {
continue continue
} }
retvals := thread.Common().ReturnValues(normalLoadConfig) retvals := thread.Common().ReturnValues(normalLoadConfig)
if len(retvals) != 0 { if len(retvals) == 0 {
return true continue
}
n, _ := constant.Int64Val(retvals[0].Value)
t.Logf("injection on goroutine %d (thread %d) returned %v\n", g.ID, thread.ThreadID(), n)
switch g.ID {
case gid1:
if n != 11 {
t.Errorf("wrong return value for goroutine %d", g.ID)
}
found++
case gid2:
if n != 12 {
t.Errorf("wrong return value for goroutine %d", g.ID)
}
found++
} }
} }
return false return found
} }
func TestCallConcurrent(t *testing.T) { func TestCallConcurrent(t *testing.T) {
@ -4344,26 +4359,34 @@ func TestCallConcurrent(t *testing.T) {
withTestProcess("teststepconcurrent", t, func(p proc.Process, fixture protest.Fixture) { withTestProcess("teststepconcurrent", t, func(p proc.Process, fixture protest.Fixture) {
bp := setFileBreakpoint(p, t, fixture, 24) bp := setFileBreakpoint(p, t, fixture, 24)
assertNoError(proc.Continue(p), t, "Continue()") assertNoError(proc.Continue(p), t, "Continue()")
_, err := p.ClearBreakpoint(bp.Addr) //_, err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error") //assertNoError(err, t, "ClearBreakpoint() returned an error")
gid1 := p.SelectedGoroutine().ID gid1 := p.SelectedGoroutine().ID
t.Logf("starting injection in %d / %d", p.SelectedGoroutine().ID, p.CurrentThread().ThreadID()) t.Logf("starting injection in %d / %d", p.SelectedGoroutine().ID, p.CurrentThread().ThreadID())
assertNoError(proc.EvalExpressionWithCalls(p, "Foo(10, 1)", normalLoadConfig, false), t, "EvalExpressionWithCalls()") assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "Foo(10, 1)", normalLoadConfig, false), t, "EvalExpressionWithCalls()")
returned := testCallConcurrentCheckReturns(p, t, gid1) returned := testCallConcurrentCheckReturns(p, t, gid1, -1)
curthread := p.CurrentThread() curthread := p.CurrentThread()
if curbp := curthread.Breakpoint(); curbp.Breakpoint == nil || curbp.ID != bp.ID || returned { if curbp := curthread.Breakpoint(); curbp.Breakpoint == nil || curbp.ID != bp.ID || returned > 0 {
t.Logf("skipping test, the call injection terminated before we hit a breakpoint in a different thread")
return return
} }
_, err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error")
gid2 := p.SelectedGoroutine().ID
t.Logf("starting second injection in %d / %d", p.SelectedGoroutine().ID, p.CurrentThread().ThreadID())
assertNoError(proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), "Foo(10, 2)", normalLoadConfig, false), t, "EvalExpressioniWithCalls")
for { for {
returned = testCallConcurrentCheckReturns(p, t, gid1) returned += testCallConcurrentCheckReturns(p, t, gid1, gid2)
if returned { if returned >= 2 {
break break
} }
t.Logf("Continuing... %v", returned) t.Logf("Continuing... %d", returned)
assertNoError(proc.Continue(p), t, "Continue()") assertNoError(proc.Continue(p), t, "Continue()")
} }

@ -218,7 +218,7 @@ type EvalScope struct {
Location Location
Regs op.DwarfRegisters Regs op.DwarfRegisters
Mem MemoryReadWriter // Target's memory Mem MemoryReadWriter // Target's memory
Gvar *Variable g *G
BinInfo *BinaryInfo BinInfo *BinaryInfo
frameOffset int64 frameOffset int64
@ -250,7 +250,7 @@ func (err *IsNilErr) Error() string {
} }
func globalScope(bi *BinaryInfo, image *Image, mem MemoryReadWriter) *EvalScope { func globalScope(bi *BinaryInfo, image *Image, mem MemoryReadWriter) *EvalScope {
return &EvalScope{Location: Location{}, Regs: op.DwarfRegisters{StaticBase: image.StaticBase}, Mem: mem, Gvar: nil, BinInfo: bi, frameOffset: 0} return &EvalScope{Location: Location{}, Regs: op.DwarfRegisters{StaticBase: image.StaticBase}, Mem: mem, g: nil, BinInfo: bi, frameOffset: 0}
} }
func (scope *EvalScope) newVariable(name string, addr uintptr, dwarfType godwarf.Type, mem MemoryReadWriter) *Variable { func (scope *EvalScope) newVariable(name string, addr uintptr, dwarfType godwarf.Type, mem MemoryReadWriter) *Variable {

@ -1035,7 +1035,7 @@ func (c *Commands) call(t *Term, ctx callContext, args string) error {
unsafe = true unsafe = true
args = args[len(unsafePrefix):] args = args[len(unsafePrefix):]
} }
state, err := exitedToError(t.client.Call(args, unsafe)) state, err := exitedToError(t.client.Call(ctx.Scope.GoroutineID, args, unsafe))
c.frame = 0 c.frame = 0
if err != nil { if err != nil {
printcontextNoState(t) printcontextNoState(t)

@ -304,7 +304,7 @@ type DebuggerCommand struct {
// command. // command.
ThreadID int `json:"threadID,omitempty"` ThreadID int `json:"threadID,omitempty"`
// GoroutineID is used to specify which thread to use with the SwitchGoroutine // GoroutineID is used to specify which thread to use with the SwitchGoroutine
// command. // and Call commands.
GoroutineID int `json:"goroutineID,omitempty"` GoroutineID int `json:"goroutineID,omitempty"`
// When ReturnInfoLoadConfig is not nil it will be used to load the value // When ReturnInfoLoadConfig is not nil it will be used to load the value
// of any return variables. // of any return variables.

@ -39,7 +39,7 @@ type Client interface {
// StepOut continues to the return address of the current function // StepOut continues to the return address of the current function
StepOut() (*api.DebuggerState, error) StepOut() (*api.DebuggerState, error)
// Call resumes process execution while making a function call. // Call resumes process execution while making a function call.
Call(expr string, unsafe bool) (*api.DebuggerState, error) Call(goroutineID int, expr string, unsafe bool) (*api.DebuggerState, error)
// SingleStep will step a single cpu instruction. // SingleStep will step a single cpu instruction.
StepInstruction() (*api.DebuggerState, error) StepInstruction() (*api.DebuggerState, error)

@ -598,7 +598,14 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
if command.ReturnInfoLoadConfig == nil { if command.ReturnInfoLoadConfig == nil {
return nil, errors.New("can not call function with nil ReturnInfoLoadConfig") return nil, errors.New("can not call function with nil ReturnInfoLoadConfig")
} }
err = proc.EvalExpressionWithCalls(d.target, command.Expr, *api.LoadConfigToProc(command.ReturnInfoLoadConfig), !command.UnsafeCall) g := d.target.SelectedGoroutine()
if command.GoroutineID > 0 {
g, err = proc.FindGoroutine(d.target, command.GoroutineID)
if err != nil {
return nil, err
}
}
err = proc.EvalExpressionWithCalls(d.target, g, command.Expr, *api.LoadConfigToProc(command.ReturnInfoLoadConfig), !command.UnsafeCall)
case api.Rewind: case api.Rewind:
d.log.Debug("rewinding") d.log.Debug("rewinding")
if err := d.target.Direction(proc.Backward); err != nil { if err := d.target.Direction(proc.Backward); err != nil {

@ -148,9 +148,9 @@ func (c *RPCClient) StepOut() (*api.DebuggerState, error) {
return &out.State, err return &out.State, err
} }
func (c *RPCClient) Call(expr string, unsafe bool) (*api.DebuggerState, error) { func (c *RPCClient) Call(goroutineID int, expr string, unsafe bool) (*api.DebuggerState, error) {
var out CommandOut var out CommandOut
err := c.call("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe}, &out) err := c.call("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID}, &out)
return &out.State, err return &out.State, err
} }

@ -1555,7 +1555,7 @@ func TestClientServerFunctionCall(t *testing.T) {
state := <-c.Continue() state := <-c.Continue()
assertNoError(state.Err, t, "Continue()") assertNoError(state.Err, t, "Continue()")
beforeCallFn := state.CurrentThread.Function.Name() beforeCallFn := state.CurrentThread.Function.Name()
state, err := c.Call("call1(one, two)", false) state, err := c.Call(-1, "call1(one, two)", false)
assertNoError(err, t, "Call()") assertNoError(err, t, "Call()")
t.Logf("returned to %q", state.CurrentThread.Function.Name()) t.Logf("returned to %q", state.CurrentThread.Function.Name())
if state.CurrentThread.Function.Name() != beforeCallFn { if state.CurrentThread.Function.Name() != beforeCallFn {
@ -1598,7 +1598,7 @@ func TestClientServerFunctionCallBadPos(t *testing.T) {
assertNoError(state.Err, t, "Continue()") assertNoError(state.Err, t, "Continue()")
c.SetReturnValuesLoadConfig(&normalLoadConfig) c.SetReturnValuesLoadConfig(&normalLoadConfig)
state, err = c.Call("main.call1(main.zero, main.zero)", false) state, err = c.Call(-1, "main.call1(main.zero, main.zero)", false)
if err == nil || err.Error() != "call not at safe point" { if err == nil || err.Error() != "call not at safe point" {
t.Fatalf("wrong error or no error: %v", err) t.Fatalf("wrong error or no error: %v", err)
} }
@ -1612,7 +1612,7 @@ func TestClientServerFunctionCallPanic(t *testing.T) {
c.SetReturnValuesLoadConfig(&normalLoadConfig) c.SetReturnValuesLoadConfig(&normalLoadConfig)
state := <-c.Continue() state := <-c.Continue()
assertNoError(state.Err, t, "Continue()") assertNoError(state.Err, t, "Continue()")
state, err := c.Call("callpanic()", false) state, err := c.Call(-1, "callpanic()", false)
assertNoError(err, t, "Call()") assertNoError(err, t, "Call()")
t.Logf("at: %s:%d", state.CurrentThread.File, state.CurrentThread.Line) t.Logf("at: %s:%d", state.CurrentThread.File, state.CurrentThread.Line)
if state.CurrentThread.ReturnValues == nil { if state.CurrentThread.ReturnValues == nil {
@ -1638,7 +1638,7 @@ func TestClientServerFunctionCallStacktrace(t *testing.T) {
c.SetReturnValuesLoadConfig(&api.LoadConfig{false, 0, 2048, 0, 0}) c.SetReturnValuesLoadConfig(&api.LoadConfig{false, 0, 2048, 0, 0})
state := <-c.Continue() state := <-c.Continue()
assertNoError(state.Err, t, "Continue()") assertNoError(state.Err, t, "Continue()")
state, err := c.Call("callstacktrace()", false) state, err := c.Call(-1, "callstacktrace()", false)
assertNoError(err, t, "Call()") assertNoError(err, t, "Call()")
t.Logf("at: %s:%d", state.CurrentThread.File, state.CurrentThread.Line) t.Logf("at: %s:%d", state.CurrentThread.File, state.CurrentThread.Line)
if state.CurrentThread.ReturnValues == nil { if state.CurrentThread.ReturnValues == nil {

@ -1155,6 +1155,10 @@ func TestCallFunction(t *testing.T) {
// Escape tests // Escape tests
{"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2")}, {"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2")},
// Issue 1577
{"1+2", []string{`::3`}, nil},
{`"de"+"mo"`, []string{`::"demo"`}, nil},
} }
var testcases112 = []testCaseCallFunction{ var testcases112 = []testCaseCallFunction{
@ -1224,7 +1228,7 @@ func testCallFunction(t *testing.T, p proc.Process, tc testCaseCallFunction) {
checkEscape = false checkEscape = false
} }
t.Logf("call %q", tc.expr) t.Logf("call %q", tc.expr)
err := proc.EvalExpressionWithCalls(p, callExpr, pnormalLoadConfig, checkEscape) err := proc.EvalExpressionWithCalls(p, p.SelectedGoroutine(), callExpr, pnormalLoadConfig, checkEscape)
if tc.err != nil { if tc.err != nil {
t.Logf("\terr = %v\n", err) t.Logf("\terr = %v\n", err)
if err == nil { if err == nil {