611 lines
15 KiB
Go
611 lines
15 KiB
Go
package evalop
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/constant"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/scanner"
|
|
"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)
|
|
HasBuiltin(string) bool
|
|
}
|
|
|
|
// 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}
|
|
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
|
|
}
|
|
|
|
// 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) {
|
|
t, err := parser.ParseExpr(expr)
|
|
if err != nil {
|
|
if canSet {
|
|
eqOff, isAs := isAssignment(err)
|
|
if isAs {
|
|
return CompileSet(lookup, expr[:eqOff], expr[eqOff+1:])
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
return CompileAST(lookup, t)
|
|
}
|
|
|
|
func isAssignment(err error) (int, bool) {
|
|
el, isScannerErr := err.(scanner.ErrorList)
|
|
if isScannerErr && el[0].Msg == "expected '==', found '='" {
|
|
return el[0].Pos.Offset, true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// CompileSet compiles the expression setting lhexpr to rhexpr into a list of
|
|
// instructions.
|
|
func CompileSet(lookup evalLookup, lhexpr, rhexpr string) ([]Op, error) {
|
|
lhe, err := parser.ParseExpr(lhexpr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rhe, err := parser.ParseExpr(rhexpr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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() {
|
|
jmp := &Jump{When: JumpIfAllocStringChecksFail}
|
|
ctx.pushOp(jmp)
|
|
|
|
ctx.compileSpecialCall("runtime.mallocgc", []ast.Expr{
|
|
&ast.BasicLit{Kind: token.INT, Value: "0"},
|
|
&ast.Ident{Name: "nil"},
|
|
&ast.Ident{Name: "false"},
|
|
}, []Op{
|
|
&PushLen{},
|
|
&PushNil{},
|
|
&PushConst{constant.MakeBool(false)},
|
|
})
|
|
|
|
ctx.pushOp(&ConvertAllocToString{})
|
|
jmp.Target = len(ctx.ops)
|
|
}
|
|
|
|
func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args []Op) {
|
|
id := ctx.curCall
|
|
ctx.curCall++
|
|
ctx.pushOp(&CallInjectionStartSpecial{
|
|
id: id,
|
|
FnName: fnname,
|
|
ArgAst: argAst})
|
|
ctx.pushOp(&CallInjectionSetTarget{id: id})
|
|
|
|
for i := range args {
|
|
ctx.pushOp(args[i])
|
|
ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i})
|
|
}
|
|
|
|
ctx.pushOp(&CallInjectionComplete{id: id})
|
|
}
|
|
|
|
func (ctx *compileCtx) pushOp(op Op) {
|
|
ctx.ops = append(ctx.ops, op)
|
|
}
|
|
|
|
// depthCheck validates the list of instructions produced by Compile and
|
|
// CompileSet by performing 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{})
|
|
|
|
default:
|
|
ctx.pushOp(&PushPackageVarOrSelect{Name: x.Name, Sel: node.Sel.Name})
|
|
}
|
|
|
|
case *ast.CallExpr:
|
|
ident, ok := x.Fun.(*ast.SelectorExpr)
|
|
if ok {
|
|
f, ok := ident.X.(*ast.Ident)
|
|
if ok && f.Name == "runtime" && ident.Sel.Name == "frame" {
|
|
switch arg := x.Args[0].(type) {
|
|
case *ast.BasicLit:
|
|
fr, err := strconv.ParseInt(arg.Value, 10, 8)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Push local onto the stack to be evaluated in the new frame context.
|
|
ctx.pushOp(&PushLocal{Name: node.Sel.Name, Frame: fr})
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("expected integer value for frame, got %v", arg)
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
ctx.pushOp(&PushPackageVarOrSelect{Name: s, Sel: node.Sel.Name, NameIsString: true})
|
|
|
|
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 errors.New("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 typ, _ := ctx.FindTypeExpr(n); typ != nil {
|
|
return ctx.compileTypeCast(node, fmt.Errorf("could not find symbol value for %s", n.Name))
|
|
}
|
|
return ctx.compileFunctionCall(node)
|
|
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 *ast.IndexListExpr:
|
|
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 {
|
|
ctx.pushOp(&PushIdent{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
|
|
}
|
|
|
|
trustLen := true
|
|
hasHigh := false
|
|
if node.High != nil {
|
|
hasHigh = true
|
|
err = ctx.compileAST(node.High)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, isbasiclit := node.High.(*ast.BasicLit)
|
|
trustLen = trustLen && isbasiclit
|
|
} else {
|
|
trustLen = false
|
|
}
|
|
|
|
if node.Low != nil {
|
|
err = ctx.compileAST(node.Low)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, isbasiclit := node.Low.(*ast.BasicLit)
|
|
trustLen = trustLen && isbasiclit
|
|
} else {
|
|
ctx.pushOp(&PushConst{constant.MakeInt64(0)})
|
|
}
|
|
|
|
ctx.pushOp(&Reslice{Node: node, HasHigh: hasHigh, TrustLen: trustLen})
|
|
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()
|
|
}
|