proc: refactor identifier evaluation for range-over-func support (#3738)

Because of how range-over-func is implemented it is difficult to
determine the set of visible local variables during expression
compilation (i.e. it is difficulto to keep the HasLocal function
correct).
This commit moves that logic from expression compilation to expression
evaluation.

Updates #3733
This commit is contained in:
Alessandro Arzilli 2024-06-14 23:36:11 +02:00 committed by GitHub
parent cce54c0992
commit 4b628b81cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 165 additions and 204 deletions

@ -203,81 +203,6 @@ func (s scopeToEvalLookup) FindTypeExpr(expr ast.Expr) (godwarf.Type, error) {
return s.BinInfo.findTypeExpr(expr)
}
func (scope scopeToEvalLookup) HasLocal(name string) bool {
if scope.Fn == nil {
return false
}
flags := reader.VariablesOnlyVisible
if scope.BinInfo.Producer() != "" && goversion.ProducerAfterOrEqual(scope.BinInfo.Producer(), 1, 15) {
flags |= reader.VariablesTrustDeclLine
}
dwarfTree, err := scope.image().getDwarfTree(scope.Fn.offset)
if err != nil {
return false
}
varEntries := reader.Variables(dwarfTree, scope.PC, scope.Line, flags)
for _, entry := range varEntries {
curname, _ := entry.Val(dwarf.AttrName).(string)
if curname == name {
return true
}
if len(curname) > 0 && curname[0] == '&' {
if curname[1:] == name {
return true
}
}
}
return false
}
func (scope scopeToEvalLookup) HasGlobal(pkgName, varName string) bool {
hasGlobalInternal := func(name string) bool {
for _, pkgvar := range scope.BinInfo.packageVars {
if pkgvar.name == name || strings.HasSuffix(pkgvar.name, "/"+name) {
return true
}
}
for _, fn := range scope.BinInfo.Functions {
if fn.Name == name || strings.HasSuffix(fn.Name, "/"+name) {
return true
}
}
for _, ctyp := range scope.BinInfo.consts {
for _, cval := range ctyp.values {
if cval.fullName == name || strings.HasSuffix(cval.fullName, "/"+name) {
return true
}
}
}
return false
}
if pkgName == "" {
if scope.Fn == nil {
return false
}
return hasGlobalInternal(scope.Fn.PackageName() + "." + varName)
}
for _, pkgPath := range scope.BinInfo.PackageMap[pkgName] {
if hasGlobalInternal(pkgPath + "." + varName) {
return true
}
}
return hasGlobalInternal(pkgName + "." + varName)
}
func (scope scopeToEvalLookup) LookupRegisterName(name string) (int, bool) {
s := validRegisterName(name)
if s == "" {
return 0, false
}
return scope.BinInfo.Arch.RegisterNameToDwarf(s)
}
func (scope scopeToEvalLookup) HasBuiltin(name string) bool {
return supportedBuiltins[name] != nil
}
@ -680,7 +605,20 @@ func (scope *EvalScope) findGlobal(pkgName, varName string) (*Variable, error) {
if err != nil || v != nil {
return v, err
}
return nil, fmt.Errorf("could not find symbol value for %s.%s", pkgName, varName)
return nil, &errCouldNotFindSymbol{fmt.Sprintf("%s.%s", pkgName, varName)}
}
type errCouldNotFindSymbol struct {
name string
}
func (e *errCouldNotFindSymbol) Error() string {
return fmt.Sprintf("could not find symbol %s", e.name)
}
func isSymbolNotFound(e error) bool {
var e2 *errCouldNotFindSymbol
return errors.As(e, &e2)
}
func (scope *EvalScope) findGlobalInternal(name string) (*Variable, error) {
@ -967,30 +905,7 @@ func (stack *evalStack) executeOp() {
stack.push(newConstant(op.Value, scope.Mem))
case *evalop.PushLocal:
var vars []*Variable
var err error
if op.Frame != 0 {
frameScope, err2 := ConvertEvalScope(scope.target, -1, int(op.Frame), 0)
if err2 != nil {
stack.err = err2
return
}
vars, err = frameScope.Locals(0)
} else {
vars, err = scope.Locals(0)
}
if err != nil {
stack.err = err
return
}
found := false
for i := range vars {
if vars[i].Name == op.Name && vars[i].Flags&VariableShadowed == 0 {
stack.push(vars[i])
found = true
break
}
}
found := stack.pushLocal(scope, op.Name, op.Frame)
if !found {
stack.err = fmt.Errorf("could not find symbol value for %s", op.Name)
}
@ -998,52 +913,36 @@ func (stack *evalStack) executeOp() {
case *evalop.PushNil:
stack.push(nilVariable)
case *evalop.PushRegister:
reg := scope.Regs.Reg(uint64(op.Regnum))
if reg == nil {
stack.err = fmt.Errorf("could not find symbol value for %s", op.Regname)
return
}
reg.FillBytes()
var typ godwarf.Type
if len(reg.Bytes) <= 8 {
typ = godwarf.FakeBasicType("uint", 64)
} else {
var err error
typ, err = scope.BinInfo.findType("string")
if err != nil {
stack.err = err
return
}
}
v := newVariable(op.Regname, 0, typ, scope.BinInfo, scope.Mem)
if v.Kind == reflect.String {
v.Len = int64(len(reg.Bytes) * 2)
v.Base = fakeAddressUnresolv
}
v.Addr = fakeAddressUnresolv
v.Flags = VariableCPURegister
v.reg = reg
stack.push(v)
case *evalop.PushPackageVar:
pkgName := op.PkgName
replaceName := false
if pkgName == "" {
replaceName = true
pkgName = scope.Fn.PackageName()
}
v, err := scope.findGlobal(pkgName, op.Name)
if err != nil {
case *evalop.PushPackageVarOrSelect:
v, err := scope.findGlobal(op.Name, op.Sel)
if err != nil && !isSymbolNotFound(err) {
stack.err = err
return
}
if replaceName {
v.Name = op.Name
if v != nil {
stack.push(v)
} else {
if op.NameIsString {
stack.err = fmt.Errorf("%q (type string) is not a struct", op.Name)
return
}
found := stack.pushIdent(scope, op.Name)
if stack.err != nil {
return
}
if found {
scope.evalStructSelector(&evalop.Select{Name: op.Sel}, stack)
} else {
stack.err = fmt.Errorf("could not find symbol value for %s", op.Name)
}
}
case *evalop.PushIdent:
found := stack.pushIdent(scope, op.Name)
if !found {
stack.err = fmt.Errorf("could not find symbol value for %s", op.Name)
}
stack.push(v)
case *evalop.PushLen:
v := stack.peek()
@ -1134,6 +1033,98 @@ func (stack *evalStack) executeOp() {
stack.opidx++
}
func (stack *evalStack) pushLocal(scope *EvalScope, name string, frame int64) (found bool) {
var vars []*Variable
var err error
if frame != 0 {
frameScope, err2 := ConvertEvalScope(scope.target, -1, int(frame), 0)
if err2 != nil {
stack.err = err2
return
}
vars, err = frameScope.Locals(0)
} else {
vars, err = scope.Locals(0)
}
if err != nil {
stack.err = err
return
}
found = false
for i := range vars {
if vars[i].Name == name && vars[i].Flags&VariableShadowed == 0 {
stack.push(vars[i])
found = true
break
}
}
return found
}
func (stack *evalStack) pushIdent(scope *EvalScope, name string) (found bool) {
found = stack.pushLocal(scope, name, 0)
if found || stack.err != nil {
return found
}
v, err := scope.findGlobal(scope.Fn.PackageName(), name)
if err != nil && !isSymbolNotFound(err) {
stack.err = err
return false
}
if v != nil {
v.Name = name
stack.push(v)
return true
}
switch name {
case "true", "false":
stack.push(newConstant(constant.MakeBool(name == "true"), scope.Mem))
return true
case "nil":
stack.push(nilVariable)
return true
}
regname := validRegisterName(name)
if regname == "" {
return false
}
regnum, ok := scope.BinInfo.Arch.RegisterNameToDwarf(regname)
if !ok {
return false
}
reg := scope.Regs.Reg(uint64(regnum))
if reg == nil {
return
}
reg.FillBytes()
var typ godwarf.Type
if len(reg.Bytes) <= 8 {
typ = godwarf.FakeBasicType("uint", 64)
} else {
var err error
typ, err = scope.BinInfo.findType("string")
if err != nil {
stack.err = err
return false
}
}
v = newVariable(regname, 0, typ, scope.BinInfo, scope.Mem)
if v.Kind == reflect.String {
v.Len = int64(len(reg.Bytes) * 2)
v.Base = fakeAddressUnresolv
}
v.Addr = fakeAddressUnresolv
v.Flags = VariableCPURegister
v.reg = reg
stack.push(v)
return true
}
func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
ops, err := evalop.CompileAST(scopeToEvalLookup{scope}, t)
if err != nil {

@ -30,10 +30,7 @@ type compileCtx struct {
type evalLookup interface {
FindTypeExpr(ast.Expr) (godwarf.Type, error)
HasLocal(string) bool
HasGlobal(string, string) bool
HasBuiltin(string) bool
LookupRegisterName(string) (int, bool)
}
// CompileAST compiles the expression t into a list of instructions.
@ -221,15 +218,8 @@ func (ctx *compileCtx) compileAST(t ast.Expr) error {
case x.Name == "runtime" && node.Sel.Name == "threadid":
ctx.pushOp(&PushThreadID{})
case ctx.HasLocal(x.Name):
ctx.pushOp(&PushLocal{Name: 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})
ctx.pushOp(&PushPackageVarOrSelect{Name: x.Name, Sel: node.Sel.Name})
}
case *ast.CallExpr:
@ -258,11 +248,7 @@ func (ctx *compileCtx) compileAST(t ast.Expr) error {
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})
ctx.pushOp(&PushPackageVarOrSelect{Name: s, Sel: node.Sel.Name, NameIsString: true})
default:
return ctx.compileUnary(node.X, &Select{node.Sel.Name})
@ -367,13 +353,10 @@ func (ctx *compileCtx) compileTypeCastOrFuncCall(node *ast.CallExpr) error {
}
return ctx.compileFunctionCall(node)
case *ast.Ident:
if ctx.HasBuiltin(n.Name) {
return ctx.compileFunctionCall(node)
if typ, _ := ctx.FindTypeExpr(n); typ != nil {
return ctx.compileTypeCast(node, fmt.Errorf("could not find symbol value for %s", n.Name))
}
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))
return ctx.compileFunctionCall(node)
case *ast.IndexExpr:
// Ambiguous, could be a parametric type
switch n.X.(type) {
@ -438,25 +421,7 @@ func (ctx *compileCtx) compileBuiltinCall(builtin string, args []ast.Expr) error
}
func (ctx *compileCtx) compileIdent(node *ast.Ident) error {
switch {
case ctx.HasLocal(node.Name):
ctx.pushOp(&PushLocal{Name: 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)
}
}
ctx.pushOp(&PushIdent{node.Name})
return nil
}

@ -45,27 +45,30 @@ type PushLocal struct {
func (*PushLocal) depthCheck() (npop, npush int) { return 0, 1 }
// PushIdent pushes a local or global variable or a predefined identifier
// (true, false, etc) or the value of a register on the stack.
type PushIdent struct {
Name string
}
func (*PushIdent) depthCheck() (npop, npush int) { return 0, 1 }
// PushPackageVarOrSelect pushes the value of Name.Sel on the stack, which
// could either be a global variable (with the package name specified), or a
// field of a local variable.
type PushPackageVarOrSelect struct {
Name, Sel string
NameIsString bool
}
func (*PushPackageVarOrSelect) depthCheck() (npop, npush int) { return 0, 1 }
// PushNil pushes an untyped nil on the stack.
type PushNil struct {
}
func (*PushNil) depthCheck() (npop, npush int) { return 0, 1 }
// PushRegister pushes the CPU register Regnum on the stack.
type PushRegister struct {
Regnum int
Regname string
}
func (*PushRegister) depthCheck() (npop, npush int) { return 0, 1 }
// PushPackageVar pushes a package variable on the stack.
type PushPackageVar struct {
PkgName, Name string // if PkgName == "" use current function's package
}
func (*PushPackageVar) depthCheck() (npop, npush int) { return 0, 1 }
// PushLen pushes the length of the variable at the top of the stack into
// the stack.
type PushLen struct {

@ -143,6 +143,7 @@ func dataAtAddr(thread proc.MemoryReadWriter, addr uint64) ([]byte, error) {
}
func assertNoError(err error, t testing.TB, s string) {
t.Helper()
if err != nil {
_, file, line, _ := runtime.Caller(1)
fname := filepath.Base(file)
@ -1260,6 +1261,7 @@ func evalVariableOrError(p *proc.Target, symbol string) (*proc.Variable, error)
}
func evalVariable(p *proc.Target, t testing.TB, symbol string) *proc.Variable {
t.Helper()
v, err := evalVariableOrError(p, symbol)
if err != nil {
_, file, line, _ := runtime.Caller(1)

@ -820,9 +820,9 @@ func getEvalExpressionTestCases() []varTest {
{"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 evaluate function or type unknownthing: could not find symbol value for unknownthing")},
{"(*unknownthing)(2)", false, "", "", "", errors.New("could not evaluate function or type (*unknownthing): could not find symbol value for unknownthing")},
{"(*strings.Split)(2)", false, "", "", "", errors.New("could not evaluate function or type (*strings.Split): could not find symbol value for strings")},
{"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")},
// pretty printing special types
{"tim1", false, `time.Time(1977-05-25T18:00:00Z)…`, `time.Time(1977-05-25T18:00:00Z)…`, "time.Time", nil},
@ -1195,7 +1195,7 @@ func TestCallFunction(t *testing.T) {
{`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(`error evaluating "s1" as argument 1 in function stringsJoin: could not find symbol value for s1`)},
{`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},
@ -1295,11 +1295,11 @@ func TestCallFunction(t *testing.T) {
}
var testcasesBefore114After112 = []testCaseCallFunction{
{`strings.Join(s1, comma)`, nil, errors.New(`error evaluating "s1" as argument 1 in function strings.Join: could not find symbol value for s1`)},
{`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`)},
}
var testcases114 = []testCaseCallFunction{
{`strings.Join(s1, comma)`, nil, errors.New(`error evaluating "s1" as argument 1 in function strings.Join: could not find symbol value for s1`)},
{`strings.Join(s1, comma)`, nil, errors.New(`could not find symbol value for s1`)},
}
var testcases117 = []testCaseCallFunction{