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 {
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 {
uintExprCheck(t, scope, name, uint64(value))
}
@ -213,7 +213,7 @@ func TestDwarfExprLoclist(t *testing.T) {
mem := newFakeMemory(defaultCFA, uint16(before), uint16(after))
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)
scope.PC = 0x40800
@ -247,7 +247,7 @@ func TestIssue1419(t *testing.T) {
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)
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
if maybePkg, ok := node.X.(*ast.Ident); ok {
if maybePkg.Name == "runtime" && node.Sel.Name == "curg" {
if scope.Gvar == nil {
if scope.g == nil {
return nilVariable, nil
}
return scope.Gvar.clone(), nil
return scope.g.variable.clone(), nil
} else if maybePkg.Name == "runtime" && node.Sel.Name == "frameoff" {
return newConstant(constant.MakeInt64(scope.frameOffset), scope.Mem), nil
} else if v, err := scope.findGlobal(maybePkg.Name + "." + node.Sel.Name); err == nil {

@ -11,6 +11,7 @@ import (
"reflect"
"sort"
"strconv"
"strings"
"github.com/go-delve/delve/pkg/dwarf/godwarf"
"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
// continueRequest having cont == false and the return values in ret.
continueRequest chan<- continueRequest
continueCompleted <-chan struct{}
continueCompleted <-chan *G
}
type continueRequest struct {
@ -114,9 +115,9 @@ type continueRequest struct {
ret *Variable
}
func (callCtx *callContext) doContinue() {
func (callCtx *callContext) doContinue() *G {
callCtx.continueRequest <- continueRequest{cont: true}
<-callCtx.continueCompleted
return <-callCtx.continueCompleted
}
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'.
// Because this can only be done in the current goroutine, unlike
// 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()
if !p.Common().fncallEnabled {
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
}
@ -143,22 +153,13 @@ func EvalExpressionWithCalls(p Process, expr string, retLoadCfg LoadConfig, chec
return errFuncCallUnsupported
}
// check that the selected goroutine is running
g := p.SelectedGoroutine()
if g == nil {
return errNoGoroutine
}
if g.Status != Grunning || g.Thread == nil {
return errGoroutineNotRunning
}
scope, err := GoroutineScope(p.CurrentThread())
scope, err := GoroutineScope(g.Thread)
if err != nil {
return err
}
continueRequest := make(chan continueRequest)
continueCompleted := make(chan struct{})
continueCompleted := make(chan *G)
scope.callCtx = &callContext{
p: p,
@ -168,8 +169,10 @@ func EvalExpressionWithCalls(p Process, expr string, retLoadCfg LoadConfig, chec
continueCompleted: continueCompleted,
}
p.Common().continueRequest = continueRequest
p.Common().continueCompleted = continueCompleted
p.Common().fncallForG[g.ID] = &callInjection{
continueCompleted,
continueRequest,
}
go scope.EvalExpression(expr, retLoadCfg)
@ -178,35 +181,35 @@ func EvalExpressionWithCalls(p Process, expr string, retLoadCfg LoadConfig, chec
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
if !ok {
err = errors.New("internal error EvalExpressionWithCalls didn't return anything")
} else if contReq.err != nil {
if fpe, ispanic := contReq.err.(fncallPanicErr); ispanic {
p.CurrentThread().Common().returnValues = []*Variable{fpe.panicVar}
g.Thread.Common().returnValues = []*Variable{fpe.panicVar}
} else {
err = contReq.err
}
} else if contReq.ret == nil {
p.CurrentThread().Common().returnValues = nil
} else if contReq.ret.Addr == 0 && contReq.ret.DwarfType == nil {
g.Thread.Common().returnValues = 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
r := make([]*Variable, len(contReq.ret.Children))
for i := range contReq.ret.Children {
r[i] = &contReq.ret.Children[i]
}
p.CurrentThread().Common().returnValues = r
g.Thread.Common().returnValues = r
} else {
p.CurrentThread().Common().returnValues = []*Variable{contReq.ret}
g.Thread.Common().returnValues = []*Variable{contReq.ret}
}
p.Common().continueRequest = nil
close(p.Common().continueCompleted)
p.Common().continueCompleted = nil
close(p.Common().fncallForG[g.ID].continueCompleted)
delete(p.Common().fncallForG, g.ID)
return err
}
@ -240,22 +243,13 @@ func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error)
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
regs, err := g.Thread.Registers(true)
regs, err := scope.g.Thread.Registers(true)
if err != nil {
return nil, err
}
regs = regs.Copy()
if regs.SP()-256 <= g.stacklo {
if regs.SP()-256 <= scope.g.stacklo {
return nil, errNotEnoughStack
}
_, err = regs.Get(int(x86asm.RAX))
@ -273,42 +267,39 @@ func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error)
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
}
// 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
}
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)
bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(g.stackhi)
fboff := scope.Regs.FrameBase - int64(g.stackhi)
spoff := int64(scope.Regs.Uint64Val(scope.Regs.SPRegNum)) - int64(scope.g.stackhi)
bpoff := int64(scope.Regs.Uint64Val(scope.Regs.BPRegNum)) - int64(scope.g.stackhi)
fboff := scope.Regs.FrameBase - int64(scope.g.stackhi)
for {
scope.callCtx.doContinue()
scope.g = scope.callCtx.doContinue()
g = p.SelectedGoroutine()
if g != nil {
// adjust the value of registers inside scope
for regnum := range scope.Regs.Regs {
switch uint64(regnum) {
case scope.Regs.PCRegNum, scope.Regs.SPRegNum, scope.Regs.BPRegNum:
// leave these alone
default:
// every other register is dirty and unrecoverable
scope.Regs.Regs[regnum] = nil
}
// adjust the value of registers inside scope
for regnum := range scope.Regs.Regs {
switch uint64(regnum) {
case scope.Regs.PCRegNum, scope.Regs.SPRegNum, scope.Regs.BPRegNum:
// leave these alone
default:
// 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)
if finished {
break
@ -464,14 +455,13 @@ type funcCallArg struct {
// 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 {
g := scope.callCtx.p.SelectedGoroutine()
if g == nil {
if scope.g == nil {
// this should never happen
return errNoGoroutine
}
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 {
return err
}
@ -487,7 +477,7 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr
}
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 {
return err
}
@ -496,10 +486,10 @@ func funcCallEvalArgs(scope *EvalScope, fncall *functionCallState, argFrameAddr
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 {
//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)
}
}
@ -631,7 +621,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
p := callScope.callCtx.p
bi := p.BinInfo()
thread := p.CurrentThread()
thread := callScope.g.Thread
regs, err := thread.Registers(false)
if err != nil {
fncall.err = err
@ -651,7 +641,7 @@ func funcCallStep(callScope *EvalScope, fncall *functionCallState) bool {
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 {
@ -869,3 +859,49 @@ func (scope *EvalScope) allocString(v *Variable) error {
_, err = scope.Mem.WriteMemory(v.Base, []byte(constant.StringVal(v.Value)))
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
fncallEnabled bool
fncallForG map[int]*callInjection
}
type callInjection struct {
// if continueCompleted is not nil it means we are in the process of
// executing an injected function call, see comments throughout
// pkg/proc/fncall.go for a description of how this works.
continueCompleted chan<- struct{}
continueCompleted chan<- *G
continueRequest <-chan continueRequest
}
// NewCommonProcess returns a struct with fields common across
// all process implementations.
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.

@ -8,7 +8,6 @@ import (
"go/token"
"path/filepath"
"strconv"
"strings"
)
// ErrNotExecutable is returned after attempting to execute a non-executable file
@ -190,6 +189,11 @@ func Continue(dbp Process) error {
threads := dbp.ThreadList()
callInjectionDone, err := callInjectionProtocol(dbp, threads)
if err != nil {
return err
}
if err := pickCurrentThread(dbp, trapthread, threads); err != nil {
return err
}
@ -209,6 +213,7 @@ func Continue(dbp Process) error {
if err != nil || loc.Fn == nil {
return conditionErrors(threads)
}
g, _ := GetG(curthread)
switch {
case loc.Fn.Name == "runtime.breakpoint":
@ -220,22 +225,8 @@ func Continue(dbp Process) error {
return err
}
return conditionErrors(threads)
case strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) || strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2):
continueCompleted := dbp.Common().continueCompleted
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:
case g == nil || dbp.Common().fncallForG[g.ID] == nil:
// a hardcoded breakpoint somewhere else in the code (probably cgo)
return conditionErrors(threads)
}
case curbp.Active && curbp.Internal:
@ -285,6 +276,11 @@ func Continue(dbp Process) error {
default:
// 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()
if err != nil || loc.Fn == nil || (loc.Fn.Name != fnname1 && loc.Fn.Name != fnname2) {
if g := dbp.SelectedGoroutine(); g != nil {
g.CurrentLoc = *loc
g, _ := GetG(curthread)
selg := dbp.SelectedGoroutine()
if g != nil && selg != nil && g.ID == selg.ID {
selg.CurrentLoc = *loc
}
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
// will be cached.
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
// time any local variable is read.
// 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))
}
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
return s
}

@ -4144,7 +4144,7 @@ func TestIssue1374(t *testing.T) {
setFileBreakpoint(p, t, fixture, 7)
assertNoError(proc.Continue(p), t, "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)
if _, isexited := err.(proc.ErrProcessExited); !isexited {
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() {
g, _ := proc.GetG(thread)
if g == nil || g.ID != gid1 {
if g == nil || (g.ID != gid1 && g.ID != gid2) {
continue
}
retvals := thread.Common().ReturnValues(normalLoadConfig)
if len(retvals) != 0 {
return true
if len(retvals) == 0 {
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) {
@ -4344,26 +4359,34 @@ func TestCallConcurrent(t *testing.T) {
withTestProcess("teststepconcurrent", t, func(p proc.Process, fixture protest.Fixture) {
bp := setFileBreakpoint(p, t, fixture, 24)
assertNoError(proc.Continue(p), t, "Continue()")
_, err := p.ClearBreakpoint(bp.Addr)
assertNoError(err, t, "ClearBreakpoint() returned an error")
//_, err := p.ClearBreakpoint(bp.Addr)
//assertNoError(err, t, "ClearBreakpoint() returned an error")
gid1 := p.SelectedGoroutine().ID
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()
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
}
_, 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 {
returned = testCallConcurrentCheckReturns(p, t, gid1)
if returned {
returned += testCallConcurrentCheckReturns(p, t, gid1, gid2)
if returned >= 2 {
break
}
t.Logf("Continuing... %v", returned)
t.Logf("Continuing... %d", returned)
assertNoError(proc.Continue(p), t, "Continue()")
}

@ -218,7 +218,7 @@ type EvalScope struct {
Location
Regs op.DwarfRegisters
Mem MemoryReadWriter // Target's memory
Gvar *Variable
g *G
BinInfo *BinaryInfo
frameOffset int64
@ -250,7 +250,7 @@ func (err *IsNilErr) Error() string {
}
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 {

@ -1035,7 +1035,7 @@ func (c *Commands) call(t *Term, ctx callContext, args string) error {
unsafe = true
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
if err != nil {
printcontextNoState(t)

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

@ -39,7 +39,7 @@ type Client interface {
// StepOut continues to the return address of the current function
StepOut() (*api.DebuggerState, error)
// 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.
StepInstruction() (*api.DebuggerState, error)

@ -598,7 +598,14 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
if command.ReturnInfoLoadConfig == nil {
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:
d.log.Debug("rewinding")
if err := d.target.Direction(proc.Backward); err != nil {

@ -148,9 +148,9 @@ func (c *RPCClient) StepOut() (*api.DebuggerState, error) {
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
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
}

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

@ -1155,6 +1155,10 @@ func TestCallFunction(t *testing.T) {
// Escape tests
{"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{
@ -1224,7 +1228,7 @@ func testCallFunction(t *testing.T, p proc.Process, tc testCaseCallFunction) {
checkEscape = false
}
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 {
t.Logf("\terr = %v\n", err)
if err == nil {