
* proc: use stack machine to evaluate expressions This commit splits expression evaluation into two parts. The first part (in pkg/proc/evalop/evalcompile.go) "compiles" as ast.Expr into a list of instructions (defined in pkg/proc/evalop/ops.go) for a stack machine (defined by `proc.(*evalStack)`). The second part is a stack machine (implemented by `proc.(*EvalScope).eval` and `proc.(*EvalScope).evalOne`) that has two modes of operation: in the main mode it executes inteructions from the list (by calling `evalOne`), in the second mode it executes the call injection protocol by calling `funcCallStep` repeatedly until it either the protocol finishes, needs more input from the stack machine (to set call arguments) or fails. This approach has several benefits: - it is now possible to remove the goroutine we use to evaluate expression and the channel used to communicate with the Continue loop. - every time we resume the target to execute the call injection protocol we need to update several local variables to match the changed state of the target, this is now done at the top level of the evaluation loop instead of being hidden inside a recurisive evaluator - using runtime.Pin to pin addresses returned by an injected call would allow us to use a more natural evaluation order for function calls, which would solve some bugs #3310, allow users to inspect values returned by a call injection #1599 and allow implementing some other features #1465. Doing this with the recursive evaluator, while keeping backwards compatibility with versions of Go that do not have runtime.Pin is very hard. However after this change we can simply conditionally change how compileFunctionCall works and add some opcodes. * review round 1 * review round 2
557 lines
14 KiB
Go
557 lines
14 KiB
Go
package evalop
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/printer"
|
|
"go/token"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-delve/delve/pkg/dwarf/godwarf"
|
|
"github.com/go-delve/delve/pkg/dwarf/reader"
|
|
)
|
|
|
|
var (
|
|
ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
|
|
)
|
|
|
|
type compileCtx struct {
|
|
evalLookup
|
|
ops []Op
|
|
allowCalls bool
|
|
curCall int
|
|
}
|
|
|
|
type evalLookup interface {
|
|
FindTypeExpr(ast.Expr) (godwarf.Type, error)
|
|
HasLocal(string) bool
|
|
HasGlobal(string, string) bool
|
|
HasBuiltin(string) bool
|
|
LookupRegisterName(string) (int, bool)
|
|
}
|
|
|
|
// Compile compiles the expression t into a list of instructions.
|
|
func Compile(lookup evalLookup, t ast.Expr) ([]Op, error) {
|
|
ctx := &compileCtx{evalLookup: lookup, allowCalls: true}
|
|
err := ctx.compileAST(t)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = ctx.depthCheck(1)
|
|
if err != nil {
|
|
return ctx.ops, err
|
|
}
|
|
return ctx.ops, nil
|
|
}
|
|
|
|
// CompileSet compiles the expression setting lhe to rhe into a list of
|
|
// instructions.
|
|
func CompileSet(lookup evalLookup, lhe, rhe ast.Expr) ([]Op, error) {
|
|
ctx := &compileCtx{evalLookup: lookup, allowCalls: true}
|
|
err := ctx.compileAST(rhe)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isStringLiteral(rhe) {
|
|
ctx.compileAllocLiteralString()
|
|
}
|
|
|
|
err = ctx.compileAST(lhe)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx.pushOp(&SetValue{lhe: lhe, Rhe: rhe})
|
|
|
|
err = ctx.depthCheck(0)
|
|
if err != nil {
|
|
return ctx.ops, err
|
|
}
|
|
return ctx.ops, nil
|
|
|
|
}
|
|
|
|
func (ctx *compileCtx) compileAllocLiteralString() {
|
|
ctx.pushOp(&CallInjectionAllocString{Phase: 0})
|
|
ctx.pushOp(&CallInjectionAllocString{Phase: 1})
|
|
ctx.pushOp(&CallInjectionAllocString{Phase: 2})
|
|
}
|
|
|
|
func (ctx *compileCtx) pushOp(op Op) {
|
|
ctx.ops = append(ctx.ops, op)
|
|
}
|
|
|
|
// depthCheck validates the list of instructions produced by Compile and
|
|
// CompileSet by peforming a stack depth check.
|
|
// It calculates the depth of the stack at every instruction in ctx.ops and
|
|
// checks that they have enough arguments to execute. For instructions that
|
|
// can be reached through multiple paths (because of a jump) it checks that
|
|
// all paths reach the instruction with the same stack depth.
|
|
// Finally it checks that the stack depth after all instructions have
|
|
// executed is equal to endDepth.
|
|
func (ctx *compileCtx) depthCheck(endDepth int) error {
|
|
depth := make([]int, len(ctx.ops)+1) // depth[i] is the depth of the stack before i-th instruction
|
|
for i := range depth {
|
|
depth[i] = -1
|
|
}
|
|
depth[0] = 0
|
|
|
|
var err error
|
|
checkAndSet := func(j, d int) { // sets depth[j] to d after checking that we can
|
|
if depth[j] < 0 {
|
|
depth[j] = d
|
|
}
|
|
if d != depth[j] {
|
|
err = fmt.Errorf("internal debugger error: depth check error at instruction %d: expected depth %d have %d (jump target)\n%s", j, d, depth[j], Listing(depth, ctx.ops))
|
|
}
|
|
}
|
|
|
|
for i, op := range ctx.ops {
|
|
npop, npush := op.depthCheck()
|
|
if depth[i] < npop {
|
|
return fmt.Errorf("internal debugger error: depth check error at instruction %d: expected at least %d have %d\n%s", i, npop, depth[i], Listing(depth, ctx.ops))
|
|
}
|
|
d := depth[i] - npop + npush
|
|
checkAndSet(i+1, d)
|
|
if jmp, _ := op.(*Jump); jmp != nil {
|
|
checkAndSet(jmp.Target, d)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if depth[len(ctx.ops)] != endDepth {
|
|
return fmt.Errorf("internal debugger error: depth check failed: depth at the end is not %d (got %d)\n%s", depth[len(ctx.ops)], endDepth, Listing(depth, ctx.ops))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileAST(t ast.Expr) error {
|
|
switch node := t.(type) {
|
|
case *ast.CallExpr:
|
|
return ctx.compileTypeCastOrFuncCall(node)
|
|
|
|
case *ast.Ident:
|
|
return ctx.compileIdent(node)
|
|
|
|
case *ast.ParenExpr:
|
|
// otherwise just eval recursively
|
|
return ctx.compileAST(node.X)
|
|
|
|
case *ast.SelectorExpr: // <expression>.<identifier>
|
|
switch x := node.X.(type) {
|
|
case *ast.Ident:
|
|
switch {
|
|
case x.Name == "runtime" && node.Sel.Name == "curg":
|
|
ctx.pushOp(&PushCurg{})
|
|
|
|
case x.Name == "runtime" && node.Sel.Name == "frameoff":
|
|
ctx.pushOp(&PushFrameoff{})
|
|
|
|
case x.Name == "runtime" && node.Sel.Name == "threadid":
|
|
ctx.pushOp(&PushThreadID{})
|
|
|
|
case ctx.HasLocal(x.Name):
|
|
ctx.pushOp(&PushLocal{x.Name})
|
|
ctx.pushOp(&Select{node.Sel.Name})
|
|
|
|
case ctx.HasGlobal(x.Name, node.Sel.Name):
|
|
ctx.pushOp(&PushPackageVar{x.Name, node.Sel.Name})
|
|
|
|
default:
|
|
return ctx.compileUnary(node.X, &Select{node.Sel.Name})
|
|
}
|
|
|
|
case *ast.BasicLit: // try to accept "package/path".varname syntax for package variables
|
|
s, err := strconv.Unquote(x.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ctx.HasGlobal(s, node.Sel.Name) {
|
|
ctx.pushOp(&PushPackageVar{s, node.Sel.Name})
|
|
return nil
|
|
}
|
|
return ctx.compileUnary(node.X, &Select{node.Sel.Name})
|
|
|
|
default:
|
|
return ctx.compileUnary(node.X, &Select{node.Sel.Name})
|
|
|
|
}
|
|
|
|
case *ast.TypeAssertExpr: // <expression>.(<type>)
|
|
return ctx.compileTypeAssert(node)
|
|
|
|
case *ast.IndexExpr:
|
|
return ctx.compileBinary(node.X, node.Index, nil, &Index{node})
|
|
|
|
case *ast.SliceExpr:
|
|
if node.Slice3 {
|
|
return fmt.Errorf("3-index slice expressions not supported")
|
|
}
|
|
return ctx.compileReslice(node)
|
|
|
|
case *ast.StarExpr:
|
|
// pointer dereferencing *<expression>
|
|
return ctx.compileUnary(node.X, &PointerDeref{node})
|
|
|
|
case *ast.UnaryExpr:
|
|
// The unary operators we support are +, - and & (note that unary * is parsed as ast.StarExpr)
|
|
switch node.Op {
|
|
case token.AND:
|
|
return ctx.compileUnary(node.X, &AddrOf{node})
|
|
default:
|
|
return ctx.compileUnary(node.X, &Unary{node})
|
|
}
|
|
|
|
case *ast.BinaryExpr:
|
|
switch node.Op {
|
|
case token.INC, token.DEC, token.ARROW:
|
|
return fmt.Errorf("operator %s not supported", node.Op.String())
|
|
}
|
|
// short circuits logical operators
|
|
var sop *Jump
|
|
switch node.Op {
|
|
case token.LAND:
|
|
sop = &Jump{When: JumpIfFalse, Node: node.X}
|
|
case token.LOR:
|
|
sop = &Jump{When: JumpIfTrue, Node: node.X}
|
|
}
|
|
err := ctx.compileBinary(node.X, node.Y, sop, &Binary{node})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sop != nil {
|
|
sop.Target = len(ctx.ops)
|
|
ctx.pushOp(&BoolToConst{})
|
|
}
|
|
|
|
case *ast.BasicLit:
|
|
ctx.pushOp(&PushConst{constant.MakeFromLiteral(node.Value, node.Kind, 0)})
|
|
|
|
default:
|
|
return fmt.Errorf("expression %T not implemented", t)
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileTypeCastOrFuncCall(node *ast.CallExpr) error {
|
|
if len(node.Args) != 1 {
|
|
// Things that have more or less than one argument are always function calls.
|
|
return ctx.compileFunctionCall(node)
|
|
}
|
|
|
|
ambiguous := func() error {
|
|
// Ambiguous, could be a function call or a type cast, if node.Fun can be
|
|
// evaluated then try to treat it as a function call, otherwise try the
|
|
// type cast.
|
|
ctx2 := &compileCtx{evalLookup: ctx.evalLookup}
|
|
err0 := ctx2.compileAST(node.Fun)
|
|
if err0 == nil {
|
|
return ctx.compileFunctionCall(node)
|
|
}
|
|
return ctx.compileTypeCast(node, err0)
|
|
}
|
|
|
|
fnnode := node.Fun
|
|
for {
|
|
fnnode = removeParen(fnnode)
|
|
n, _ := fnnode.(*ast.StarExpr)
|
|
if n == nil {
|
|
break
|
|
}
|
|
fnnode = n.X
|
|
}
|
|
|
|
switch n := fnnode.(type) {
|
|
case *ast.BasicLit:
|
|
// It can only be a ("type string")(x) type cast
|
|
return ctx.compileTypeCast(node, nil)
|
|
case *ast.ArrayType, *ast.StructType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.ChanType:
|
|
return ctx.compileTypeCast(node, nil)
|
|
case *ast.SelectorExpr:
|
|
if _, isident := n.X.(*ast.Ident); isident {
|
|
if typ, _ := ctx.FindTypeExpr(n); typ != nil {
|
|
return ctx.compileTypeCast(node, nil)
|
|
}
|
|
return ambiguous()
|
|
}
|
|
return ctx.compileFunctionCall(node)
|
|
case *ast.Ident:
|
|
if ctx.HasBuiltin(n.Name) {
|
|
return ctx.compileFunctionCall(node)
|
|
}
|
|
if ctx.HasGlobal("", n.Name) || ctx.HasLocal(n.Name) {
|
|
return ctx.compileFunctionCall(node)
|
|
}
|
|
return ctx.compileTypeCast(node, fmt.Errorf("could not find symbol value for %s", n.Name))
|
|
case *ast.IndexExpr:
|
|
// Ambiguous, could be a parametric type
|
|
switch n.X.(type) {
|
|
case *ast.Ident, *ast.SelectorExpr:
|
|
// Do the type-cast first since evaluating node.Fun could be expensive.
|
|
err := ctx.compileTypeCast(node, nil)
|
|
if err == nil || err != reader.ErrTypeNotFound {
|
|
return err
|
|
}
|
|
return ctx.compileFunctionCall(node)
|
|
default:
|
|
return ctx.compileFunctionCall(node)
|
|
}
|
|
case *astIndexListExpr:
|
|
return ctx.compileTypeCast(node, nil)
|
|
default:
|
|
// All other expressions must be function calls
|
|
return ctx.compileFunctionCall(node)
|
|
}
|
|
}
|
|
|
|
func (ctx *compileCtx) compileTypeCast(node *ast.CallExpr, ambiguousErr error) error {
|
|
err := ctx.compileAST(node.Args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fnnode := node.Fun
|
|
|
|
// remove all enclosing parenthesis from the type name
|
|
fnnode = removeParen(fnnode)
|
|
|
|
targetTypeStr := exprToString(removeParen(node.Fun))
|
|
styp, err := ctx.FindTypeExpr(fnnode)
|
|
if err != nil {
|
|
switch targetTypeStr {
|
|
case "[]byte", "[]uint8":
|
|
styp = godwarf.FakeSliceType(godwarf.FakeBasicType("uint", 8))
|
|
case "[]int32", "[]rune":
|
|
styp = godwarf.FakeSliceType(godwarf.FakeBasicType("int", 32))
|
|
default:
|
|
if ambiguousErr != nil && err == reader.ErrTypeNotFound {
|
|
return fmt.Errorf("could not evaluate function or type %s: %v", exprToString(node.Fun), ambiguousErr)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
ctx.pushOp(&TypeCast{DwarfType: styp, Node: node})
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileBuiltinCall(builtin string, args []ast.Expr) error {
|
|
for _, arg := range args {
|
|
err := ctx.compileAST(arg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
ctx.pushOp(&BuiltinCall{builtin, args})
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileIdent(node *ast.Ident) error {
|
|
switch {
|
|
case ctx.HasLocal(node.Name):
|
|
ctx.pushOp(&PushLocal{node.Name})
|
|
case ctx.HasGlobal("", node.Name):
|
|
ctx.pushOp(&PushPackageVar{"", node.Name})
|
|
case node.Name == "true" || node.Name == "false":
|
|
ctx.pushOp(&PushConst{constant.MakeBool(node.Name == "true")})
|
|
case node.Name == "nil":
|
|
ctx.pushOp(&PushNil{})
|
|
default:
|
|
found := false
|
|
if regnum, ok := ctx.LookupRegisterName(node.Name); ok {
|
|
ctx.pushOp(&PushRegister{regnum, node.Name})
|
|
found = true
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("could not find symbol value for %s", node.Name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileUnary(expr ast.Expr, op Op) error {
|
|
err := ctx.compileAST(expr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx.pushOp(op)
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileTypeAssert(node *ast.TypeAssertExpr) error {
|
|
err := ctx.compileAST(node.X)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Accept .(data) as a type assertion that always succeeds, so that users
|
|
// can access the data field of an interface without actually having to
|
|
// type the concrete type.
|
|
if idtyp, isident := node.Type.(*ast.Ident); !isident || idtyp.Name != "data" {
|
|
typ, err := ctx.FindTypeExpr(node.Type)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx.pushOp(&TypeAssert{typ, node})
|
|
return nil
|
|
}
|
|
ctx.pushOp(&TypeAssert{nil, node})
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileBinary(a, b ast.Expr, sop *Jump, op Op) error {
|
|
err := ctx.compileAST(a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sop != nil {
|
|
ctx.pushOp(sop)
|
|
}
|
|
err = ctx.compileAST(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx.pushOp(op)
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileReslice(node *ast.SliceExpr) error {
|
|
err := ctx.compileAST(node.X)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hasHigh := false
|
|
if node.High != nil {
|
|
hasHigh = true
|
|
err = ctx.compileAST(node.High)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if node.Low != nil {
|
|
err = ctx.compileAST(node.Low)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
ctx.pushOp(&PushConst{constant.MakeInt64(0)})
|
|
}
|
|
|
|
ctx.pushOp(&Reslice{Node: node, HasHigh: hasHigh})
|
|
return nil
|
|
}
|
|
|
|
func (ctx *compileCtx) compileFunctionCall(node *ast.CallExpr) error {
|
|
if fnnode, ok := node.Fun.(*ast.Ident); ok {
|
|
if ctx.HasBuiltin(fnnode.Name) {
|
|
return ctx.compileBuiltinCall(fnnode.Name, node.Args)
|
|
}
|
|
}
|
|
if !ctx.allowCalls {
|
|
return ErrFuncCallNotAllowed
|
|
}
|
|
|
|
id := ctx.curCall
|
|
ctx.curCall++
|
|
|
|
oldAllowCalls := ctx.allowCalls
|
|
oldOps := ctx.ops
|
|
ctx.allowCalls = false
|
|
err := ctx.compileAST(node.Fun)
|
|
ctx.allowCalls = oldAllowCalls
|
|
hasFunc := false
|
|
if err != nil {
|
|
ctx.ops = oldOps
|
|
if err != ErrFuncCallNotAllowed {
|
|
return err
|
|
}
|
|
} else {
|
|
hasFunc = true
|
|
}
|
|
ctx.pushOp(&CallInjectionStart{HasFunc: hasFunc, id: id, Node: node})
|
|
|
|
// CallInjectionStart pushes true on the stack if it needs the function argument re-evaluated
|
|
var jmpif *Jump
|
|
if hasFunc {
|
|
jmpif = &Jump{When: JumpIfFalse, Pop: true}
|
|
ctx.pushOp(jmpif)
|
|
}
|
|
ctx.pushOp(&Pop{})
|
|
err = ctx.compileAST(node.Fun)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if jmpif != nil {
|
|
jmpif.Target = len(ctx.ops)
|
|
}
|
|
|
|
ctx.pushOp(&CallInjectionSetTarget{id: id})
|
|
|
|
for i, arg := range node.Args {
|
|
err := ctx.compileAST(arg)
|
|
if err != nil {
|
|
return fmt.Errorf("error evaluating %q as argument %d in function %s: %v", exprToString(arg), i+1, exprToString(node.Fun), err)
|
|
}
|
|
if isStringLiteral(arg) {
|
|
ctx.compileAllocLiteralString()
|
|
}
|
|
ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i, ArgExpr: arg})
|
|
}
|
|
|
|
ctx.pushOp(&CallInjectionComplete{id: id})
|
|
|
|
return nil
|
|
}
|
|
|
|
func Listing(depth []int, ops []Op) string {
|
|
if depth == nil {
|
|
depth = make([]int, len(ops)+1)
|
|
}
|
|
buf := new(strings.Builder)
|
|
for i, op := range ops {
|
|
fmt.Fprintf(buf, " %3d (%2d->%2d) %#v\n", i, depth[i], depth[i+1], op)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func isStringLiteral(expr ast.Expr) bool {
|
|
switch expr := expr.(type) {
|
|
case *ast.BasicLit:
|
|
return expr.Kind == token.STRING
|
|
case *ast.BinaryExpr:
|
|
if expr.Op == token.ADD {
|
|
return isStringLiteral(expr.X) && isStringLiteral(expr.Y)
|
|
}
|
|
case *ast.ParenExpr:
|
|
return isStringLiteral(expr.X)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func removeParen(n ast.Expr) ast.Expr {
|
|
for {
|
|
p, ok := n.(*ast.ParenExpr)
|
|
if !ok {
|
|
break
|
|
}
|
|
n = p.X
|
|
}
|
|
return n
|
|
}
|
|
|
|
func exprToString(t ast.Expr) string {
|
|
var buf bytes.Buffer
|
|
printer.Fprint(&buf, token.NewFileSet(), t)
|
|
return buf.String()
|
|
}
|