
This is probably the cause of the panic such as in issue #3848 and other similar issues, it is hard to be sure because we never get a proper reproducer but judging from the stack traces it should be this. Also it doesn't affect versions of Go that have the debug pinner. Fixes #3848
1310 lines
40 KiB
Go
1310 lines
40 KiB
Go
package proc
|
|
|
|
import (
|
|
"debug/dwarf"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"reflect"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-delve/delve/pkg/dwarf/godwarf"
|
|
"github.com/go-delve/delve/pkg/dwarf/op"
|
|
"github.com/go-delve/delve/pkg/dwarf/reader"
|
|
"github.com/go-delve/delve/pkg/dwarf/regnum"
|
|
"github.com/go-delve/delve/pkg/goversion"
|
|
"github.com/go-delve/delve/pkg/logflags"
|
|
"github.com/go-delve/delve/pkg/proc/evalop"
|
|
)
|
|
|
|
// This file implements the function call injection introduced in go1.11.
|
|
//
|
|
// The protocol is described in $GOROOT/src/runtime/asm_amd64.s in the
|
|
// comments for function runtime·debugCallVn.
|
|
//
|
|
// The main entry point is EvalExpressionWithCalls which will set up an
|
|
// evalStack object to evaluate the provided expression.
|
|
// This object can either finish immediately, if no function calls were
|
|
// needed, or return with callInjectionContinue set. When this happens
|
|
// EvalExpressionWithCalls will call Continue and return.
|
|
//
|
|
// The Continue loop will call evalStack.resume 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:
|
|
//
|
|
// - evalop.CallInjectionStart
|
|
// - evalop.CallInjectionSetTarget
|
|
// - evalCallInjectionCopyArg
|
|
// - evalCallInjectionComplete
|
|
//
|
|
// When the target has runtime.debugPinner then evalCallInjectionPinPointer
|
|
// must be also called in a loop until it returns false.
|
|
|
|
const (
|
|
debugCallFunctionNamePrefix1 = "debugCall"
|
|
debugCallFunctionNamePrefix2 = "runtime.debugCall"
|
|
maxDebugCallVersion = 2
|
|
maxArgFrameSize = 65535
|
|
)
|
|
|
|
var (
|
|
errFuncCallUnsupported = errors.New("function calls not supported by this version of Go")
|
|
errFuncCallUnsupportedBackend = errors.New("backend does not support function calls")
|
|
errFuncCallInProgress = errors.New("cannot call function while another function call is already in progress")
|
|
errNoGoroutine = errors.New("no goroutine selected")
|
|
errGoroutineNotRunning = errors.New("selected goroutine not running")
|
|
errNotEnoughStack = errors.New("not enough stack space")
|
|
errTooManyArguments = errors.New("too many arguments")
|
|
errNotEnoughArguments = errors.New("not enough arguments")
|
|
errNotAGoFunction = errors.New("not a Go function")
|
|
errFuncCallNotAllowedStrAlloc = errors.New("literal string can not be allocated because function calls are not allowed without using 'call'")
|
|
)
|
|
|
|
type functionCallState struct {
|
|
// savedRegs contains the saved registers
|
|
savedRegs Registers
|
|
// err contains a saved error
|
|
err error
|
|
// expr is the expression being evaluated
|
|
expr *ast.CallExpr
|
|
// fn is the function that is being called
|
|
fn *Function
|
|
// receiver is the receiver argument for the function
|
|
receiver *Variable
|
|
// closureAddr is the address of the closure being called
|
|
closureAddr uint64
|
|
// formalArgs are the formal arguments of fn
|
|
formalArgs []funcCallArg
|
|
// argFrameSize contains the size of the arguments
|
|
argFrameSize int64
|
|
// retvars contains the return variables after the function call terminates without panic'ing
|
|
retvars []*Variable
|
|
// panicvar is a variable used to store the value of the panic, if the
|
|
// called function panics.
|
|
panicvar *Variable
|
|
// undoInjection is set after evalop.CallInjectionSetTarget runs and cleared by evalCallInjectionComplete
|
|
// it contains information on how to undo a function call injection without running it
|
|
undoInjection *undoInjection
|
|
|
|
// hasDebugPinner is true if the target has runtime.debugPinner
|
|
hasDebugPinner bool
|
|
// doPinning is true if this call injection should pin the results
|
|
doPinning bool
|
|
// addrsToPin addresses from return variables that should be pinned
|
|
addrsToPin []uint64
|
|
|
|
protocolReg uint64
|
|
debugCallName string
|
|
}
|
|
|
|
type undoInjection struct {
|
|
oldpc, oldlr uint64
|
|
doComplete2 bool
|
|
}
|
|
|
|
type callContext struct {
|
|
grp *TargetGroup
|
|
p *Target
|
|
|
|
// 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
|
|
|
|
// 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 callInjection struct {
|
|
evalStack *evalStack
|
|
startThreadID int
|
|
endCallInjection func()
|
|
}
|
|
|
|
//lint:ignore U1000 this variable is only used by tests
|
|
var debugPinCount int
|
|
|
|
// 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(grp *TargetGroup, g *G, expr string, retLoadCfg LoadConfig, checkEscape bool) error {
|
|
debugPinCount = 0
|
|
t := grp.Selected
|
|
bi := t.BinInfo()
|
|
if !t.SupportsFunctionCalls() {
|
|
return errFuncCallUnsupportedBackend
|
|
}
|
|
producer := bi.Producer()
|
|
if producer == "" || !goversion.ProducerAfterOrEqual(bi.Producer(), 1, 12) {
|
|
return errFuncCallUnsupported
|
|
}
|
|
|
|
// check that the target goroutine is running
|
|
if g == nil {
|
|
return errNoGoroutine
|
|
}
|
|
if g.Status != Grunning || g.Thread == nil {
|
|
return errGoroutineNotRunning
|
|
}
|
|
|
|
if callinj := t.fncallForG[g.ID]; callinj != nil && callinj.evalStack != nil {
|
|
return errFuncCallInProgress
|
|
}
|
|
|
|
dbgcallfn, _ := debugCallFunction(bi)
|
|
if dbgcallfn == nil {
|
|
return errFuncCallUnsupported
|
|
}
|
|
|
|
scope, err := GoroutineScope(t, g.Thread)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
scope.callCtx = &callContext{
|
|
grp: grp,
|
|
p: t,
|
|
checkEscape: checkEscape,
|
|
retLoadCfg: retLoadCfg,
|
|
}
|
|
scope.loadCfg = &retLoadCfg
|
|
|
|
endCallInjection, err := t.proc.StartCallInjection()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, scope.evalopFlags()|evalop.CanSet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stack := &evalStack{}
|
|
|
|
t.fncallForG[g.ID] = &callInjection{
|
|
evalStack: stack,
|
|
startThreadID: 0,
|
|
endCallInjection: endCallInjection,
|
|
}
|
|
|
|
stack.eval(scope, ops)
|
|
if stack.callInjectionContinue && stack.err == nil {
|
|
return grp.Continue()
|
|
}
|
|
|
|
return finishEvalExpressionWithCalls(t, g, stack)
|
|
}
|
|
|
|
func finishEvalExpressionWithCalls(t *Target, g *G, stack *evalStack) error {
|
|
fncallLog("stashing return values for %d in thread=%d", g.ID, g.Thread.ThreadID())
|
|
g.Thread.Common().CallReturn = true
|
|
ret, err := stack.result(&stack.scope.callCtx.retLoadCfg)
|
|
if err != nil {
|
|
if fpe, ispanic := stack.err.(fncallPanicErr); ispanic {
|
|
err = nil
|
|
g.Thread.Common().returnValues = []*Variable{fpe.panicVar}
|
|
}
|
|
} else if ret == nil {
|
|
g.Thread.Common().returnValues = nil
|
|
} else if ret.Addr == 0 && ret.DwarfType == nil && ret.Kind == reflect.Invalid {
|
|
// this is a variable returned by a function call with multiple return values
|
|
r := make([]*Variable, len(ret.Children))
|
|
for i := range ret.Children {
|
|
r[i] = &ret.Children[i]
|
|
}
|
|
g.Thread.Common().returnValues = r
|
|
} else {
|
|
g.Thread.Common().returnValues = []*Variable{ret}
|
|
}
|
|
|
|
callinj := t.fncallForG[g.ID]
|
|
for goid := range t.fncallForG {
|
|
if t.fncallForG[goid] == callinj {
|
|
delete(t.fncallForG, goid)
|
|
}
|
|
}
|
|
callinj.evalStack = nil
|
|
callinj.endCallInjection()
|
|
return err
|
|
}
|
|
|
|
func (scope *EvalScope) evalCallInjectionStart(op *evalop.CallInjectionStart, stack *evalStack) {
|
|
if scope.callCtx == nil {
|
|
stack.err = evalop.ErrFuncCallNotAllowed
|
|
return
|
|
}
|
|
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 happened 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 {
|
|
stack.err = err
|
|
return
|
|
}
|
|
stacklo = g2.stack.lo
|
|
}
|
|
if thread == nil {
|
|
stack.err = errGoroutineNotRunning
|
|
return
|
|
}
|
|
|
|
p := scope.callCtx.p
|
|
bi := scope.BinInfo
|
|
if !p.SupportsFunctionCalls() {
|
|
stack.err = errFuncCallUnsupportedBackend
|
|
return
|
|
}
|
|
|
|
dbgcallfn, dbgcallversion := debugCallFunction(bi)
|
|
if dbgcallfn == nil {
|
|
stack.err = errFuncCallUnsupported
|
|
return
|
|
}
|
|
|
|
// check that there are at least 256 bytes free on the stack
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
regs, err = regs.Copy()
|
|
if err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
if regs.SP()-bi.Arch.debugCallMinStackSize <= stacklo {
|
|
stack.err = errNotEnoughStack
|
|
return
|
|
}
|
|
protocolReg, ok := debugCallProtocolReg(bi.Arch.Name, dbgcallversion)
|
|
if !ok {
|
|
stack.err = errFuncCallUnsupported
|
|
return
|
|
}
|
|
if bi.Arch.RegistersToDwarfRegisters(0, regs).Reg(protocolReg) == nil {
|
|
stack.err = errFuncCallUnsupportedBackend
|
|
return
|
|
}
|
|
|
|
for _, v := range stack.stack {
|
|
if v.Flags&(VariableFakeAddress|VariableCPURegister|variableSaved) != 0 || v.Unreadable != nil || v.DwarfType == nil || v.RealType == nil || v.Addr == 0 {
|
|
continue
|
|
}
|
|
saveVariable(v)
|
|
}
|
|
|
|
fncall := functionCallState{
|
|
expr: op.Node,
|
|
savedRegs: regs,
|
|
protocolReg: protocolReg,
|
|
debugCallName: dbgcallfn.Name,
|
|
hasDebugPinner: scope.BinInfo.hasDebugPinner(),
|
|
}
|
|
|
|
if op.HasFunc {
|
|
err = funcCallEvalFuncExpr(scope, stack, &fncall)
|
|
if err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
switch bi.Arch.Name {
|
|
case "amd64":
|
|
if err := callOP(bi, thread, regs, dbgcallfn.Entry); err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
// write the desired argument frame size at SP-(2*pointer_size) (the extra pointer is the saved PC)
|
|
if err := writePointer(bi, scope.Mem, regs.SP()-3*uint64(bi.Arch.PtrSize()), uint64(fncall.argFrameSize)); err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
case "arm64", "ppc64le":
|
|
// debugCallV2 on arm64 needs a special call sequence, callOP can not be used
|
|
sp := regs.SP()
|
|
var spOffset uint64
|
|
if bi.Arch.Name == "arm64" {
|
|
spOffset = 2 * uint64(bi.Arch.PtrSize())
|
|
} else {
|
|
spOffset = 4 * uint64(bi.Arch.PtrSize())
|
|
}
|
|
sp -= spOffset
|
|
if err := setSP(thread, sp); err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
if err := writePointer(bi, scope.Mem, sp, regs.LR()); err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
if err := setLR(thread, regs.PC()); err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
if err := writePointer(bi, scope.Mem, sp-spOffset, uint64(fncall.argFrameSize)); err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
regs, err = thread.Registers()
|
|
if err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
regs, err = regs.Copy()
|
|
if err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
fncall.savedRegs = regs
|
|
err = setPC(thread, dbgcallfn.Entry)
|
|
if err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
fncallLog("function call initiated %v frame size %d goroutine %d (thread %d)", fncall.fn, fncall.argFrameSize, scope.g.ID, thread.ThreadID())
|
|
|
|
thread.Breakpoint().Clear() // since we moved address in PC the thread is no longer stopped at a breakpoint, leaving the breakpoint set will confuse Continue
|
|
p.fncallForG[scope.g.ID].startThreadID = thread.ThreadID()
|
|
|
|
stack.fncallPush(&fncall)
|
|
stack.push(newConstant(constant.MakeBool(!fncall.hasDebugPinner && (fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0)), scope.Mem))
|
|
stack.callInjectionContinue = true
|
|
}
|
|
|
|
func saveVariable(v *Variable) {
|
|
v.mem = cacheMemory(v.mem, v.Addr, int(v.RealType.Size()))
|
|
v.Flags |= variableSaved
|
|
if cachemem, ok := v.mem.(*memCache); ok {
|
|
v.Unreadable = cachemem.load()
|
|
}
|
|
}
|
|
|
|
func funcCallFinish(scope *EvalScope, stack *evalStack) {
|
|
fncall := stack.fncallPop()
|
|
if fncall.err != nil {
|
|
if stack.err == nil {
|
|
stack.err = fncall.err
|
|
} else {
|
|
fncallLog("additional fncall error: %v", fncall.err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if fncall.panicvar != nil {
|
|
if stack.err == nil {
|
|
stack.err = fncallPanicErr{fncall.panicvar}
|
|
} else {
|
|
fncallLog("additional fncall panic: %v", fncall.panicvar)
|
|
}
|
|
return
|
|
}
|
|
switch len(fncall.retvars) {
|
|
case 0:
|
|
r := newVariable("", 0, nil, scope.BinInfo, nil)
|
|
r.loaded = true
|
|
r.Unreadable = errors.New("no return values")
|
|
stack.push(r)
|
|
case 1:
|
|
stack.push(fncall.retvars[0])
|
|
default:
|
|
// create a fake variable without address or type to return multiple values
|
|
r := newVariable("", 0, nil, scope.BinInfo, nil)
|
|
r.loaded = true
|
|
r.Children = make([]Variable, len(fncall.retvars))
|
|
for i := range fncall.retvars {
|
|
r.Children[i] = *fncall.retvars[i]
|
|
}
|
|
stack.push(r)
|
|
}
|
|
}
|
|
|
|
// fncallPanicErr is the error returned if a called function panics
|
|
type fncallPanicErr struct {
|
|
panicVar *Variable
|
|
}
|
|
|
|
func (err fncallPanicErr) Error() string {
|
|
return "panic calling a function"
|
|
}
|
|
|
|
func fncallLog(fmtstr string, args ...interface{}) {
|
|
logflags.FnCallLogger().Infof(fmtstr, args...)
|
|
}
|
|
|
|
// writePointer writes val as an architecture pointer at addr in mem.
|
|
func writePointer(bi *BinaryInfo, mem MemoryReadWriter, addr, val uint64) error {
|
|
ptrbuf := make([]byte, bi.Arch.PtrSize())
|
|
|
|
// TODO: use target architecture endianness instead of LittleEndian
|
|
switch len(ptrbuf) {
|
|
case 4:
|
|
binary.LittleEndian.PutUint32(ptrbuf, uint32(val))
|
|
case 8:
|
|
binary.LittleEndian.PutUint64(ptrbuf, val)
|
|
default:
|
|
panic(fmt.Errorf("unsupported pointer size %d", len(ptrbuf)))
|
|
}
|
|
_, err := mem.WriteMemory(addr, ptrbuf)
|
|
return err
|
|
}
|
|
|
|
// callOP simulates a call instruction on the given thread:
|
|
// * pushes the current value of PC on the stack (adjusting SP)
|
|
// * changes the value of PC to callAddr
|
|
// Note: regs are NOT updated!
|
|
func callOP(bi *BinaryInfo, thread Thread, regs Registers, callAddr uint64) error {
|
|
switch bi.Arch.Name {
|
|
case "amd64":
|
|
sp := regs.SP()
|
|
// push PC on the stack
|
|
sp -= uint64(bi.Arch.PtrSize())
|
|
if err := setSP(thread, sp); err != nil {
|
|
return err
|
|
}
|
|
if err := writePointer(bi, thread.ProcessMemory(), sp, regs.PC()); err != nil {
|
|
return err
|
|
}
|
|
return setPC(thread, callAddr)
|
|
case "arm64", "ppc64le":
|
|
if err := setLR(thread, regs.PC()); err != nil {
|
|
return err
|
|
}
|
|
return setPC(thread, callAddr)
|
|
|
|
default:
|
|
panic("not implemented")
|
|
}
|
|
}
|
|
|
|
// funcCallEvalFuncExpr evaluates expr.Fun and returns the function that we're trying to call.
|
|
// If allowCalls is false function calls will be disabled even if scope.callCtx != nil
|
|
func funcCallEvalFuncExpr(scope *EvalScope, stack *evalStack, fncall *functionCallState) error {
|
|
bi := scope.BinInfo
|
|
|
|
fnvar := stack.peek()
|
|
if fnvar.Kind != reflect.Func {
|
|
return fmt.Errorf("expression %q is not a function", exprToString(fncall.expr.Fun))
|
|
}
|
|
fnvar.loadValue(LoadConfig{false, 0, 0, 0, 0, 0})
|
|
if fnvar.Unreadable != nil {
|
|
return fnvar.Unreadable
|
|
}
|
|
if fnvar.Base == 0 {
|
|
return errors.New("nil pointer dereference")
|
|
}
|
|
fncall.fn = bi.PCToFunc(fnvar.Base)
|
|
if fncall.fn == nil {
|
|
return fmt.Errorf("could not find DIE for function %q", exprToString(fncall.expr.Fun))
|
|
}
|
|
if !fncall.fn.cu.isgo {
|
|
return errNotAGoFunction
|
|
}
|
|
fncall.closureAddr = fnvar.closureAddr
|
|
|
|
var err error
|
|
fncall.argFrameSize, fncall.formalArgs, err = funcCallArgs(fncall.fn, bi, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
argnum := len(fncall.expr.Args)
|
|
|
|
// If the function variable has a child then that child is the method
|
|
// receiver. However, if the method receiver is not being used (e.g.
|
|
// func (_ X) Foo()) then it will not actually be listed as a formal
|
|
// argument. Ensure that we are really off by 1 to add the receiver to
|
|
// the function call.
|
|
if len(fnvar.Children) > 0 && argnum == (len(fncall.formalArgs)-1) {
|
|
argnum++
|
|
fncall.receiver = &fnvar.Children[0]
|
|
fncall.receiver.Name = exprToString(fncall.expr.Fun)
|
|
}
|
|
|
|
if argnum > len(fncall.formalArgs) {
|
|
return errTooManyArguments
|
|
}
|
|
if argnum < len(fncall.formalArgs) {
|
|
return errNotEnoughArguments
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type funcCallArg struct {
|
|
name string
|
|
typ godwarf.Type
|
|
off int64
|
|
dwarfEntry *godwarf.Tree // non-nil if Go 1.17+
|
|
isret bool
|
|
}
|
|
|
|
func funcCallCopyOneArg(scope *EvalScope, fncall *functionCallState, actualArg *Variable, formalArg *funcCallArg, thread Thread) error {
|
|
if scope.callCtx.checkEscape {
|
|
//TODO(aarzilli): only apply the escapeCheck to leaking parameters.
|
|
err := allPointers(actualArg, formalArg.name, func(addr uint64, name string) error {
|
|
if !pointerEscapes(addr, scope.g.stack, scope.callCtx.stacks) {
|
|
return fmt.Errorf("cannot use %s as argument %s in function %s: stack object passed to escaping pointer: %s", actualArg.Name, formalArg.name, fncall.fn.Name, name)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
//TODO(aarzilli): automatic wrapping in interfaces for cases not handled
|
|
// by convertToEface.
|
|
|
|
formalScope, err := GoroutineScope(scope.target, thread)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var formalArgVar *Variable
|
|
if formalArg.dwarfEntry != nil {
|
|
var err error
|
|
formalArgVar, err = extractVarInfoFromEntry(scope.target, formalScope.BinInfo, formalScope.image(), formalScope.Regs, formalScope.Mem, formalArg.dwarfEntry, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
formalArgVar = newVariable(formalArg.name, uint64(formalArg.off+formalScope.Regs.CFA), formalArg.typ, scope.BinInfo, scope.Mem)
|
|
}
|
|
if err := scope.setValue(formalArgVar, actualArg, actualArg.Name); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func funcCallArgs(fn *Function, bi *BinaryInfo, includeRet bool) (argFrameSize int64, formalArgs []funcCallArg, err error) {
|
|
dwarfTree, err := fn.cu.image.getDwarfTree(fn.offset)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("DWARF read error: %v", err)
|
|
}
|
|
|
|
if bi.regabi && fn.Optimized() {
|
|
if runtimeWhitelist[fn.Name] {
|
|
runtimeOptimizedWorkaround(bi, fn.cu.image, dwarfTree)
|
|
} else {
|
|
// Debug info for function arguments on optimized functions is currently
|
|
// too incomplete to attempt injecting calls to arbitrary optimized
|
|
// functions.
|
|
// Prior to regabi we could do this because the ABI was simple enough to
|
|
// manually encode it in Delve.
|
|
// Runtime.mallocgc is an exception, we specifically patch it's DIE to be
|
|
// correct for call injection purposes.
|
|
return 0, nil, fmt.Errorf("can not call optimized function %s when regabi is in use", fn.Name)
|
|
}
|
|
}
|
|
|
|
varEntries := reader.Variables(dwarfTree, fn.Entry, int(^uint(0)>>1), reader.VariablesSkipInlinedSubroutines)
|
|
|
|
// typechecks arguments, calculates argument frame size
|
|
for _, entry := range varEntries {
|
|
if entry.Tag != dwarf.TagFormalParameter {
|
|
continue
|
|
}
|
|
argname, typ, err := readVarEntry(entry.Tree, fn.cu.image)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
typ = resolveTypedef(typ)
|
|
|
|
var formalArg *funcCallArg
|
|
if bi.regabi {
|
|
formalArg, err = funcCallArgRegABI(fn, bi, entry, argname, typ, &argFrameSize)
|
|
} else {
|
|
formalArg, err = funcCallArgOldABI(fn, bi, entry, argname, typ, &argFrameSize)
|
|
}
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
if !formalArg.isret || includeRet {
|
|
formalArgs = append(formalArgs, *formalArg)
|
|
}
|
|
}
|
|
|
|
if bi.regabi {
|
|
// The argument frame size is computed conservatively, assuming that
|
|
// there's space for each argument on the stack even if its passed in
|
|
// registers. Unfortunately this isn't quite enough because the register
|
|
// assignment algorithm Go uses can result in an amount of additional
|
|
// space used due to alignment requirements, bounded by the number of argument registers.
|
|
// Because we currently don't have an easy way to obtain the frame size,
|
|
// let's be even more conservative.
|
|
// A safe lower-bound on the size of the argument frame includes space for
|
|
// each argument plus the total bytes of register arguments.
|
|
// This is derived from worst-case alignment padding of up to
|
|
// (pointer-word-bytes - 1) per argument passed in registers.
|
|
// See: https://github.com/go-delve/delve/pull/2451#discussion_r665761531
|
|
// TODO: Make this generic for other platforms.
|
|
argFrameSize = alignAddr(argFrameSize, 8)
|
|
argFrameSize += int64(bi.Arch.maxRegArgBytes)
|
|
}
|
|
|
|
sort.Slice(formalArgs, func(i, j int) bool {
|
|
return formalArgs[i].off < formalArgs[j].off
|
|
})
|
|
|
|
return argFrameSize, formalArgs, nil
|
|
}
|
|
|
|
func funcCallArgOldABI(fn *Function, bi *BinaryInfo, entry reader.Variable, argname string, typ godwarf.Type, pargFrameSize *int64) (*funcCallArg, error) {
|
|
const CFA = 0x1000
|
|
var off int64
|
|
|
|
locprog, _, err := bi.locationExpr(entry, dwarf.AttrLocation, fn.Entry)
|
|
if err != nil {
|
|
err = fmt.Errorf("could not get argument location of %s: %v", argname, err)
|
|
} else {
|
|
var pieces []op.Piece
|
|
off, pieces, err = op.ExecuteStackProgram(op.DwarfRegisters{CFA: CFA, FrameBase: CFA}, locprog, bi.Arch.PtrSize(), nil)
|
|
if err != nil {
|
|
err = fmt.Errorf("unsupported location expression for argument %s: %v", argname, err)
|
|
}
|
|
if pieces != nil {
|
|
err = fmt.Errorf("unsupported location expression for argument %s (uses DW_OP_piece)", argname)
|
|
}
|
|
off -= CFA
|
|
}
|
|
if err != nil {
|
|
// With Go version 1.12 or later we can trust that the arguments appear
|
|
// in the same order as declared, which means we can calculate their
|
|
// address automatically.
|
|
// With this we can call optimized functions (which sometimes do not have
|
|
// an argument address, due to a compiler bug) as well as runtime
|
|
// functions (which are always optimized).
|
|
off = *pargFrameSize
|
|
off = alignAddr(off, typ.Align())
|
|
}
|
|
|
|
if e := off + typ.Size(); e > *pargFrameSize {
|
|
*pargFrameSize = e
|
|
}
|
|
|
|
isret, _ := entry.Val(dwarf.AttrVarParam).(bool)
|
|
return &funcCallArg{name: argname, typ: typ, off: off, isret: isret}, nil
|
|
}
|
|
|
|
func funcCallArgRegABI(fn *Function, bi *BinaryInfo, entry reader.Variable, argname string, typ godwarf.Type, pargFrameSize *int64) (*funcCallArg, error) {
|
|
// Conservatively calculate the full stack argument space for ABI0.
|
|
*pargFrameSize = alignAddr(*pargFrameSize, typ.Align())
|
|
*pargFrameSize += typ.Size()
|
|
|
|
isret, _ := entry.Val(dwarf.AttrVarParam).(bool)
|
|
return &funcCallArg{name: argname, typ: typ, dwarfEntry: entry.Tree, isret: isret}, nil
|
|
}
|
|
|
|
// alignAddr rounds up addr to a multiple of align. Align must be a power of 2.
|
|
func alignAddr(addr, align int64) int64 {
|
|
return (addr + align - 1) &^ (align - 1)
|
|
}
|
|
|
|
// allPointers calls f on every pointer contained in v
|
|
func allPointers(v *Variable, name string, f func(addr uint64, name string) error) error {
|
|
if v.Unreadable != nil {
|
|
return fmt.Errorf("escape check for %s failed, variable unreadable: %v", name, v.Unreadable)
|
|
}
|
|
switch v.Kind {
|
|
case reflect.Ptr, reflect.UnsafePointer:
|
|
var w *Variable
|
|
if len(v.Children) == 1 {
|
|
// this branch is here to support pointers constructed with typecasts from ints or the '&' operator
|
|
w = &v.Children[0]
|
|
} else {
|
|
w = v.maybeDereference()
|
|
}
|
|
return f(w.Addr, name)
|
|
case reflect.Chan, reflect.String, reflect.Slice:
|
|
return f(v.Base, name)
|
|
case reflect.Map:
|
|
sv := v.clone()
|
|
sv.RealType = resolveTypedef(&(v.RealType.(*godwarf.MapType).TypedefType))
|
|
sv = sv.maybeDereference()
|
|
return f(sv.Addr, name)
|
|
case reflect.Interface:
|
|
sv := v.clone()
|
|
sv.RealType = resolveTypedef(&(v.RealType.(*godwarf.InterfaceType).TypedefType))
|
|
sv = sv.maybeDereference()
|
|
sv.Kind = reflect.Struct
|
|
return allPointers(sv, name, f)
|
|
case reflect.Struct:
|
|
t := v.RealType.(*godwarf.StructType)
|
|
for _, field := range t.Field {
|
|
fv, _ := v.toField(field)
|
|
if err := allPointers(fv, fmt.Sprintf("%s.%s", name, field.Name), f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case reflect.Array:
|
|
for i := int64(0); i < v.Len; i++ {
|
|
sv, _ := v.sliceAccess(int(i))
|
|
if err := allPointers(sv, fmt.Sprintf("%s[%d]", name, i), f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case reflect.Func:
|
|
if err := f(v.funcvalAddr(), name); err != nil {
|
|
return err
|
|
}
|
|
case reflect.Complex64, reflect.Complex128, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Bool, reflect.Float32, reflect.Float64:
|
|
// nothing to do
|
|
default:
|
|
panic(fmt.Errorf("not implemented: %s", v.Kind))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func pointerEscapes(addr uint64, stack stack, stacks []stack) bool {
|
|
if addr >= stack.lo && addr < stack.hi {
|
|
return false
|
|
}
|
|
for _, stack := range stacks {
|
|
if addr >= stack.lo && addr < stack.hi {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
const (
|
|
debugCallRegPrecheckFailed = 8
|
|
debugCallRegCompleteCall = 0
|
|
debugCallRegReadReturn = 1
|
|
debugCallRegReadPanic = 2
|
|
debugCallRegRestoreRegisters = 16
|
|
)
|
|
|
|
// funcCallStep executes one step of the function call injection protocol.
|
|
func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
|
|
p := callScope.callCtx.p
|
|
bi := p.BinInfo()
|
|
fncall := stack.fncallPeek()
|
|
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
fncall.err = err
|
|
return true
|
|
}
|
|
|
|
regval := bi.Arch.RegistersToDwarfRegisters(0, regs).Uint64Val(fncall.protocolReg)
|
|
|
|
if logflags.FnCall() {
|
|
loc, _ := thread.Location()
|
|
var pc uint64
|
|
var fnname string
|
|
if loc != nil {
|
|
pc = loc.PC
|
|
if loc.Fn != nil {
|
|
fnname = loc.Fn.Name
|
|
}
|
|
}
|
|
fncallLog("function call interrupt gid=%d (original) thread=%d regval=%#x (PC=%#x in %s %s:%d)", callScope.g.ID, thread.ThreadID(), regval, pc, fnname, loc.File, loc.Line)
|
|
}
|
|
|
|
switch regval {
|
|
case debugCallRegPrecheckFailed: // 8
|
|
stack.callInjectionContinue = true
|
|
archoff := uint64(0)
|
|
if bi.Arch.Name == "arm64" {
|
|
archoff = 8
|
|
} else if bi.Arch.Name == "ppc64le" {
|
|
archoff = 40
|
|
}
|
|
// get error from top of the stack and return it to user
|
|
errvar, err := readStackVariable(p, thread, regs, archoff, "string", loadFullValue)
|
|
if err != nil {
|
|
fncall.err = fmt.Errorf("could not get precheck error reason: %v", err)
|
|
break
|
|
}
|
|
errvar.Name = "err"
|
|
fncall.err = fmt.Errorf("%v", constant.StringVal(errvar.Value))
|
|
|
|
case debugCallRegCompleteCall: // 0
|
|
p.fncallForG[callScope.g.ID].startThreadID = 0
|
|
|
|
case debugCallRegRestoreRegisters: // 16
|
|
// runtime requests that we restore the registers (all except pc and sp),
|
|
// this is also the last step of the function call protocol.
|
|
pc, sp := regs.PC(), regs.SP()
|
|
if err := thread.RestoreRegisters(fncall.savedRegs); err != nil {
|
|
fncall.err = fmt.Errorf("could not restore registers: %v", err)
|
|
}
|
|
if err := setPC(thread, pc); err != nil {
|
|
fncall.err = fmt.Errorf("could not restore PC: %v", err)
|
|
}
|
|
if err := setSP(thread, sp); err != nil {
|
|
fncall.err = fmt.Errorf("could not restore SP: %v", err)
|
|
}
|
|
fncallLog("stepping thread %d", thread.ThreadID())
|
|
if err := stepInstructionOut(callScope.callCtx.grp, p, thread, fncall.debugCallName, fncall.debugCallName); err != nil {
|
|
fncall.err = fmt.Errorf("could not step out of %s: %v", fncall.debugCallName, err)
|
|
}
|
|
if bi.Arch.Name == "amd64" {
|
|
// The tail of debugCallV2 corrupts the state of RFLAGS, we must restore
|
|
// it one extra time after stepping out of it.
|
|
// See https://github.com/go-delve/delve/issues/2985 and
|
|
// TestCallInjectionFlagCorruption
|
|
rflags := bi.Arch.RegistersToDwarfRegisters(0, fncall.savedRegs).Uint64Val(regnum.AMD64_Rflags)
|
|
err := thread.SetReg(regnum.AMD64_Rflags, op.DwarfRegisterFromUint64(rflags))
|
|
if err != nil {
|
|
fncall.err = fmt.Errorf("could not restore RFLAGS register: %v", err)
|
|
}
|
|
}
|
|
return true
|
|
|
|
case debugCallRegReadReturn: // 1
|
|
// read return arguments from stack
|
|
stack.callInjectionContinue = true
|
|
if fncall.panicvar != nil || fncall.err != nil {
|
|
break
|
|
}
|
|
retScope, err := ThreadScope(p, thread)
|
|
if err != nil {
|
|
fncall.err = fmt.Errorf("could not get return values: %v", err)
|
|
break
|
|
}
|
|
|
|
// pretend we are still inside the function we called
|
|
fakeFunctionEntryScope(retScope, fncall.fn, int64(regs.SP()), regs.SP()-uint64(bi.Arch.PtrSize()))
|
|
var flags localsFlags
|
|
flags |= localsNoDeclLineCheck // if the function we are calling is an autogenerated stub then declaration lines have no meaning
|
|
if !bi.regabi {
|
|
flags |= localsTrustArgOrder
|
|
}
|
|
|
|
fncall.retvars, err = retScope.Locals(flags, "")
|
|
if err != nil {
|
|
fncall.err = fmt.Errorf("could not get return values: %v", err)
|
|
break
|
|
}
|
|
fncall.retvars = filterVariables(fncall.retvars, func(v *Variable) bool {
|
|
return (v.Flags & VariableReturnArgument) != 0
|
|
})
|
|
|
|
if !fncall.doPinning {
|
|
loadValues(fncall.retvars, callScope.callCtx.retLoadCfg)
|
|
}
|
|
for _, v := range fncall.retvars {
|
|
v.Flags |= VariableFakeAddress
|
|
}
|
|
|
|
if fncall.doPinning {
|
|
stack.callInjectionContinue = false
|
|
for _, v := range fncall.retvars {
|
|
saveVariable(v)
|
|
allPointers(v, "", func(addr uint64, _ string) error {
|
|
if addr != 0 && pointerEscapes(addr, callScope.g.stack, callScope.callCtx.stacks) {
|
|
fncall.addrsToPin = append(fncall.addrsToPin, addr)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
slices.Sort(fncall.addrsToPin)
|
|
fncall.addrsToPin = slices.Compact(fncall.addrsToPin)
|
|
|
|
return false // will continue with evalop.CallInjectionComplete2
|
|
}
|
|
|
|
callInjectionComplete2(callScope, bi, fncall, regs, thread)
|
|
|
|
case debugCallRegReadPanic: // 2
|
|
// read panic value from stack
|
|
stack.callInjectionContinue = true
|
|
archoff := uint64(0)
|
|
if bi.Arch.Name == "arm64" {
|
|
archoff = 8
|
|
} else if bi.Arch.Name == "ppc64le" {
|
|
archoff = 32
|
|
}
|
|
fncall.panicvar, err = readStackVariable(p, thread, regs, archoff, "interface {}", callScope.callCtx.retLoadCfg)
|
|
if err != nil {
|
|
fncall.err = fmt.Errorf("could not get panic: %v", err)
|
|
break
|
|
}
|
|
fncall.panicvar.Name = "~panic"
|
|
|
|
default:
|
|
// Got an unknown protocol register value, this is probably bad but the safest thing
|
|
// possible is to ignore it and hope it didn't matter.
|
|
stack.callInjectionContinue = true
|
|
fncallLog("unknown value of protocol register %#x", regval)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func callInjectionComplete2(callScope *EvalScope, bi *BinaryInfo, fncall *functionCallState, regs Registers, thread Thread) {
|
|
// 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)
|
|
}
|
|
if bi.Arch.Name == "arm64" || bi.Arch.Name == "ppc64le" {
|
|
oldlr, err := readUintRaw(thread.ProcessMemory(), regs.SP(), int64(bi.Arch.PtrSize()))
|
|
if err != nil {
|
|
fncall.err = fmt.Errorf("could not restore LR: %v", err)
|
|
return
|
|
}
|
|
if err = setLR(thread, oldlr); err != nil {
|
|
fncall.err = fmt.Errorf("could not restore LR: %v", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (scope *EvalScope) evalCallInjectionSetTarget(op *evalop.CallInjectionSetTarget, stack *evalStack, thread Thread) {
|
|
fncall := stack.fncallPeek()
|
|
if !fncall.hasDebugPinner && (fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0) {
|
|
stack.err = funcCallEvalFuncExpr(scope, stack, fncall)
|
|
if stack.err != nil {
|
|
return
|
|
}
|
|
}
|
|
stack.pop() // target function, consumed by funcCallEvalFuncExpr either above or in evalop.CallInjectionStart
|
|
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
stack.err = err
|
|
return
|
|
}
|
|
|
|
if fncall.closureAddr != 0 {
|
|
// When calling a function pointer we must set the DX register to the
|
|
// address of the function pointer itself.
|
|
setClosureReg(thread, fncall.closureAddr)
|
|
}
|
|
|
|
undo := new(undoInjection)
|
|
undo.oldpc = regs.PC()
|
|
if scope.BinInfo.Arch.Name == "arm64" || scope.BinInfo.Arch.Name == "ppc64le" {
|
|
undo.oldlr = regs.LR()
|
|
}
|
|
callOP(scope.BinInfo, thread, regs, fncall.fn.Entry)
|
|
|
|
fncall.undoInjection = undo
|
|
|
|
if fncall.receiver != nil {
|
|
err := funcCallCopyOneArg(scope, fncall, fncall.receiver, &fncall.formalArgs[0], thread)
|
|
if err != nil {
|
|
stack.err = fmt.Errorf("could not set call receiver: %v", err)
|
|
return
|
|
}
|
|
fncall.formalArgs = fncall.formalArgs[1:]
|
|
}
|
|
}
|
|
|
|
func readStackVariable(t *Target, thread Thread, regs Registers, off uint64, typename string, loadCfg LoadConfig) (*Variable, error) {
|
|
bi := thread.BinInfo()
|
|
scope, err := ThreadScope(t, thread)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
typ, err := bi.findType(typename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
v := newVariable("", regs.SP()+off, typ, scope.BinInfo, scope.Mem)
|
|
v.loadValue(loadCfg)
|
|
if v.Unreadable != nil {
|
|
return nil, v.Unreadable
|
|
}
|
|
v.Flags |= VariableFakeAddress
|
|
return v, nil
|
|
}
|
|
|
|
// fakeFunctionEntryScope alters scope to pretend that we are at the entry point of
|
|
// fn and CFA and SP are the ones passed as argument.
|
|
// This function is used to create a scope for a call frame that doesn't
|
|
// exist anymore, to read the return variables of an injected function call,
|
|
// or after a stepout command.
|
|
func fakeFunctionEntryScope(scope *EvalScope, fn *Function, cfa int64, sp uint64) error {
|
|
scope.PC = fn.Entry
|
|
scope.Fn = fn
|
|
scope.File, scope.Line = scope.BinInfo.EntryLineForFunc(fn)
|
|
scope.Regs.CFA = cfa
|
|
scope.Regs.Reg(scope.Regs.SPRegNum).Uint64Val = sp
|
|
scope.Regs.Reg(scope.Regs.PCRegNum).Uint64Val = fn.Entry
|
|
|
|
fn.cu.image.dwarfReader.Seek(fn.offset)
|
|
e, err := fn.cu.image.dwarfReader.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
scope.Regs.FrameBase, _, _, _ = scope.BinInfo.Location(e, dwarf.AttrFrameBase, scope.PC, scope.Regs, nil)
|
|
return nil
|
|
}
|
|
|
|
func (scope *EvalScope) callInjectionStartSpecial(stack *evalStack, op *evalop.CallInjectionStartSpecial, curthread Thread) bool {
|
|
fnv, err := scope.findGlobalInternal(op.FnName)
|
|
if fnv == nil {
|
|
if err == nil {
|
|
err = fmt.Errorf("function %s not found", op.FnName)
|
|
}
|
|
stack.err = err
|
|
return false
|
|
}
|
|
stack.push(fnv)
|
|
scope.evalCallInjectionStart(&evalop.CallInjectionStart{HasFunc: true, Node: &ast.CallExpr{
|
|
Fun: &ast.Ident{Name: op.FnName},
|
|
Args: op.ArgAst,
|
|
}}, stack)
|
|
if stack.err == nil {
|
|
stack.pop() // return value of evalop.CallInjectionStart
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (scope *EvalScope) convertAllocToString(stack *evalStack) {
|
|
mallocv := stack.pop()
|
|
v := stack.pop()
|
|
|
|
mallocv.loadValue(loadFullValue)
|
|
|
|
if mallocv.Unreadable != nil {
|
|
stack.err = mallocv.Unreadable
|
|
return
|
|
}
|
|
|
|
if mallocv.DwarfType.String() != "*void" {
|
|
stack.err = fmt.Errorf("unexpected return type for mallocgc call: %v", mallocv.DwarfType.String())
|
|
return
|
|
}
|
|
|
|
if len(mallocv.Children) != 1 {
|
|
stack.err = errors.New("internal error, could not interpret return value of mallocgc call")
|
|
return
|
|
}
|
|
|
|
v.Base = mallocv.Children[0].Addr
|
|
_, stack.err = scope.Mem.WriteMemory(v.Base, []byte(constant.StringVal(v.Value)))
|
|
stack.push(v)
|
|
}
|
|
|
|
func isCallInjectionStop(t *Target, thread Thread, loc *Location) bool {
|
|
if loc.Fn == nil {
|
|
return false
|
|
}
|
|
if !strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix1) && !strings.HasPrefix(loc.Fn.Name, debugCallFunctionNamePrefix2) {
|
|
return false
|
|
}
|
|
if loc.PC == loc.Fn.Entry {
|
|
// call injection just started, did not make any progress before being interrupted by a concurrent breakpoint.
|
|
return false
|
|
}
|
|
off := int64(0)
|
|
if thread.BinInfo().Arch.breakInstrMovesPC {
|
|
off = -int64(len(thread.BinInfo().Arch.breakpointInstruction))
|
|
}
|
|
text, err := disassembleCurrentInstruction(t, thread, off)
|
|
if err != nil || len(text) == 0 {
|
|
return false
|
|
}
|
|
return text[0].IsHardBreak()
|
|
}
|
|
|
|
// callInjectionProtocol is the function called from Continue to progress
|
|
// the injection protocol for all threads.
|
|
// Returns true if a call injection terminated
|
|
func callInjectionProtocol(t *Target, trapthread Thread, threads []Thread) (done bool, err error) {
|
|
if len(t.fncallForG) == 0 {
|
|
// we aren't injecting any calls, no need to check the threads.
|
|
return false, nil
|
|
}
|
|
currentThread := t.currentThread
|
|
defer func() {
|
|
t.currentThread = currentThread
|
|
}()
|
|
for _, thread := range threads {
|
|
loc, err := thread.Location()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if (thread.ThreadID() != trapthread.ThreadID()) && !thread.SoftExc() {
|
|
continue
|
|
}
|
|
if !isCallInjectionStop(t, thread, loc) {
|
|
continue
|
|
}
|
|
|
|
regs, _ := thread.Registers()
|
|
fncallLog("call injection found thread=%d %s %s:%d PC=%#x SP=%#x", thread.ThreadID(), loc.Fn.Name, loc.File, loc.Line, regs.PC(), regs.SP())
|
|
|
|
g, callinj, err := findCallInjectionStateForThread(t, thread)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
arch := thread.BinInfo().Arch
|
|
if !arch.breakInstrMovesPC {
|
|
setPC(thread, loc.PC+uint64(len(arch.breakpointInstruction)))
|
|
}
|
|
|
|
fncallLog("step for injection on goroutine %d (current) thread=%d (location %s)", g.ID, thread.ThreadID(), loc.Fn.Name)
|
|
t.currentThread = thread
|
|
callinj.evalStack.resume(g)
|
|
if !callinj.evalStack.callInjectionContinue {
|
|
err := finishEvalExpressionWithCalls(t, g, callinj.evalStack)
|
|
if err != nil {
|
|
return done, err
|
|
}
|
|
done = true
|
|
}
|
|
}
|
|
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.evalStack == 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.evalStack != 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()
|
|
}
|
|
|
|
// debugCallFunction searches for the debug call function in the binary and
|
|
// uses this search to detect the debug call version.
|
|
// Returns the debug call function and its version as an integer (the lowest
|
|
// valid version is 1) or nil and zero.
|
|
func debugCallFunction(bi *BinaryInfo) (*Function, int) {
|
|
for version := maxDebugCallVersion; version >= 1; version-- {
|
|
name := debugCallFunctionNamePrefix2 + "V" + strconv.Itoa(version)
|
|
fn := bi.lookupOneFunc(name)
|
|
if fn != nil {
|
|
return fn, version
|
|
}
|
|
}
|
|
return nil, 0
|
|
}
|
|
|
|
// debugCallProtocolReg returns the register ID (as defined in pkg/dwarf/regnum)
|
|
// of the register used in the debug call protocol, given the debug call version.
|
|
// Also returns a bool indicating whether the version is supported.
|
|
func debugCallProtocolReg(archName string, version int) (uint64, bool) {
|
|
switch archName {
|
|
case "amd64":
|
|
var protocolReg uint64
|
|
switch version {
|
|
case 1:
|
|
protocolReg = regnum.AMD64_Rax
|
|
case 2:
|
|
protocolReg = regnum.AMD64_R12
|
|
default:
|
|
return 0, false
|
|
}
|
|
return protocolReg, true
|
|
case "arm64", "ppc64le":
|
|
if version == 2 {
|
|
return regnum.ARM64_X0 + 20, true
|
|
}
|
|
return 0, false
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
// runtimeWhitelist is a list of functions in the runtime that we can call
|
|
// (through call injection) even if they are optimized.
|
|
var runtimeWhitelist = map[string]bool{
|
|
"runtime.mallocgc": true,
|
|
evalop.DebugPinnerFunctionName: true,
|
|
"runtime.(*Pinner).Unpin": true,
|
|
"runtime.(*Pinner).Pin": true,
|
|
}
|
|
|
|
// runtimeOptimizedWorkaround modifies the input DIE so that arguments and
|
|
// return variables have the appropriate registers for call injection.
|
|
// This function can not be called on arbitrary DIEs, it is only valid for
|
|
// the functions specified in runtimeWhitelist.
|
|
// In particular this will fail if any of the arguments of the function
|
|
// passed in input does not fit in an integer CPU register.
|
|
func runtimeOptimizedWorkaround(bi *BinaryInfo, image *Image, in *godwarf.Tree) {
|
|
if image.workaroundCache == nil {
|
|
image.workaroundCache = make(map[dwarf.Offset]*godwarf.Tree)
|
|
}
|
|
if image.workaroundCache[in.Offset] == in {
|
|
return
|
|
}
|
|
image.workaroundCache[in.Offset] = in
|
|
|
|
curArg, curRet := 0, 0
|
|
for _, child := range in.Children {
|
|
if child.Tag == dwarf.TagFormalParameter {
|
|
childEntry, ok := child.Entry.(*dwarf.Entry)
|
|
if !ok {
|
|
panic("internal error: bad DIE for runtimeOptimizedWorkaround")
|
|
}
|
|
isret, _ := child.Entry.Val(dwarf.AttrVarParam).(bool)
|
|
|
|
var reg int
|
|
if isret {
|
|
reg = bi.Arch.argumentRegs[curRet]
|
|
curRet++
|
|
} else {
|
|
reg = bi.Arch.argumentRegs[curArg]
|
|
curArg++
|
|
}
|
|
|
|
newlocfield := dwarf.Field{Attr: dwarf.AttrLocation, Val: []byte{byte(op.DW_OP_reg0) + byte(reg)}, Class: dwarf.ClassBlock}
|
|
|
|
locfield := childEntry.AttrField(dwarf.AttrLocation)
|
|
if locfield != nil {
|
|
*locfield = newlocfield
|
|
} else {
|
|
childEntry.Field = append(childEntry.Field, newlocfield)
|
|
}
|
|
}
|
|
}
|
|
}
|