diff --git a/_fixtures/fncall.go b/_fixtures/fncall.go
index c837f919..4c39d276 100644
--- a/_fixtures/fncall.go
+++ b/_fixtures/fncall.go
@@ -76,6 +76,21 @@ func escapeArg(pa2 *a2struct) {
globalPA2 = pa2
}
+func square(x int) int {
+ return x * x
+}
+
+func intcallpanic(a int) int {
+ if a == 0 {
+ panic("panic requested")
+ }
+ return a
+}
+
+func onetwothree(n int) []int {
+ return []int{n + 1, n + 2, n + 3}
+}
+
func main() {
one, two := 1, 2
intslice := []int{1, 2, 3}
@@ -98,5 +113,5 @@ func main() {
runtime.Breakpoint()
call1(one, two)
fn2clos(2)
- fmt.Println(one, two, zero, callpanic, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2)
+ fmt.Println(one, two, zero, callpanic, callstacktrace, stringsJoin, intslice, stringslice, comma, a.VRcvr, a.PRcvr, pa, vable_a, vable_pa, pable_pa, fn2clos, fn2glob, fn2valmeth, fn2ptrmeth, fn2nil, ga, escapeArg, a2, square, intcallpanic, onetwothree)
}
diff --git a/pkg/proc/eval.go b/pkg/proc/eval.go
index 1020c6c6..9d815178 100644
--- a/pkg/proc/eval.go
+++ b/pkg/proc/eval.go
@@ -23,8 +23,13 @@ var errOperationOnSpecialFloat = errors.New("operations on non-finite floats not
// EvalExpression returns the value of the given expression.
func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable, error) {
+ if scope.callCtx != nil {
+ // makes sure that the other goroutine won't wait forever if we make a mistake
+ defer close(scope.callCtx.continueRequest)
+ }
t, err := parser.ParseExpr(expr)
if err != nil {
+ scope.callCtx.doReturn(nil, err)
return nil, err
}
@@ -33,12 +38,14 @@ func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable,
ev, err = scope.evalAST(t)
}
if err != nil {
+ scope.callCtx.doReturn(nil, err)
return nil, err
}
ev.loadValue(cfg)
if ev.Name == "" {
ev.Name = expr
}
+ scope.callCtx.doReturn(ev, nil)
return ev, nil
}
@@ -174,20 +181,11 @@ func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
case *ast.CallExpr:
if len(node.Args) == 1 {
v, err := scope.evalTypeCast(node)
- if err == nil {
- return v, nil
- }
- _, isident := node.Fun.(*ast.Ident)
- // we don't support function calls at the moment except for a few
- // builtin functions so just return the type error here if the function
- // isn't an identifier.
- // More sophisticated logic will be required when function calls
- // are implemented.
- if err != reader.TypeNotFoundErr || !isident {
+ if err == nil || err != reader.TypeNotFoundErr {
return v, err
}
}
- return scope.evalBuiltinCall(node)
+ return scope.evalFunctionCall(node)
case *ast.Ident:
return scope.evalIdent(node)
@@ -395,7 +393,7 @@ func convertInt(n uint64, signed bool, size int64) uint64 {
func (scope *EvalScope) evalBuiltinCall(node *ast.CallExpr) (*Variable, error) {
fnnode, ok := node.Fun.(*ast.Ident)
if !ok {
- return nil, fmt.Errorf("function calls are not supported")
+ return nil, nil
}
args := make([]*Variable, len(node.Args))
@@ -421,7 +419,7 @@ func (scope *EvalScope) evalBuiltinCall(node *ast.CallExpr) (*Variable, error) {
return realBuiltin(args, node.Args)
}
- return nil, fmt.Errorf("function calls are not supported")
+ return nil, nil
}
func capBuiltin(args []*Variable, nodeargs []ast.Expr) (*Variable, error) {
diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go
index ef0d3404..16d70de2 100644
--- a/pkg/proc/fncall.go
+++ b/pkg/proc/fncall.go
@@ -7,7 +7,6 @@ import (
"fmt"
"go/ast"
"go/constant"
- "go/parser"
"reflect"
"sort"
@@ -23,13 +22,18 @@ import (
// The protocol is described in $GOROOT/src/runtime/asm_amd64.s in the
// comments for function runtime·debugCallV1.
//
-// There are two main entry points here. The first one is CallFunction which
-// evaluates a function call expression, sets up the function call on the
-// selected goroutine and resumes execution of the process.
+// The main entry point is EvalExpressionWithCalls which will start a goroutine to
+// evaluate the provided expression.
+// This goroutine can either return immediately, if no function calls were
+// needed, or write a continue request to the scope.callCtx.continueRequest
+// channel. When this happens EvalExpressionWithCalls will call Continue and
+// return.
//
-// The second one is (*FunctionCallState).step() which is called every time
-// the process stops at a breakpoint inside one of the debug injcetion
-// functions.
+// The Continue loop will write to scope.callCtx.continueCompleted when it
+// hits a breakpoint in the call injection protocol.
+//
+// The work of setting up the function call and executing the protocol is
+// done by evalFunctionCall and funcCallStep.
const (
debugCallFunctionNamePrefix1 = "debugCall"
@@ -49,17 +53,12 @@ var (
errNotEnoughArguments = errors.New("not enough arguments")
errNoAddrUnsupported = errors.New("arguments to a function call must have an address")
errNotAGoFunction = errors.New("not a Go function")
+ errFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
)
type functionCallState struct {
- // inProgress is true if a function call is in progress
- inProgress bool
- // finished is true if the function call terminated
- finished bool
// savedRegs contains the saved registers
savedRegs Registers
- // expr contains an expression describing the current function call
- expr string
// err contains a saved error
err error
// fn is the function that is being called
@@ -77,21 +76,56 @@ type functionCallState struct {
panicvar *Variable
}
-// CallFunction starts a debugger injected function call on the current thread of p.
-// See runtime.debugCallV1 in $GOROOT/src/runtime/asm_amd64.s for a
-// description of the protocol.
-func CallFunction(p Process, expr string, retLoadCfg *LoadConfig, checkEscape bool) error {
+type callContext struct {
+ p Process
+
+ // checkEscape is true if the escape check should be performed.
+ // See service/api.DebuggerCommand.UnsafeCall in service/api/types.go.
+ checkEscape bool
+
+ // retLoadCfg is the load configuration used to load return values
+ retLoadCfg LoadConfig
+
+ // Write to continueRequest to request a call to Continue from the
+ // debugger's main goroutine.
+ // Read from continueCompleted to wait for the target process to stop at
+ // one of the interaction point of the function call protocol.
+ // 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{}
+}
+
+type continueRequest struct {
+ cont bool
+ err error
+ ret *Variable
+}
+
+func (callCtx *callContext) doContinue() {
+ callCtx.continueRequest <- continueRequest{cont: true}
+ <-callCtx.continueCompleted
+}
+
+func (callCtx *callContext) doReturn(ret *Variable, err error) {
+ if callCtx == nil {
+ return
+ }
+ callCtx.continueRequest <- continueRequest{cont: false, ret: ret, err: err}
+}
+
+// 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 {
bi := p.BinInfo()
if !p.Common().fncallEnabled {
return errFuncCallUnsupportedBackend
}
- fncall := &p.Common().fncallState
- if fncall.inProgress {
+ if p.Common().continueCompleted != nil {
return errFuncCallInProgress
}
- *fncall = functionCallState{}
-
dbgcallfn := bi.LookupFunc[debugCallFunctionName]
if dbgcallfn == nil {
return errFuncCallUnsupported
@@ -106,49 +140,217 @@ func CallFunction(p Process, expr string, retLoadCfg *LoadConfig, checkEscape bo
return errGoroutineNotRunning
}
+ scope, err := GoroutineScope(p.CurrentThread())
+ if err != nil {
+ return err
+ }
+
+ continueRequest := make(chan continueRequest)
+ continueCompleted := make(chan struct{})
+
+ scope.callCtx = &callContext{
+ p: p,
+ checkEscape: checkEscape,
+ retLoadCfg: retLoadCfg,
+ continueRequest: continueRequest,
+ continueCompleted: continueCompleted,
+ }
+
+ p.Common().continueRequest = continueRequest
+ p.Common().continueCompleted = continueCompleted
+
+ go scope.EvalExpression(expr, retLoadCfg)
+
+ contReq, ok := <-continueRequest
+ if contReq.cont {
+ return Continue(p)
+ }
+
+ return finishEvalExpressionWithCalls(p, contReq, ok)
+}
+
+func finishEvalExpressionWithCalls(p Process, contReq continueRequest, ok bool) error {
+ 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}
+ } else {
+ err = contReq.err
+ }
+ } else if contReq.ret.Addr == 0 && contReq.ret.DwarfType == nil {
+ // 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
+ } else {
+ p.CurrentThread().Common().returnValues = []*Variable{contReq.ret}
+ }
+
+ p.Common().continueRequest = nil
+ close(p.Common().continueCompleted)
+ p.Common().continueCompleted = nil
+ return err
+}
+
+// evalFunctionCall evaluates a function call.
+// If this is a built-in function it's evaluated directly.
+// Otherwise this will start the function call injection protocol and
+// request that the target process resumes.
+// See the comment describing the field EvalScope.callCtx for a description
+// of the preconditions that make starting the function call protocol
+// possible.
+// See runtime.debugCallV1 in $GOROOT/src/runtime/asm_amd64.s for a
+// description of the protocol.
+func (scope *EvalScope) evalFunctionCall(node *ast.CallExpr) (*Variable, error) {
+ r, err := scope.evalBuiltinCall(node)
+ if r != nil || err != nil {
+ // it was a builtin call
+ return r, err
+ }
+ if scope.callCtx == nil {
+ return nil, errFuncCallNotAllowed
+ }
+
+ p := scope.callCtx.p
+ bi := scope.BinInfo
+ if !p.Common().fncallEnabled {
+ return nil, errFuncCallUnsupportedBackend
+ }
+ if p.Common().callInProgress {
+ return nil, errFuncCallInProgress
+ }
+
+ p.Common().callInProgress = true
+ defer func() {
+ p.Common().callInProgress = false
+ }()
+
+ dbgcallfn := bi.LookupFunc[debugCallFunctionName]
+ if dbgcallfn == nil {
+ 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)
if err != nil {
- return err
+ return nil, err
}
regs = regs.Copy()
if regs.SP()-256 <= g.stacklo {
- return errNotEnoughStack
+ return nil, errNotEnoughStack
}
_, err = regs.Get(int(x86asm.RAX))
if err != nil {
- return errFuncCallUnsupportedBackend
+ return nil, errFuncCallUnsupportedBackend
}
- fn, closureAddr, argvars, err := funcCallEvalExpr(p, expr)
+ fn, closureAddr, argvars, err := scope.funcCallEvalExpr(node)
if err != nil {
- return err
+ return nil, err
}
- argmem, err := funcCallArgFrame(fn, argvars, g, bi, checkEscape)
+ argmem, err := funcCallArgFrame(fn, argvars, g, bi, scope.callCtx.checkEscape)
if err != nil {
- return err
+ return nil, err
}
if err := callOP(bi, g.Thread, regs, dbgcallfn.Entry); err != nil {
- return err
+ 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(len(argmem))); err != nil {
- return err
+ return nil, err
}
- fncall.inProgress = true
- fncall.savedRegs = regs
- fncall.expr = expr
- fncall.fn = fn
- fncall.closureAddr = closureAddr
- fncall.argmem = argmem
- fncall.retLoadCfg = retLoadCfg
+ fncall := functionCallState{
+ savedRegs: regs,
+ fn: fn,
+ closureAddr: closureAddr,
+ argmem: argmem,
+ retLoadCfg: &scope.callCtx.retLoadCfg,
+ }
fncallLog("function call initiated %v frame size %d\n", fn, len(argmem))
- return Continue(p)
+ 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)
+
+ for {
+ 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
+ }
+ }
+
+ 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)
+ }
+
+ finished := funcCallStep(scope, &fncall)
+ if finished {
+ break
+ }
+ }
+
+ if fncall.err != nil {
+ return nil, fncall.err
+ }
+
+ if fncall.panicvar != nil {
+ return nil, fncallPanicErr{fncall.panicvar}
+ }
+ switch len(fncall.retvars) {
+ case 0:
+ r := scope.newVariable("", 0, nil, nil)
+ r.loaded = true
+ r.Unreadable = errors.New("no return values")
+ return r, nil
+ case 1:
+ return fncall.retvars[0], nil
+ default:
+ // create a fake variable without address or type to return multiple values
+ r := scope.newVariable("", 0, nil, nil)
+ r.loaded = true
+ r.Children = make([]Variable, len(fncall.retvars))
+ for i := range fncall.retvars {
+ r.Children[i] = *fncall.retvars[i]
+ }
+ return r, nil
+ }
+}
+
+// fncallPanicErr is the error returned if a called function panics
+type fncallPanicErr struct {
+ panicVar *Variable
+}
+
+func (err fncallPanicErr) Error() string {
+ return fmt.Sprintf("panic calling a function")
}
func fncallLog(fmtstr string, args ...interface{}) {
@@ -191,21 +393,8 @@ func callOP(bi *BinaryInfo, thread Thread, regs Registers, callAddr uint64) erro
// funcCallEvalExpr evaluates expr, which must be a function call, returns
// the function being called and its arguments.
-func funcCallEvalExpr(p Process, expr string) (fn *Function, closureAddr uint64, argvars []*Variable, err error) {
- bi := p.BinInfo()
- scope, err := GoroutineScope(p.CurrentThread())
- if err != nil {
- return nil, 0, nil, err
- }
-
- t, err := parser.ParseExpr(expr)
- if err != nil {
- return nil, 0, nil, err
- }
- callexpr, iscall := t.(*ast.CallExpr)
- if !iscall {
- return nil, 0, nil, errNotACallExpr
- }
+func (scope *EvalScope) funcCallEvalExpr(callexpr *ast.CallExpr) (fn *Function, closureAddr uint64, argvars []*Variable, err error) {
+ bi := scope.BinInfo
fnvar, err := scope.evalAST(callexpr.Fun)
if err != nil {
@@ -395,16 +584,16 @@ const (
debugCallAXRestoreRegisters = 16
)
-func (fncall *functionCallState) step(p Process) {
+// funcCallStep executes one step of the function call injection protocol.
+func funcCallStep(scope *EvalScope, fncall *functionCallState) bool {
+ p := scope.callCtx.p
bi := p.BinInfo()
thread := p.CurrentThread()
regs, err := thread.Registers(false)
if err != nil {
fncall.err = err
- fncall.finished = true
- fncall.inProgress = false
- return
+ return true
}
regs = regs.Copy()
@@ -453,7 +642,6 @@ func (fncall *functionCallState) step(p Process) {
case debugCallAXRestoreRegisters:
// runtime requests that we restore the registers (all except pc and sp),
// this is also the last step of the function call protocol.
- fncall.finished = true
pc, sp := regs.PC(), regs.SP()
if err := thread.RestoreRegisters(fncall.savedRegs); err != nil {
fncall.err = fmt.Errorf("could not restore registers: %v", err)
@@ -467,6 +655,7 @@ func (fncall *functionCallState) step(p Process) {
if err := stepInstructionOut(p, thread, debugCallFunctionName, debugCallFunctionName); err != nil {
fncall.err = fmt.Errorf("could not step out of %s: %v", debugCallFunctionName, err)
}
+ return true
case debugCallAXReadReturn:
// read return arguments from stack
@@ -496,7 +685,7 @@ func (fncall *functionCallState) step(p Process) {
case debugCallAXReadPanic:
// read panic value from stack
if fncall.retLoadCfg == nil {
- return
+ return false
}
fncall.panicvar, err = readTopstackVariable(thread, regs, "interface {}", *fncall.retLoadCfg)
if err != nil {
@@ -515,6 +704,8 @@ func (fncall *functionCallState) step(p Process) {
// possible is to ignore it and hope it didn't matter.
fncallLog("unknown value of AX %#x", rax)
}
+
+ return false
}
func readTopstackVariable(thread Thread, regs Registers, typename string, loadCfg LoadConfig) (*Variable, error) {
diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go
index 9a4245a5..6362837b 100644
--- a/pkg/proc/interface.go
+++ b/pkg/proc/interface.go
@@ -115,8 +115,19 @@ type BreakpointManipulation interface {
// implementations of the Process interface.
type CommonProcess struct {
allGCache []*G
- fncallState functionCallState
fncallEnabled bool
+
+ // 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{}
+ continueRequest <-chan continueRequest
+
+ // callInProgress is true when a function call is being injected in the
+ // target process.
+ // This is only used to prevent nested function calls, it should be removed
+ // when we add support for them.
+ callInProgress bool
}
// NewCommonProcess returns a struct with fields common across
diff --git a/pkg/proc/proc.go b/pkg/proc/proc.go
index 85ef473f..1685b7f6 100644
--- a/pkg/proc/proc.go
+++ b/pkg/proc/proc.go
@@ -228,18 +228,18 @@ func Continue(dbp Process) error {
}
return conditionErrors(threads)
case strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) || strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2):
- fncall := &dbp.Common().fncallState
- if !fncall.inProgress {
+ continueCompleted := dbp.Common().continueCompleted
+ if continueCompleted == nil {
return conditionErrors(threads)
}
- fncall.step(dbp)
- // only stop execution if the function call finished
- if fncall.finished {
- fncall.inProgress = false
- if fncall.err != nil {
- return fncall.err
+ 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
}
- curthread.Common().returnValues = fncall.returnValues()
return conditionErrors(threads)
}
default:
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 5ef5a06c..2a73f8fe 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -4123,7 +4123,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.CallFunction(p, "getNum()", &normalLoadConfig, true), t, "Call")
+ assertNoError(proc.EvalExpressionWithCalls(p, "getNum()", normalLoadConfig, true), t, "Call")
err := proc.Continue(p)
if _, isexited := err.(proc.ErrProcessExited); !isexited {
regs, _ := p.CurrentThread().Registers(false)
@@ -4328,7 +4328,7 @@ func TestCallConcurrent(t *testing.T) {
gid1 := p.SelectedGoroutine().ID
t.Logf("starting injection in %d / %d", p.SelectedGoroutine().ID, p.CurrentThread().ThreadID())
- assertNoError(proc.CallFunction(p, "Foo(10, 1)", &normalLoadConfig, false), t, "EvalExpressionWithCalls()")
+ assertNoError(proc.EvalExpressionWithCalls(p, "Foo(10, 1)", normalLoadConfig, false), t, "EvalExpressionWithCalls()")
returned := testCallConcurrentCheckReturns(p, t, gid1)
diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go
index 3e83fa7f..ba143318 100644
--- a/pkg/proc/variables.go
+++ b/pkg/proc/variables.go
@@ -221,6 +221,20 @@ type EvalScope struct {
frameOffset int64
aordr *dwarf.Reader // extra reader to load DW_AT_abstract_origin entries, do not initialize
+
+ // When the following pointer is not nil this EvalScope was created
+ // by CallFunction and the expression evaluation is executing on a
+ // different goroutine from the debugger's main goroutine.
+ // Under this circumstance the expression evaluator can make function
+ // calls by setting up the runtime.debugCallV1 call and then writing a
+ // value to the continueRequest channel.
+ // When a value is written to continueRequest the debugger's main goroutine
+ // will call Continue, when the runtime in the target process sends us a
+ // request in the function call protocol the debugger's main goroutine will
+ // write a value to the continueCompleted channel.
+ // The goroutine executing the expression evaluation shall signal that the
+ // evaluation is complete by closing the continueRequest channel.
+ callCtx *callContext
}
// IsNilErr is returned when a variable is nil.
diff --git a/service/api/types.go b/service/api/types.go
index 0ac4f3d6..2bd6cf6f 100644
--- a/service/api/types.go
+++ b/service/api/types.go
@@ -311,7 +311,20 @@ type DebuggerCommand struct {
ReturnInfoLoadConfig *LoadConfig
// Expr is the expression argument for a Call command
Expr string `json:"expr,omitempty"`
- // UnsafeCall disabled parameter escape checking for function calls
+
+ // UnsafeCall disables parameter escape checking for function calls.
+ // Go objects can be allocated on the stack or on the heap. Heap objects
+ // can be used by any goroutine; stack objects can only be used by the
+ // goroutine that owns the stack they are allocated on and can not surivive
+ // the stack frame of allocation.
+ // The Go compiler will use escape analysis to determine whether to
+ // allocate an object on the stack or the heap.
+ // When injecting a function call Delve will check that no address of a
+ // stack allocated object is passed to the called function: this ensures
+ // the rules for stack objects will not be violated.
+ // If you are absolutely sure that the function you are calling will not
+ // violate the rules about stack objects you can disable this safety check
+ // by setting UnsafeCall to true.
UnsafeCall bool `json:"unsafeCall,omitempty"`
}
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 74f05782..33020237 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -599,7 +599,10 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
err = proc.Continue(d.target)
case api.Call:
d.log.Debugf("function call %s", command.Expr)
- err = proc.CallFunction(d.target, command.Expr, api.LoadConfigToProc(command.ReturnInfoLoadConfig), !command.UnsafeCall)
+ 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)
case api.Rewind:
d.log.Debug("rewinding")
if err := d.target.Direction(proc.Backward); err != nil {
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index f00ce682..252bb399 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -1584,6 +1584,7 @@ func TestClientServerFunctionCallBadPos(t *testing.T) {
state = <-c.Continue()
assertNoError(state.Err, t, "Continue()")
+ c.SetReturnValuesLoadConfig(&normalLoadConfig)
state, err = c.Call("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)
diff --git a/service/test/variables_test.go b/service/test/variables_test.go
index 421773ea..6c7c67d6 100644
--- a/service/test/variables_test.go
+++ b/service/test/variables_test.go
@@ -737,7 +737,7 @@ func TestEvalExpression(t *testing.T) {
{"i2 << i3", false, "", "", "int", fmt.Errorf("shift count type int, must be unsigned integer")},
{"*(i2 + i3)", false, "", "", "", fmt.Errorf("expression \"(i2 + i3)\" (int) can not be dereferenced")},
{"i2.member", false, "", "", "", fmt.Errorf("i2 (type int) is not a struct")},
- {"fmt.Println(\"hello\")", false, "", "", "", fmt.Errorf("no type entry found, use 'types' for a list of valid types")},
+ {"fmt.Println(\"hello\")", false, "", "", "", fmt.Errorf("function calls not allowed without using 'call'")},
{"*nil", false, "", "", "", fmt.Errorf("nil can not be dereferenced")},
{"!nil", false, "", "", "", fmt.Errorf("operator ! can not be applied to \"nil\"")},
{"&nil", false, "", "", "", fmt.Errorf("can not take address of \"nil\"")},
@@ -1093,6 +1093,8 @@ func TestCallFunction(t *testing.T) {
outs []string // list of return parameters in this format: ::
err error // if not nil should return an error
}{
+ // Basic function call injection tests
+
{"call1(one, two)", []string{":int:3"}, nil},
{"call1(one+two, 4)", []string{":int:7"}, nil},
{"callpanic()", []string{`~panic:interface {}:interface {}(string) "callpanic panicked"`}, nil},
@@ -1102,6 +1104,13 @@ func TestCallFunction(t *testing.T) {
{`stringsJoin(s1, comma)`, nil, errors.New("could not find symbol value for s1")},
{`stringsJoin(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string")},
+ // Expression tests
+ {`square(2) + 1`, []string{":int:5"}, nil},
+ {`intcallpanic(1) + 1`, []string{":int:2"}, nil},
+ {`intcallpanic(0) + 1`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil},
+ {`onetwothree(5)[1] + 2`, []string{":int:9"}, nil},
+
+ // Call types tests (methods, function pointers, etc.)
// The following set of calls was constructed using https://docs.google.com/document/d/1bMwCey-gmqZVTpRax-ESeVuZGmjwbocYs1iHplK-cjo/pub as a reference
{`a.VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil}, // direct call of a method with value receiver / on a value
@@ -1130,6 +1139,8 @@ func TestCallFunction(t *testing.T) {
{"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil},
+ // Escape tests
+
{"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2")},
{"-unsafe escapeArg(&a2)", nil, nil}, // LEAVE THIS AS THE LAST ITEM, IT BREAKS THE TARGET PROCESS!!!
@@ -1151,9 +1162,9 @@ func TestCallFunction(t *testing.T) {
checkEscape = false
}
t.Logf("call %q", tc.expr)
- err := proc.CallFunction(p, expr, &pnormalLoadConfig, checkEscape)
+ err := proc.EvalExpressionWithCalls(p, expr, pnormalLoadConfig, checkEscape)
if tc.err != nil {
-
+ t.Logf("\terr = %v\n", err)
if err == nil {
t.Fatalf("call %q: expected error %q, got no error", tc.expr, tc.err.Error())
}