proc: adds pointer pinning to call injection (#3787)
This commit adds a new mode to call injection. If the runtime.debugPinner function is available in the target executable it obtains a pinner by calling it and then uses it to pin the pointers in the results of call injection. This allows the code for call injection to be refactored to execute the calls in the normal order, since it doesn't need to be concerned with having space on the target's memory to store intermediate values. Updates #3310
This commit is contained in:
parent
52405ba86b
commit
025d47c6e9
@ -34,6 +34,7 @@ import (
|
||||
"github.com/go-delve/delve/pkg/internal/gosym"
|
||||
"github.com/go-delve/delve/pkg/logflags"
|
||||
"github.com/go-delve/delve/pkg/proc/debuginfod"
|
||||
"github.com/go-delve/delve/pkg/proc/evalop"
|
||||
"github.com/hashicorp/golang-lru/simplelru"
|
||||
)
|
||||
|
||||
@ -110,6 +111,7 @@ type BinaryInfo struct {
|
||||
// Go 1.17 register ABI is enabled.
|
||||
regabi bool
|
||||
|
||||
debugPinnerFn *Function
|
||||
logger logflags.Logger
|
||||
}
|
||||
|
||||
@ -2574,13 +2576,23 @@ func (bi *BinaryInfo) LookupFunc() map[string][]*Function {
|
||||
}
|
||||
|
||||
func (bi *BinaryInfo) lookupOneFunc(name string) *Function {
|
||||
if name == evalop.DebugPinnerFunctionName && bi.debugPinnerFn != nil {
|
||||
return bi.debugPinnerFn
|
||||
}
|
||||
fns := bi.LookupFunc()[name]
|
||||
if fns == nil {
|
||||
return nil
|
||||
}
|
||||
if name == evalop.DebugPinnerFunctionName {
|
||||
bi.debugPinnerFn = fns[0]
|
||||
}
|
||||
return fns[0]
|
||||
}
|
||||
|
||||
func (bi *BinaryInfo) hasDebugPinner() bool {
|
||||
return bi.lookupOneFunc(evalop.DebugPinnerFunctionName) != nil
|
||||
}
|
||||
|
||||
// loadDebugInfoMapsCompileUnit loads entry from a single compile unit.
|
||||
func (bi *BinaryInfo) loadDebugInfoMapsCompileUnit(ctxt *loadDebugInfoMapsContext, image *Image, reader *reader.Reader, cu *compileUnit) {
|
||||
hasAttrGoPkgName := goversion.ProducerAfterOrEqual(cu.producer, 1, 13)
|
||||
|
@ -30,3 +30,14 @@ func NewCompositeMemory(p *Target, pieces []op.Piece, base uint64) (*compositeMe
|
||||
func IsJNZ(inst archInst) bool {
|
||||
return inst.(*x86Inst).Op == x86asm.JNE
|
||||
}
|
||||
|
||||
// HasDebugPinner returns true if the target has runtime.debugPinner.
|
||||
func (bi *BinaryInfo) HasDebugPinner() bool {
|
||||
return bi.hasDebugPinner()
|
||||
}
|
||||
|
||||
// DebugPinCount returns the number of addresses pinned during the last
|
||||
// function call injection.
|
||||
func DebugPinCount() int {
|
||||
return debugPinCount
|
||||
}
|
||||
|
@ -186,9 +186,17 @@ func GoroutineScope(t *Target, thread Thread) (*EvalScope, error) {
|
||||
return FrameToScope(t, thread.ProcessMemory(), g, threadID, locations...), nil
|
||||
}
|
||||
|
||||
func (scope *EvalScope) evalopFlags() evalop.Flags {
|
||||
flags := evalop.Flags(0)
|
||||
if scope.BinInfo.hasDebugPinner() {
|
||||
flags |= evalop.HasDebugPinner
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// EvalExpression returns the value of the given expression.
|
||||
func (scope *EvalScope) EvalExpression(expr string, cfg LoadConfig) (*Variable, error) {
|
||||
ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, false)
|
||||
ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, scope.evalopFlags())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -642,7 +650,7 @@ func (scope *EvalScope) setValue(dstv, srcv *Variable, srcExpr string) error {
|
||||
|
||||
// SetVariable sets the value of the named variable
|
||||
func (scope *EvalScope) SetVariable(name, value string) error {
|
||||
ops, err := evalop.CompileSet(scopeToEvalLookup{scope}, name, value)
|
||||
ops, err := evalop.CompileSet(scopeToEvalLookup{scope}, name, value, scope.evalopFlags())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -828,9 +836,13 @@ type evalStack struct {
|
||||
scope *EvalScope
|
||||
curthread Thread
|
||||
lastRetiredFncall *functionCallState
|
||||
debugPinner *Variable
|
||||
}
|
||||
|
||||
func (s *evalStack) push(v *Variable) {
|
||||
if v == nil {
|
||||
panic(errors.New("internal debugger error, nil pushed onto variables stack"))
|
||||
}
|
||||
s.stack = append(s.stack, v)
|
||||
}
|
||||
|
||||
@ -944,7 +956,7 @@ func (stack *evalStack) run() {
|
||||
stack.executeOp()
|
||||
// If the instruction we just executed requests the call injection
|
||||
// protocol by setting callInjectionContinue we switch to it.
|
||||
if stack.callInjectionContinue {
|
||||
if stack.callInjectionContinue && stack.err == nil {
|
||||
scope.callCtx.injectionThread = nil
|
||||
return
|
||||
}
|
||||
@ -959,13 +971,22 @@ func (stack *evalStack) run() {
|
||||
// injections before returning.
|
||||
|
||||
if len(stack.fncalls) > 0 {
|
||||
fncallLog("undoing calls (%v)", stack.err)
|
||||
fncall := stack.fncallPeek()
|
||||
if fncall == stack.lastRetiredFncall {
|
||||
stack.err = fmt.Errorf("internal debugger error: could not undo injected call during error recovery, original error: %v", stack.err)
|
||||
return
|
||||
}
|
||||
if fncall.undoInjection != nil {
|
||||
// setTargetExecuted is set if evalop.CallInjectionSetTarget has been
|
||||
if fncall.undoInjection.doComplete2 {
|
||||
// doComplete2 is set if CallInjectionComplete{DoPinning: true} has been
|
||||
// executed but CallInjectionComplete2 hasn't.
|
||||
regs, err := curthread.Registers()
|
||||
if err == nil {
|
||||
callInjectionComplete2(scope, scope.BinInfo, fncall, regs, curthread)
|
||||
}
|
||||
} else {
|
||||
// undoInjection is set if evalop.CallInjectionSetTarget has been
|
||||
// executed but evalop.CallInjectionComplete hasn't, we must undo the callOP
|
||||
// call in evalop.CallInjectionSetTarget before continuing.
|
||||
switch scope.BinInfo.Arch.Name {
|
||||
@ -980,6 +1001,7 @@ func (stack *evalStack) run() {
|
||||
panic("not implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
stack.lastRetiredFncall = fncall
|
||||
// Resume target to undo one call
|
||||
stack.callInjectionContinue = true
|
||||
@ -1137,6 +1159,11 @@ func (stack *evalStack) executeOp() {
|
||||
case *evalop.Pop:
|
||||
stack.pop()
|
||||
|
||||
case *evalop.Roll:
|
||||
rolled := stack.stack[len(stack.stack)-op.N-1]
|
||||
copy(stack.stack[len(stack.stack)-op.N-1:], stack.stack[len(stack.stack)-op.N:])
|
||||
stack.stack[len(stack.stack)-1] = rolled
|
||||
|
||||
case *evalop.BuiltinCall:
|
||||
vars := make([]*Variable, len(op.Args))
|
||||
for i := len(op.Args) - 1; i >= 0; i-- {
|
||||
@ -1159,9 +1186,29 @@ func (stack *evalStack) executeOp() {
|
||||
stack.err = funcCallCopyOneArg(scope, fncall, actualArg, &fncall.formalArgs[op.ArgNum], curthread)
|
||||
|
||||
case *evalop.CallInjectionComplete:
|
||||
stack.fncallPeek().undoInjection = nil
|
||||
fncall := stack.fncallPeek()
|
||||
fncall.doPinning = op.DoPinning
|
||||
if op.DoPinning {
|
||||
fncall.undoInjection.doComplete2 = true
|
||||
} else {
|
||||
fncall.undoInjection = nil
|
||||
}
|
||||
stack.callInjectionContinue = true
|
||||
|
||||
case *evalop.CallInjectionComplete2:
|
||||
fncall := stack.fncallPeek()
|
||||
if len(fncall.addrsToPin) != 0 {
|
||||
stack.err = fmt.Errorf("internal debugger error: CallInjectionComplete2 called when there still are addresses to pin")
|
||||
}
|
||||
fncall.undoInjection = nil
|
||||
regs, err := curthread.Registers()
|
||||
if err == nil {
|
||||
callInjectionComplete2(scope, scope.BinInfo, fncall, regs, curthread)
|
||||
stack.callInjectionContinue = true
|
||||
} else {
|
||||
stack.err = err
|
||||
}
|
||||
|
||||
case *evalop.CallInjectionStartSpecial:
|
||||
stack.callInjectionContinue = scope.callInjectionStartSpecial(stack, op, curthread)
|
||||
|
||||
@ -1173,6 +1220,26 @@ func (stack *evalStack) executeOp() {
|
||||
rhv := stack.pop()
|
||||
stack.err = scope.setValue(lhv, rhv, exprToString(op.Rhe))
|
||||
|
||||
case *evalop.PushPinAddress:
|
||||
debugPinCount++
|
||||
fncall := stack.fncallPeek()
|
||||
addrToPin := fncall.addrsToPin[len(fncall.addrsToPin)-1]
|
||||
fncall.addrsToPin = fncall.addrsToPin[:len(fncall.addrsToPin)-1]
|
||||
typ, err := scope.BinInfo.findType("unsafe.Pointer")
|
||||
if ptyp, ok := typ.(*godwarf.PtrType); err == nil && ok {
|
||||
v := newVariable("", 0, typ, scope.BinInfo, scope.Mem)
|
||||
v.Children = []Variable{*(newVariable("", uint64(addrToPin), ptyp.Type, scope.BinInfo, scope.Mem))}
|
||||
stack.push(v)
|
||||
} else {
|
||||
stack.err = fmt.Errorf("can not pin address: %v", err)
|
||||
}
|
||||
|
||||
case *evalop.SetDebugPinner:
|
||||
stack.debugPinner = stack.pop()
|
||||
|
||||
case *evalop.PushDebugPinner:
|
||||
stack.push(stack.debugPinner)
|
||||
|
||||
default:
|
||||
stack.err = fmt.Errorf("internal debugger error: unknown eval opcode: %#v", op)
|
||||
}
|
||||
@ -1273,7 +1340,7 @@ func (stack *evalStack) pushIdent(scope *EvalScope, name string) (found bool) {
|
||||
}
|
||||
|
||||
func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
|
||||
ops, err := evalop.CompileAST(scopeToEvalLookup{scope}, t)
|
||||
ops, err := evalop.CompileAST(scopeToEvalLookup{scope}, t, scope.evalopFlags())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1289,10 +1356,15 @@ func exprToString(t ast.Expr) string {
|
||||
}
|
||||
|
||||
func (scope *EvalScope) evalJump(op *evalop.Jump, stack *evalStack) {
|
||||
x := stack.peek()
|
||||
var x *Variable
|
||||
|
||||
switch op.When {
|
||||
case evalop.JumpIfTrue, evalop.JumpIfFalse, evalop.JumpIfAllocStringChecksFail:
|
||||
x = stack.peek()
|
||||
if op.Pop {
|
||||
stack.pop()
|
||||
}
|
||||
}
|
||||
|
||||
var v bool
|
||||
switch op.When {
|
||||
@ -1308,6 +1380,17 @@ func (scope *EvalScope) evalJump(op *evalop.Jump, stack *evalStack) {
|
||||
return
|
||||
}
|
||||
return
|
||||
case evalop.JumpAlways:
|
||||
stack.opidx = op.Target - 1
|
||||
return
|
||||
case evalop.JumpIfPinningDone:
|
||||
fncall := stack.fncallPeek()
|
||||
if len(fncall.addrsToPin) == 0 {
|
||||
stack.opidx = op.Target - 1
|
||||
}
|
||||
return
|
||||
default:
|
||||
panic("internal error, bad jump condition")
|
||||
}
|
||||
|
||||
if x.Kind != reflect.Bool {
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
|
||||
var (
|
||||
ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
|
||||
DebugPinnerFunctionName = "runtime.debugPinnerV1"
|
||||
)
|
||||
|
||||
type compileCtx struct {
|
||||
@ -26,6 +27,8 @@ type compileCtx struct {
|
||||
ops []Op
|
||||
allowCalls bool
|
||||
curCall int
|
||||
flags Flags
|
||||
firstCall bool
|
||||
}
|
||||
|
||||
type evalLookup interface {
|
||||
@ -33,14 +36,24 @@ type evalLookup interface {
|
||||
HasBuiltin(string) bool
|
||||
}
|
||||
|
||||
// Flags describes flags used to control Compile and CompileAST
|
||||
type Flags uint8
|
||||
|
||||
const (
|
||||
CanSet Flags = 1 << iota // Assignment is allowed
|
||||
HasDebugPinner // runtime.debugPinner is available
|
||||
)
|
||||
|
||||
// CompileAST compiles the expression t into a list of instructions.
|
||||
func CompileAST(lookup evalLookup, t ast.Expr) ([]Op, error) {
|
||||
ctx := &compileCtx{evalLookup: lookup, allowCalls: true}
|
||||
func CompileAST(lookup evalLookup, t ast.Expr, flags Flags) ([]Op, error) {
|
||||
ctx := &compileCtx{evalLookup: lookup, allowCalls: true, flags: flags, firstCall: true}
|
||||
err := ctx.compileAST(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.compileDebugUnpin()
|
||||
|
||||
err = ctx.depthCheck(1)
|
||||
if err != nil {
|
||||
return ctx.ops, err
|
||||
@ -50,18 +63,18 @@ func CompileAST(lookup evalLookup, t ast.Expr) ([]Op, error) {
|
||||
|
||||
// Compile compiles the expression expr into a list of instructions.
|
||||
// If canSet is true expressions like "x = y" are also accepted.
|
||||
func Compile(lookup evalLookup, expr string, canSet bool) ([]Op, error) {
|
||||
func Compile(lookup evalLookup, expr string, flags Flags) ([]Op, error) {
|
||||
t, err := parser.ParseExpr(expr)
|
||||
if err != nil {
|
||||
if canSet {
|
||||
if flags&CanSet != 0 {
|
||||
eqOff, isAs := isAssignment(err)
|
||||
if isAs {
|
||||
return CompileSet(lookup, expr[:eqOff], expr[eqOff+1:])
|
||||
return CompileSet(lookup, expr[:eqOff], expr[eqOff+1:], flags)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return CompileAST(lookup, t)
|
||||
return CompileAST(lookup, t, flags)
|
||||
}
|
||||
|
||||
func isAssignment(err error) (int, bool) {
|
||||
@ -74,7 +87,7 @@ func isAssignment(err error) (int, bool) {
|
||||
|
||||
// CompileSet compiles the expression setting lhexpr to rhexpr into a list of
|
||||
// instructions.
|
||||
func CompileSet(lookup evalLookup, lhexpr, rhexpr string) ([]Op, error) {
|
||||
func CompileSet(lookup evalLookup, lhexpr, rhexpr string, flags Flags) ([]Op, error) {
|
||||
lhe, err := parser.ParseExpr(lhexpr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -84,7 +97,7 @@ func CompileSet(lookup evalLookup, lhexpr, rhexpr string) ([]Op, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := &compileCtx{evalLookup: lookup, allowCalls: true}
|
||||
ctx := &compileCtx{evalLookup: lookup, allowCalls: true, flags: flags, firstCall: true}
|
||||
err = ctx.compileAST(rhe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -120,13 +133,17 @@ func (ctx *compileCtx) compileAllocLiteralString() {
|
||||
&PushLen{},
|
||||
&PushNil{},
|
||||
&PushConst{constant.MakeBool(false)},
|
||||
})
|
||||
}, true)
|
||||
|
||||
ctx.pushOp(&ConvertAllocToString{})
|
||||
jmp.Target = len(ctx.ops)
|
||||
}
|
||||
|
||||
func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args []Op) {
|
||||
func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args []Op, doPinning bool) {
|
||||
if doPinning {
|
||||
ctx.compileGetDebugPinner()
|
||||
}
|
||||
|
||||
id := ctx.curCall
|
||||
ctx.curCall++
|
||||
ctx.pushOp(&CallInjectionStartSpecial{
|
||||
@ -136,11 +153,40 @@ func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args
|
||||
ctx.pushOp(&CallInjectionSetTarget{id: id})
|
||||
|
||||
for i := range args {
|
||||
if args[i] != nil {
|
||||
ctx.pushOp(args[i])
|
||||
}
|
||||
ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i})
|
||||
}
|
||||
|
||||
ctx.pushOp(&CallInjectionComplete{id: id})
|
||||
doPinning = doPinning && (ctx.flags&HasDebugPinner != 0)
|
||||
|
||||
ctx.pushOp(&CallInjectionComplete{id: id, DoPinning: doPinning})
|
||||
|
||||
if doPinning {
|
||||
ctx.compilePinningLoop(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *compileCtx) compileGetDebugPinner() {
|
||||
if ctx.firstCall && ctx.flags&HasDebugPinner != 0 {
|
||||
ctx.compileSpecialCall(DebugPinnerFunctionName, []ast.Expr{}, []Op{}, false)
|
||||
ctx.pushOp(&SetDebugPinner{})
|
||||
ctx.firstCall = false
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *compileCtx) compileDebugUnpin() {
|
||||
if !ctx.firstCall && ctx.flags&HasDebugPinner != 0 {
|
||||
ctx.compileSpecialCall("runtime.(*Pinner).Unpin", []ast.Expr{
|
||||
&ast.Ident{Name: "debugPinner"},
|
||||
}, []Op{
|
||||
&PushDebugPinner{},
|
||||
}, false)
|
||||
ctx.pushOp(&Pop{})
|
||||
ctx.pushOp(&PushNil{})
|
||||
ctx.pushOp(&SetDebugPinner{})
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *compileCtx) pushOp(op Op) {
|
||||
@ -172,6 +218,8 @@ func (ctx *compileCtx) depthCheck(endDepth int) error {
|
||||
}
|
||||
}
|
||||
|
||||
debugPinnerSeen := false
|
||||
|
||||
for i, op := range ctx.ops {
|
||||
npop, npush := op.depthCheck()
|
||||
if depth[i] < npop {
|
||||
@ -179,8 +227,15 @@ func (ctx *compileCtx) depthCheck(endDepth int) error {
|
||||
}
|
||||
d := depth[i] - npop + npush
|
||||
checkAndSet(i+1, d)
|
||||
if jmp, _ := op.(*Jump); jmp != nil {
|
||||
checkAndSet(jmp.Target, d)
|
||||
switch op := op.(type) {
|
||||
case *Jump:
|
||||
checkAndSet(op.Target, d)
|
||||
case *CallInjectionStartSpecial:
|
||||
debugPinnerSeen = true
|
||||
case *CallInjectionComplete:
|
||||
if op.DoPinning && !debugPinnerSeen {
|
||||
err = fmt.Errorf("internal debugger error: pinning call injection seen before call to %s at instrution %d", DebugPinnerFunctionName, i)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@ -521,6 +576,16 @@ func (ctx *compileCtx) compileFunctionCall(node *ast.CallExpr) error {
|
||||
id := ctx.curCall
|
||||
ctx.curCall++
|
||||
|
||||
if ctx.flags&HasDebugPinner != 0 {
|
||||
return ctx.compileFunctionCallWithPinning(node, id)
|
||||
}
|
||||
|
||||
return ctx.compileFunctionCallNoPinning(node, id)
|
||||
}
|
||||
|
||||
// compileFunctionCallNoPinning compiles a function call when runtime.debugPinner is
|
||||
// not available in the target.
|
||||
func (ctx *compileCtx) compileFunctionCallNoPinning(node *ast.CallExpr, id int) error {
|
||||
oldAllowCalls := ctx.allowCalls
|
||||
oldOps := ctx.ops
|
||||
ctx.allowCalls = false
|
||||
@ -570,6 +635,61 @@ func (ctx *compileCtx) compileFunctionCall(node *ast.CallExpr) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// compileFunctionCallWithPinning compiles a function call when runtime.debugPinner
|
||||
// is available in the target.
|
||||
func (ctx *compileCtx) compileFunctionCallWithPinning(node *ast.CallExpr, id int) error {
|
||||
ctx.compileGetDebugPinner()
|
||||
|
||||
err := ctx.compileAST(node.Fun)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, arg := range node.Args {
|
||||
err := ctx.compileAST(arg)
|
||||
if isStringLiteral(arg) {
|
||||
ctx.compileAllocLiteralString()
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error evaluating %q as argument %d in function %s: %v", exprToString(arg), i+1, exprToString(node.Fun), err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.pushOp(&Roll{len(node.Args)})
|
||||
ctx.pushOp(&CallInjectionStart{HasFunc: true, id: id, Node: node})
|
||||
ctx.pushOp(&Pop{})
|
||||
ctx.pushOp(&CallInjectionSetTarget{id: id})
|
||||
|
||||
for i := len(node.Args) - 1; i >= 0; i-- {
|
||||
arg := node.Args[i]
|
||||
ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i, ArgExpr: arg})
|
||||
}
|
||||
|
||||
ctx.pushOp(&CallInjectionComplete{id: id, DoPinning: true})
|
||||
|
||||
ctx.compilePinningLoop(id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctx *compileCtx) compilePinningLoop(id int) {
|
||||
loopStart := len(ctx.ops)
|
||||
jmp := &Jump{When: JumpIfPinningDone}
|
||||
ctx.pushOp(jmp)
|
||||
ctx.pushOp(&PushPinAddress{})
|
||||
ctx.compileSpecialCall("runtime.(*Pinner).Pin", []ast.Expr{
|
||||
&ast.Ident{Name: "debugPinner"},
|
||||
&ast.Ident{Name: "pinAddress"},
|
||||
}, []Op{
|
||||
&PushDebugPinner{},
|
||||
nil,
|
||||
}, false)
|
||||
ctx.pushOp(&Pop{})
|
||||
ctx.pushOp(&Jump{When: JumpAlways, Target: loopStart})
|
||||
jmp.Target = len(ctx.ops)
|
||||
ctx.pushOp(&CallInjectionComplete2{id: id})
|
||||
}
|
||||
|
||||
func Listing(depth []int, ops []Op) string {
|
||||
if depth == nil {
|
||||
depth = make([]int, len(ops)+1)
|
||||
|
@ -164,8 +164,13 @@ type Jump struct {
|
||||
}
|
||||
|
||||
func (jmpif *Jump) depthCheck() (npop, npush int) {
|
||||
switch jmpif.When {
|
||||
case JumpIfTrue, JumpIfFalse, JumpIfAllocStringChecksFail:
|
||||
if jmpif.Pop {
|
||||
return 1, 0
|
||||
} else {
|
||||
return 1, 1
|
||||
}
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
@ -177,6 +182,8 @@ const (
|
||||
JumpIfFalse JumpCond = iota
|
||||
JumpIfTrue
|
||||
JumpIfAllocStringChecksFail
|
||||
JumpAlways
|
||||
JumpIfPinningDone
|
||||
)
|
||||
|
||||
// Binary pops two variables from the stack, applies the specified binary
|
||||
@ -200,6 +207,13 @@ type Pop struct {
|
||||
|
||||
func (*Pop) depthCheck() (npop, npush int) { return 1, 0 }
|
||||
|
||||
// Roll removes the n-th element of the stack and pushes it back in at the top
|
||||
type Roll struct {
|
||||
N int
|
||||
}
|
||||
|
||||
func (*Roll) depthCheck() (npop, npush int) { return 1, 1 }
|
||||
|
||||
// BuiltinCall pops len(Args) argument from the stack, calls the specified
|
||||
// builtin on them and pushes the result back on the stack.
|
||||
type BuiltinCall struct {
|
||||
@ -240,11 +254,30 @@ type CallInjectionCopyArg struct {
|
||||
func (*CallInjectionCopyArg) depthCheck() (npop, npush int) { return 1, 0 }
|
||||
|
||||
// CallInjectionComplete resumes target execution so that the injected call can run.
|
||||
// If DoPinning is true it stops after the call is completed without undoing
|
||||
// the call injection frames so that address pinning for the return value
|
||||
// can be performed, see CallInjectionComplete2.
|
||||
type CallInjectionComplete struct {
|
||||
id int
|
||||
DoPinning bool
|
||||
}
|
||||
|
||||
func (op *CallInjectionComplete) depthCheck() (npop, npush int) {
|
||||
if op.DoPinning {
|
||||
return 0, 0
|
||||
} else {
|
||||
return 0, 1
|
||||
}
|
||||
}
|
||||
|
||||
// CallInjectionComplete2 if DoPinning was passed to CallInjectionComplete
|
||||
// this will finish the call injection protocol and push the evaluation
|
||||
// result on the stack.
|
||||
type CallInjectionComplete2 struct {
|
||||
id int
|
||||
}
|
||||
|
||||
func (*CallInjectionComplete) depthCheck() (npop, npush int) { return 0, 1 }
|
||||
func (*CallInjectionComplete2) depthCheck() (npop, npush int) { return 0, 1 }
|
||||
|
||||
// CallInjectionStartSpecial starts call injection for a function with a
|
||||
// name and arguments known at compile time.
|
||||
@ -272,3 +305,22 @@ type SetValue struct {
|
||||
}
|
||||
|
||||
func (*SetValue) depthCheck() (npop, npush int) { return 2, 0 }
|
||||
|
||||
// SetDebugPinner pops one variable from the stack and uses it as the saved debug pinner.
|
||||
type SetDebugPinner struct {
|
||||
}
|
||||
|
||||
func (*SetDebugPinner) depthCheck() (npop, npush int) { return 1, 0 }
|
||||
|
||||
// PushDebugPinner pushes the debug pinner on the stack.
|
||||
type PushDebugPinner struct {
|
||||
}
|
||||
|
||||
func (*PushDebugPinner) depthCheck() (npop, npush int) { return 0, 1 }
|
||||
|
||||
// PushPinAddress pushes an address to pin on the stack (as an
|
||||
// unsafe.Pointer) and removes it from the list of addresses to pin.
|
||||
type PushPinAddress struct {
|
||||
}
|
||||
|
||||
func (*PushPinAddress) depthCheck() (npop, npush int) { return 0, 1 }
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -42,6 +43,9 @@ import (
|
||||
// - 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"
|
||||
@ -89,12 +93,20 @@ type functionCallState struct {
|
||||
// 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 {
|
||||
@ -123,10 +135,14 @@ type callInjection struct {
|
||||
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() {
|
||||
@ -172,7 +188,7 @@ func EvalExpressionWithCalls(grp *TargetGroup, g *G, expr string, retLoadCfg Loa
|
||||
return err
|
||||
}
|
||||
|
||||
ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, true)
|
||||
ops, err := evalop.Compile(scopeToEvalLookup{scope}, expr, scope.evalopFlags()|evalop.CanSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -186,7 +202,7 @@ func EvalExpressionWithCalls(grp *TargetGroup, g *G, expr string, retLoadCfg Loa
|
||||
}
|
||||
|
||||
stack.eval(scope, ops)
|
||||
if stack.callInjectionContinue {
|
||||
if stack.callInjectionContinue && stack.err == nil {
|
||||
return grp.Continue()
|
||||
}
|
||||
|
||||
@ -289,11 +305,19 @@ func (scope *EvalScope) evalCallInjectionStart(op *evalop.CallInjectionStart, st
|
||||
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 {
|
||||
@ -365,10 +389,18 @@ func (scope *EvalScope) evalCallInjectionStart(op *evalop.CallInjectionStart, st
|
||||
p.fncallForG[scope.g.ID].startThreadID = thread.ThreadID()
|
||||
|
||||
stack.fncallPush(&fncall)
|
||||
stack.push(newConstant(constant.MakeBool(fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0), scope.Mem))
|
||||
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 {
|
||||
@ -697,7 +729,7 @@ func allPointers(v *Variable, name string, f func(addr uint64, name string) erro
|
||||
return fmt.Errorf("escape check for %s failed, variable unreadable: %v", name, v.Unreadable)
|
||||
}
|
||||
switch v.Kind {
|
||||
case reflect.Ptr:
|
||||
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
|
||||
@ -713,6 +745,12 @@ func allPointers(v *Variable, name string, f func(addr uint64, name string) erro
|
||||
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 {
|
||||
@ -732,6 +770,10 @@ func allPointers(v *Variable, name string, f func(addr uint64, name string) erro
|
||||
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
|
||||
@ -864,29 +906,32 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
|
||||
return (v.Flags & VariableReturnArgument) != 0
|
||||
})
|
||||
|
||||
if !fncall.doPinning {
|
||||
loadValues(fncall.retvars, callScope.callCtx.retLoadCfg)
|
||||
}
|
||||
for _, v := range fncall.retvars {
|
||||
v.Flags |= VariableFakeAddress
|
||||
}
|
||||
|
||||
// Store the stack span of the currently running goroutine (which in Go >=
|
||||
// 1.15 might be different from the original injection goroutine) so that
|
||||
// later on we can use it to perform the escapeCheck
|
||||
if threadg, _ := GetG(thread); threadg != nil {
|
||||
callScope.callCtx.stacks = append(callScope.callCtx.stacks, threadg.stack)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
break
|
||||
}
|
||||
if err = setLR(thread, oldlr); err != nil {
|
||||
fncall.err = fmt.Errorf("could not restore LR: %v", err)
|
||||
break
|
||||
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
|
||||
@ -913,9 +958,29 @@ func funcCallStep(callScope *EvalScope, stack *evalStack, thread Thread) bool {
|
||||
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.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0 {
|
||||
if !fncall.hasDebugPinner && (fncall.fn == nil || fncall.receiver != nil || fncall.closureAddr != 0) {
|
||||
funcCallEvalFuncExpr(scope, stack, fncall)
|
||||
}
|
||||
stack.pop() // target function, consumed by funcCallEvalFuncExpr either above or in evalop.CallInjectionStart
|
||||
@ -944,7 +1009,7 @@ func (scope *EvalScope) evalCallInjectionSetTarget(op *evalop.CallInjectionSetTa
|
||||
if fncall.receiver != nil {
|
||||
err := funcCallCopyOneArg(scope, fncall, fncall.receiver, &fncall.formalArgs[0], thread)
|
||||
if err != nil {
|
||||
stack.err = err
|
||||
stack.err = fmt.Errorf("could not set call receiver: %v", err)
|
||||
return
|
||||
}
|
||||
fncall.formalArgs = fncall.formalArgs[1:]
|
||||
@ -995,6 +1060,9 @@ func fakeFunctionEntryScope(scope *EvalScope, fn *Function, cfa int64, sp uint64
|
||||
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
|
||||
}
|
||||
@ -1013,6 +1081,9 @@ func (scope *EvalScope) callInjectionStartSpecial(stack *evalStack, op *evalop.C
|
||||
func (scope *EvalScope) convertAllocToString(stack *evalStack) {
|
||||
mallocv := stack.pop()
|
||||
v := stack.pop()
|
||||
|
||||
mallocv.loadValue(loadFullValue)
|
||||
|
||||
if mallocv.Unreadable != nil {
|
||||
stack.err = mallocv.Unreadable
|
||||
return
|
||||
@ -1058,7 +1129,7 @@ func isCallInjectionStop(t *Target, thread Thread, loc *Location) bool {
|
||||
// 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, threads []Thread) (done bool, err error) {
|
||||
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
|
||||
@ -1072,6 +1143,9 @@ func callInjectionProtocol(t *Target, threads []Thread) (done bool, err error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if (thread.ThreadID() != trapthread.ThreadID()) && !thread.SoftExc() {
|
||||
continue
|
||||
}
|
||||
if !isCallInjectionStop(t, thread, loc) {
|
||||
continue
|
||||
}
|
||||
@ -1181,6 +1255,9 @@ func debugCallProtocolReg(archName string, version int) (uint64, bool) {
|
||||
// (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
|
||||
|
@ -43,14 +43,25 @@ func (m *memCache) contains(addr uint64, size int) bool {
|
||||
return addr >= m.cacheAddr && end <= m.cacheAddr+uint64(len(m.cache))
|
||||
}
|
||||
|
||||
func (m *memCache) load() error {
|
||||
if m.loaded {
|
||||
return nil
|
||||
}
|
||||
_, err := m.mem.ReadMemory(m.cache, m.cacheAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.loaded = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memCache) ReadMemory(data []byte, addr uint64) (n int, err error) {
|
||||
if m.contains(addr, len(data)) {
|
||||
if !m.loaded {
|
||||
_, err := m.mem.ReadMemory(m.cache, m.cacheAddr)
|
||||
err := m.load()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
m.loaded = true
|
||||
}
|
||||
copy(data, m.cache[addr-m.cacheAddr:])
|
||||
return len(data), nil
|
||||
|
@ -159,7 +159,7 @@ func (grp *TargetGroup) Continue() error {
|
||||
log.Debugf("\t%d PC=%#x", th.ThreadID(), regs.PC())
|
||||
}
|
||||
}
|
||||
callInjectionDoneThis, callErrThis := callInjectionProtocol(dbp, threads)
|
||||
callInjectionDoneThis, callErrThis := callInjectionProtocol(dbp, trapthread, threads)
|
||||
callInjectionDone = callInjectionDone || callInjectionDoneThis
|
||||
if callInjectionDoneThis {
|
||||
dbp.StopReason = StopCallReturned
|
||||
|
@ -206,7 +206,10 @@ func dwarfToRuntimeType(bi *BinaryInfo, mem MemoryReadWriter, typ godwarf.Type)
|
||||
if kindv == nil || kindv.Unreadable != nil || kindv.Kind != reflect.Uint {
|
||||
kindv = _type.loadFieldNamed("Kind_")
|
||||
}
|
||||
if kindv == nil || kindv.Unreadable != nil || kindv.Kind != reflect.Uint {
|
||||
if kindv == nil {
|
||||
return 0, 0, false, fmt.Errorf("unreadable interace type (no kind field)")
|
||||
}
|
||||
if kindv.Unreadable != nil || kindv.Kind != reflect.Uint {
|
||||
return 0, 0, false, fmt.Errorf("unreadable interface type: %v", kindv.Unreadable)
|
||||
}
|
||||
typeKind, _ = constant.Uint64Val(kindv.Value)
|
||||
|
@ -89,6 +89,8 @@ const (
|
||||
// variableTrustLen means that when this variable is loaded its length
|
||||
// should be trusted and used instead of MaxArrayValues
|
||||
variableTrustLen
|
||||
|
||||
variableSaved
|
||||
)
|
||||
|
||||
// Variable represents a variable. It contains the address, name,
|
||||
@ -1235,7 +1237,7 @@ func (v *Variable) maybeDereference() *Variable {
|
||||
|
||||
switch t := v.RealType.(type) {
|
||||
case *godwarf.PtrType:
|
||||
if v.Addr == 0 && len(v.Children) == 1 && v.loaded {
|
||||
if (v.Addr == 0 || v.Flags&VariableFakeAddress != 0) && len(v.Children) == 1 && v.loaded {
|
||||
// fake pointer variable constructed by casting an integer to a pointer type
|
||||
return &v.Children[0]
|
||||
}
|
||||
@ -1469,7 +1471,7 @@ func convertToEface(srcv, dstv *Variable) error {
|
||||
}
|
||||
typeAddr, typeKind, runtimeTypeFound, err := dwarfToRuntimeType(srcv.bi, srcv.mem, srcv.RealType)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("can not convert value of type %s to %s: %v", srcv.DwarfType.String(), dstv.DwarfType.String(), err)
|
||||
}
|
||||
if !runtimeTypeFound || typeKind&kindDirectIface == 0 {
|
||||
return &typeConvErr{srcv.DwarfType, dstv.RealType}
|
||||
|
@ -912,14 +912,14 @@ func getEvalExpressionTestCases() []varTest {
|
||||
{"bytearray[0] * bytearray[0]", false, "144", "144", "uint8", nil},
|
||||
|
||||
// function call / typecast errors
|
||||
{"unknownthing(1, 2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")},
|
||||
{"(unknownthing)(1, 2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")},
|
||||
{"unknownthing(1, 2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")},
|
||||
{"(unknownthing)(1, 2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")},
|
||||
{"afunc(2)", false, "", "", "", errors.New("function calls not allowed without using 'call'")},
|
||||
{"(afunc)(2)", false, "", "", "", errors.New("function calls not allowed without using 'call'")},
|
||||
{"(*afunc)(2)", false, "", "", "", errors.New("expression \"afunc\" (func()) can not be dereferenced")},
|
||||
{"unknownthing(2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")},
|
||||
{"(*unknownthing)(2)", false, "", "", "", errors.New("could not find symbol value for unknownthing")},
|
||||
{"(*strings.Split)(2)", false, "", "", "", errors.New("could not find symbol value for strings")},
|
||||
{"(*afunc)(2)", false, "", "", "", errors.New("*")},
|
||||
{"unknownthing(2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")},
|
||||
{"(*unknownthing)(2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for unknownthing")},
|
||||
{"(*strings.Split)(2)", false, "", "", "", altErrors("function calls not allowed without using 'call'", "could not find symbol value for strings")},
|
||||
|
||||
// pretty printing special types
|
||||
{"tim1", false, `time.Time(1977-05-25T18:00:00Z)…`, `time.Time(1977-05-25T18:00:00Z)…`, "time.Time", nil},
|
||||
@ -977,6 +977,18 @@ func getEvalExpressionTestCases() []varTest {
|
||||
return testcases
|
||||
}
|
||||
|
||||
func altErrors(errs ...string) *altError {
|
||||
return &altError{errs}
|
||||
}
|
||||
|
||||
type altError struct {
|
||||
errs []string
|
||||
}
|
||||
|
||||
func (err *altError) Error() string {
|
||||
return "[multiple alternatives]"
|
||||
}
|
||||
|
||||
func TestEvalExpression(t *testing.T) {
|
||||
testcases := getEvalExpressionTestCases()
|
||||
protest.AllowRecording(t)
|
||||
@ -1000,9 +1012,23 @@ func TestEvalExpression(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error %s, got no error (%s)", tc.err.Error(), tc.name)
|
||||
}
|
||||
if tc.err.Error() != err.Error() {
|
||||
switch e := tc.err.(type) {
|
||||
case *altError:
|
||||
ok := false
|
||||
for _, tgtErr := range e.errs {
|
||||
if tgtErr == err.Error() {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error())
|
||||
}
|
||||
default:
|
||||
if tc.err.Error() != "*" && tc.err.Error() != err.Error() {
|
||||
t.Fatalf("Unexpected error. Expected %s got %s", tc.err.Error(), err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -1275,6 +1301,7 @@ type testCaseCallFunction struct {
|
||||
expr string // call expression to evaluate
|
||||
outs []string // list of return parameters in this format: <param name>:<param type>:<param value>
|
||||
err error // if not nil should return an error
|
||||
pinCount int // where debugPinner is supported this is the number of pins created during the function call injection
|
||||
}
|
||||
|
||||
func TestCallFunction(t *testing.T) {
|
||||
@ -1286,126 +1313,126 @@ func TestCallFunction(t *testing.T) {
|
||||
var testcases = []testCaseCallFunction{
|
||||
// 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},
|
||||
{`stringsJoin(nil, "")`, []string{`:string:""`}, nil},
|
||||
{`stringsJoin(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil},
|
||||
{`stringsJoin(stringslice, "~~")`, []string{`:string:"one~~two~~three"`}, nil},
|
||||
{`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")},
|
||||
{`noreturncall(2)`, nil, nil},
|
||||
{"call1(one, two)", []string{":int:3"}, nil, 0},
|
||||
{"call1(one+two, 4)", []string{":int:7"}, nil, 0},
|
||||
{"callpanic()", []string{`~panic:interface {}:interface {}(string) "callpanic panicked"`}, nil, 0},
|
||||
{`stringsJoin(nil, "")`, []string{`:string:""`}, nil, 0},
|
||||
{`stringsJoin(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil, 1},
|
||||
{`stringsJoin(stringslice, "~~")`, []string{`:string:"one~~two~~three"`}, nil, 2},
|
||||
{`stringsJoin(s1, comma)`, nil, errors.New(`could not find symbol value for s1`), 1},
|
||||
{`stringsJoin(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string"), 1},
|
||||
{`noreturncall(2)`, nil, nil, 0},
|
||||
|
||||
// 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},
|
||||
{`square(2) + 1`, []string{":int:5"}, nil, 0},
|
||||
{`intcallpanic(1) + 1`, []string{":int:2"}, nil, 0},
|
||||
{`intcallpanic(0) + 1`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil, 0},
|
||||
{`onetwothree(5)[1] + 2`, []string{":int:9"}, nil, 1},
|
||||
|
||||
// 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
|
||||
{`a.VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil, 1}, // direct call of a method with value receiver / on a value
|
||||
|
||||
{`a.PRcvr(2)`, []string{`:string:"2 - 3 = -1"`}, nil}, // direct call of a method with pointer receiver / on a value
|
||||
{`pa.VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil}, // direct call of a method with value receiver / on a pointer
|
||||
{`pa.PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil}, // direct call of a method with pointer receiver / on a pointer
|
||||
{`a.PRcvr(2)`, []string{`:string:"2 - 3 = -1"`}, nil, 1}, // direct call of a method with pointer receiver / on a value
|
||||
{`pa.VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil, 1}, // direct call of a method with value receiver / on a pointer
|
||||
{`pa.PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil, 1}, // direct call of a method with pointer receiver / on a pointer
|
||||
|
||||
{`vable_pa.VRcvr(6)`, []string{`:string:"6 + 6 = 12"`}, nil}, // indirect call of method on interface / containing value with value method
|
||||
{`pable_pa.PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil}, // indirect call of method on interface / containing pointer with value method
|
||||
{`vable_a.VRcvr(5)`, []string{`:string:"5 + 3 = 8"`}, nil}, // indirect call of method on interface / containing pointer with pointer method
|
||||
{`vable_pa.VRcvr(6)`, []string{`:string:"6 + 6 = 12"`}, nil, 1}, // indirect call of method on interface / containing value with value method
|
||||
{`pable_pa.PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil, 1}, // indirect call of method on interface / containing pointer with value method
|
||||
{`vable_a.VRcvr(5)`, []string{`:string:"5 + 3 = 8"`}, nil, 1}, // indirect call of method on interface / containing pointer with pointer method
|
||||
|
||||
{`pa.nonexistent()`, nil, errors.New("pa has no member nonexistent")},
|
||||
{`a.nonexistent()`, nil, errors.New("a has no member nonexistent")},
|
||||
{`vable_pa.nonexistent()`, nil, errors.New("vable_pa has no member nonexistent")},
|
||||
{`vable_a.nonexistent()`, nil, errors.New("vable_a has no member nonexistent")},
|
||||
{`pable_pa.nonexistent()`, nil, errors.New("pable_pa has no member nonexistent")},
|
||||
{`pa.nonexistent()`, nil, errors.New("pa has no member nonexistent"), 0},
|
||||
{`a.nonexistent()`, nil, errors.New("a has no member nonexistent"), 0},
|
||||
{`vable_pa.nonexistent()`, nil, errors.New("vable_pa has no member nonexistent"), 0},
|
||||
{`vable_a.nonexistent()`, nil, errors.New("vable_a has no member nonexistent"), 0},
|
||||
{`pable_pa.nonexistent()`, nil, errors.New("pable_pa has no member nonexistent"), 0},
|
||||
|
||||
{`fn2glob(10, 20)`, []string{":int:30"}, nil}, // indirect call of func value / set to top-level func
|
||||
{`fn2clos(11)`, []string{`:string:"1 + 6 + 11 = 18"`}, nil}, // indirect call of func value / set to func literal
|
||||
{`fn2clos(12)`, []string{`:string:"2 + 6 + 12 = 20"`}, nil},
|
||||
{`fn2valmeth(13)`, []string{`:string:"13 + 6 = 19"`}, nil}, // indirect call of func value / set to value method
|
||||
{`fn2ptrmeth(14)`, []string{`:string:"14 - 6 = 8"`}, nil}, // indirect call of func value / set to pointer method
|
||||
{`fn2glob(10, 20)`, []string{":int:30"}, nil, 0}, // indirect call of func value / set to top-level func
|
||||
{`fn2clos(11)`, []string{`:string:"1 + 6 + 11 = 18"`}, nil, 1}, // indirect call of func value / set to func literal
|
||||
{`fn2clos(12)`, []string{`:string:"2 + 6 + 12 = 20"`}, nil, 1},
|
||||
{`fn2valmeth(13)`, []string{`:string:"13 + 6 = 19"`}, nil, 1}, // indirect call of func value / set to value method
|
||||
{`fn2ptrmeth(14)`, []string{`:string:"14 - 6 = 8"`}, nil, 1}, // indirect call of func value / set to pointer method
|
||||
|
||||
{"fn2nil()", nil, errors.New("nil pointer dereference")},
|
||||
{"fn2nil()", nil, errors.New("nil pointer dereference"), 0},
|
||||
|
||||
{"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil},
|
||||
{"ga.PRcvr(2)", []string{`:string:"2 - 0 = 2"`}, nil, 1},
|
||||
|
||||
{"x.CallMe()", nil, nil},
|
||||
{"x2.CallMe(5)", []string{":int:25"}, nil},
|
||||
{"x.CallMe()", nil, nil, 0},
|
||||
{"x2.CallMe(5)", []string{":int:25"}, nil, 0},
|
||||
|
||||
{"\"delve\".CallMe()", nil, errors.New("\"delve\" (type string) is not a struct")},
|
||||
{"\"delve\".CallMe()", nil, errors.New("\"delve\" (type string) is not a struct"), 0},
|
||||
|
||||
// Nested function calls tests
|
||||
|
||||
{`onetwothree(intcallpanic(2))`, []string{`:[]int:[]int len: 3, cap: 3, [3,4,5]`}, nil},
|
||||
{`onetwothree(intcallpanic(0))`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil},
|
||||
{`onetwothree(intcallpanic(2)+1)`, []string{`:[]int:[]int len: 3, cap: 3, [4,5,6]`}, nil},
|
||||
{`onetwothree(intcallpanic("not a number"))`, nil, errors.New("can not convert \"not a number\" constant to int")},
|
||||
{`onetwothree(intcallpanic(2))`, []string{`:[]int:[]int len: 3, cap: 3, [3,4,5]`}, nil, 1},
|
||||
{`onetwothree(intcallpanic(0))`, []string{`~panic:interface {}:interface {}(string) "panic requested"`}, nil, 0},
|
||||
{`onetwothree(intcallpanic(2)+1)`, []string{`:[]int:[]int len: 3, cap: 3, [4,5,6]`}, nil, 1},
|
||||
{`onetwothree(intcallpanic("not a number"))`, nil, errors.New("can not convert \"not a number\" constant to int"), 1},
|
||||
|
||||
// Variable setting tests
|
||||
{`pa2 = getAStructPtr(8); pa2`, []string{`pa2:*main.astruct:*main.astruct {X: 8}`}, nil},
|
||||
{`pa2 = getAStructPtr(8); pa2`, []string{`pa2:*main.astruct:*main.astruct {X: 8}`}, nil, 1},
|
||||
|
||||
// Escape tests
|
||||
|
||||
{"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2")},
|
||||
{"escapeArg(&a2)", nil, errors.New("cannot use &a2 as argument pa2 in function main.escapeArg: stack object passed to escaping pointer: pa2"), 0},
|
||||
|
||||
// Issue 1577
|
||||
{"1+2", []string{`::3`}, nil},
|
||||
{`"de"+"mo"`, []string{`::"demo"`}, nil},
|
||||
{"1+2", []string{`::3`}, nil, 0},
|
||||
{`"de"+"mo"`, []string{`::"demo"`}, nil, 0},
|
||||
|
||||
// Issue 3176
|
||||
{`ref.String()[0]`, []string{`:byte:98`}, nil},
|
||||
{`ref.String()[20]`, nil, errors.New("index out of bounds")},
|
||||
{`ref.String()[0]`, []string{`:byte:98`}, nil, 1},
|
||||
{`ref.String()[20]`, nil, errors.New("index out of bounds"), 1},
|
||||
}
|
||||
|
||||
var testcases112 = []testCaseCallFunction{
|
||||
// string allocation requires trusted argument order, which we don't have in Go 1.11
|
||||
{`stringsJoin(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil},
|
||||
{`str = "a new string"; str`, []string{`str:string:"a new string"`}, nil},
|
||||
{`stringsJoin(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil, 2},
|
||||
{`str = "a new string"; str`, []string{`str:string:"a new string"`}, nil, 1},
|
||||
|
||||
// support calling optimized functions
|
||||
{`strings.Join(nil, "")`, []string{`:string:""`}, nil},
|
||||
{`strings.Join(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil},
|
||||
{`strings.Join(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string")},
|
||||
{`strings.Join(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil},
|
||||
{`strings.LastIndexByte(stringslice[1], 'w')`, []string{":int:1"}, nil},
|
||||
{`strings.LastIndexByte(stringslice[1], 'o')`, []string{":int:2"}, nil},
|
||||
{`d.Base.Method()`, []string{`:int:4`}, nil},
|
||||
{`d.Method()`, []string{`:int:4`}, nil},
|
||||
{`strings.Join(nil, "")`, []string{`:string:""`}, nil, 0},
|
||||
{`strings.Join(stringslice, comma)`, []string{`:string:"one,two,three"`}, nil, 1},
|
||||
{`strings.Join(intslice, comma)`, nil, errors.New("can not convert value of type []int to []string"), 1},
|
||||
{`strings.Join(stringslice, ",")`, []string{`:string:"one,two,three"`}, nil, 2},
|
||||
{`strings.LastIndexByte(stringslice[1], 'w')`, []string{":int:1"}, nil, 0},
|
||||
{`strings.LastIndexByte(stringslice[1], 'o')`, []string{":int:2"}, nil, 0},
|
||||
{`d.Base.Method()`, []string{`:int:4`}, nil, 0},
|
||||
{`d.Method()`, []string{`:int:4`}, nil, 0},
|
||||
}
|
||||
|
||||
var testcases113 = []testCaseCallFunction{
|
||||
{`curriedAdd(2)(3)`, []string{`:int:5`}, nil},
|
||||
{`curriedAdd(2)(3)`, []string{`:int:5`}, nil, 1},
|
||||
|
||||
// Method calls on a value returned by a function
|
||||
|
||||
{`getAStruct(3).VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil}, // direct call of a method with value receiver / on a value
|
||||
{`getAStruct(3).VRcvr(1)`, []string{`:string:"1 + 3 = 4"`}, nil, 1}, // direct call of a method with value receiver / on a value
|
||||
|
||||
{`getAStruct(3).PRcvr(2)`, nil, errors.New("cannot use getAStruct(3).PRcvr as argument pa in function main.(*astruct).PRcvr: stack object passed to escaping pointer: pa")}, // direct call of a method with pointer receiver / on a value
|
||||
{`getAStructPtr(6).VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil}, // direct call of a method with value receiver / on a pointer
|
||||
{`getAStructPtr(6).PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil}, // direct call of a method with pointer receiver / on a pointer
|
||||
{`getAStruct(3).PRcvr(2)`, nil, errors.New("could not set call receiver: cannot use getAStruct(3).PRcvr as argument pa in function main.(*astruct).PRcvr: stack object passed to escaping pointer: pa"), 0}, // direct call of a method with pointer receiver / on a value
|
||||
{`getAStructPtr(6).VRcvr(3)`, []string{`:string:"3 + 6 = 9"`}, nil, 2}, // direct call of a method with value receiver / on a pointer
|
||||
{`getAStructPtr(6).PRcvr(4)`, []string{`:string:"4 - 6 = -2"`}, nil, 2}, // direct call of a method with pointer receiver / on a pointer
|
||||
|
||||
{`getVRcvrableFromAStruct(3).VRcvr(6)`, []string{`:string:"6 + 3 = 9"`}, nil}, // indirect call of method on interface / containing value with value method
|
||||
{`getPRcvrableFromAStructPtr(6).PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil}, // indirect call of method on interface / containing pointer with value method
|
||||
{`getVRcvrableFromAStructPtr(6).VRcvr(5)`, []string{`:string:"5 + 6 = 11"`}, nil}, // indirect call of method on interface / containing pointer with pointer method
|
||||
{`getVRcvrableFromAStruct(3).VRcvr(6)`, []string{`:string:"6 + 3 = 9"`}, nil, 3}, // indirect call of method on interface / containing value with value method
|
||||
{`getPRcvrableFromAStructPtr(6).PRcvr(7)`, []string{`:string:"7 - 6 = 1"`}, nil, 3}, // indirect call of method on interface / containing pointer with value method
|
||||
{`getVRcvrableFromAStructPtr(6).VRcvr(5)`, []string{`:string:"5 + 6 = 11"`}, nil, 3}, // indirect call of method on interface / containing pointer with pointer method
|
||||
}
|
||||
|
||||
var testcasesBefore114After112 = []testCaseCallFunction{
|
||||
{`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`)},
|
||||
{`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`), 1},
|
||||
}
|
||||
|
||||
var testcases114 = []testCaseCallFunction{
|
||||
{`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`)},
|
||||
{`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`), 1},
|
||||
}
|
||||
|
||||
var testcases117 = []testCaseCallFunction{
|
||||
{`regabistacktest("one", "two", "three", "four", "five", 4)`, []string{`:string:"onetwo"`, `:string:"twothree"`, `:string:"threefour"`, `:string:"fourfive"`, `:string:"fiveone"`, ":uint8:8"}, nil},
|
||||
{`regabistacktest2(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`, []string{":int:3", ":int:5", ":int:7", ":int:9", ":int:11", ":int:13", ":int:15", ":int:17", ":int:19", ":int:11"}, nil},
|
||||
{`issue2698.String()`, []string{`:string:"1 2 3 4"`}, nil},
|
||||
{`issue3364.String()`, []string{`:string:"1 2"`}, nil},
|
||||
{`regabistacktest3(rast3, 5)`, []string{`:[10]string:[10]string ["onetwo","twothree","threefour","fourfive","fivesix","sixseven","sevenheight","heightnine","nineten","tenone"]`, ":uint8:15"}, nil},
|
||||
{`floatsum(1, 2)`, []string{":float64:3"}, nil},
|
||||
{`regabistacktest("one", "two", "three", "four", "five", 4)`, []string{`:string:"onetwo"`, `:string:"twothree"`, `:string:"threefour"`, `:string:"fourfive"`, `:string:"fiveone"`, ":uint8:8"}, nil, 10},
|
||||
{`regabistacktest2(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`, []string{":int:3", ":int:5", ":int:7", ":int:9", ":int:11", ":int:13", ":int:15", ":int:17", ":int:19", ":int:11"}, nil, 0},
|
||||
{`issue2698.String()`, []string{`:string:"1 2 3 4"`}, nil, 1},
|
||||
{`issue3364.String()`, []string{`:string:"1 2"`}, nil, 1},
|
||||
{`regabistacktest3(rast3, 5)`, []string{`:[10]string:[10]string ["onetwo","twothree","threefour","fourfive","fivesix","sixseven","sevenheight","heightnine","nineten","tenone"]`, ":uint8:15"}, nil, 10},
|
||||
{`floatsum(1, 2)`, []string{":float64:3"}, nil, 0},
|
||||
}
|
||||
|
||||
withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
|
||||
@ -1447,7 +1474,7 @@ func TestCallFunction(t *testing.T) {
|
||||
}
|
||||
|
||||
// LEAVE THIS AS THE LAST ITEM, IT BREAKS THE TARGET PROCESS!!!
|
||||
testCallFunction(t, grp, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil})
|
||||
testCallFunction(t, grp, p, testCaseCallFunction{"-unsafe escapeArg(&a2)", nil, nil, 0})
|
||||
})
|
||||
}
|
||||
|
||||
@ -1463,6 +1490,12 @@ func testCallFunctionSetBreakpoint(t *testing.T, p *proc.Target, grp *proc.Targe
|
||||
}
|
||||
|
||||
func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc testCaseCallFunction) {
|
||||
t.Run(tc.expr, func(t *testing.T) {
|
||||
testCallFunctionIntl(t, grp, p, tc)
|
||||
})
|
||||
}
|
||||
|
||||
func testCallFunctionIntl(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc testCaseCallFunction) {
|
||||
const unsafePrefix = "-unsafe "
|
||||
|
||||
var callExpr, varExpr string
|
||||
@ -1493,8 +1526,12 @@ func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc te
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "internal debugger error") {
|
||||
t.Fatalf("call %q: error %s", tc.expr, err.Error())
|
||||
} else {
|
||||
t.Fatalf("call %q: error %q", tc.expr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
retvalsVar := p.CurrentThread().Common().ReturnValues(pnormalLoadConfig)
|
||||
retvals := make([]*api.Variable, len(retvalsVar))
|
||||
@ -1538,6 +1575,13 @@ func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc te
|
||||
t.Fatalf("call %q, output parameter %d: expected value %q, got %q", tc.expr, i, tgtValue, cvs)
|
||||
}
|
||||
}
|
||||
|
||||
if p.BinInfo().HasDebugPinner() {
|
||||
t.Logf("\t(pins = %d)", proc.DebugPinCount())
|
||||
if proc.DebugPinCount() != tc.pinCount {
|
||||
t.Fatalf("call %q, expected pin count %d, got %d", tc.expr, tc.pinCount, proc.DebugPinCount())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue1531(t *testing.T) {
|
||||
|
Loading…
Reference in New Issue
Block a user