proc: do not always allocate struct literals (#3953)

Do not always allocate new struct literals to the target's memroy. Wait
until they actually need to have an address.

Fixes #1465
This commit is contained in:
Alessandro Arzilli
2025-03-24 18:24:49 +01:00
committed by GitHub
parent d15845eb91
commit a80904ca1f
5 changed files with 118 additions and 30 deletions

View File

@ -1279,6 +1279,9 @@ func (stack *evalStack) executeOp() {
v := newVariable("", typeAddr, rttyp, scope.BinInfo, scope.Mem)
stack.push(v.pointerToVariable())
case *evalop.PushNewFakeVariable:
stack.pushNewFakeVariable(scope, op.Type)
default:
stack.err = fmt.Errorf("internal debugger error: unknown eval opcode: %#v", op)
}
@ -1378,6 +1381,17 @@ func (stack *evalStack) pushIdent(scope *EvalScope, name string) (found bool) {
return true
}
func (stack *evalStack) pushNewFakeVariable(scope *EvalScope, typ godwarf.Type) {
cm, err := CreateCompositeMemory(scope.Mem, scope.BinInfo.Arch, *new(op.DwarfRegisters), []op.Piece{{Kind: op.ImmPiece, Bytes: make([]byte, typ.Size()), Size: int(typ.Size())}}, typ.Size())
if err != nil {
stack.err = err
return
}
v := newVariable("", cm.base, typ, scope.BinInfo, cm)
v.Flags = VariableFakeAddress
stack.push(v)
}
func (scope *EvalScope) evalAST(t ast.Expr) (*Variable, error) {
ops, err := evalop.CompileAST(scopeToEvalLookup{scope}, t, scope.evalopFlags())
if err != nil {

View File

@ -111,7 +111,10 @@ func CompileSet(lookup evalLookup, lhexpr, rhexpr string, flags Flags) ([]Op, er
return nil, err
}
ctx.maybeMaterialize(rhe)
err = ctx.maybeMaterialize(rhe)
if err != nil {
return nil, err
}
err = ctx.compileAST(lhe, false)
if err != nil {
@ -419,21 +422,7 @@ func (ctx *compileCtx) compileAST(t ast.Expr, toplevel bool) error {
return errFuncCallNotAllowedLitAlloc
}
ctx.compileSpecialCall("runtime.mallocgc", []ast.Expr{
&ast.BasicLit{Kind: token.INT, Value: "1"},
node.Type,
&ast.Ident{Name: "true"},
}, []Op{
&PushConst{Value: constant.MakeInt64(1)},
&PushRuntimeType{dtyp},
&PushConst{Value: constant.MakeBool(true)},
}, specialCallDoPinning)
ctx.pushOp(&TypeCast{
DwarfType: godwarf.FakePointerType(dtyp, int64(ctx.PtrSize())),
Node: &ast.CallExpr{
Fun: node.Type,
Args: []ast.Expr{&ast.Ident{Name: "new allocation"}}}})
ctx.pushOp(&PointerDeref{&ast.StarExpr{X: &ast.Ident{Name: "new allocation"}}})
ctx.pushOp(&PushNewFakeVariable{Type: dtyp})
for i, elt := range node.Elts {
ctx.pushOp(&Dup{})
@ -447,7 +436,10 @@ func (ctx *compileCtx) compileAST(t ast.Expr, toplevel bool) error {
if err != nil {
return err
}
ctx.maybeMaterialize(elt.Value)
err = ctx.maybeMaterialize(elt.Value)
if err != nil {
return err
}
default:
ctx.pushOp(&Select{Name: typ.Field[i].Name})
rhe = elt
@ -455,7 +447,10 @@ func (ctx *compileCtx) compileAST(t ast.Expr, toplevel bool) error {
if err != nil {
return err
}
ctx.maybeMaterialize(elt)
err = ctx.maybeMaterialize(elt)
if err != nil {
return err
}
}
ctx.pushOp(&Roll{1})
ctx.pushOp(&SetValue{Rhe: rhe})
@ -736,7 +731,10 @@ func (ctx *compileCtx) compileFunctionCallNoPinning(node *ast.CallExpr, id int)
if err != nil {
return fmt.Errorf("error evaluating %q as argument %d in function %s: %v", astutil.ExprToString(arg), i+1, astutil.ExprToString(node.Fun), err)
}
ctx.maybeMaterialize(arg)
err = ctx.maybeMaterialize(arg)
if err != nil {
return err
}
ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i, ArgExpr: arg})
}
@ -760,12 +758,13 @@ func (ctx *compileCtx) compileFunctionCallWithPinning(node *ast.CallExpr, id int
for i, arg := range node.Args {
err := ctx.compileAST(arg, false)
if isStringLiteralThatNeedsAlloc(arg) {
ctx.compileAllocLiteralString()
}
if err != nil {
return fmt.Errorf("error evaluating %q as argument %d in function %s: %v", astutil.ExprToString(arg), i+1, astutil.ExprToString(node.Fun), err)
}
err = ctx.maybeMaterialize(arg)
if err != nil {
return err
}
}
ctx.pushOp(&Roll{len(node.Args)})
@ -805,12 +804,67 @@ func (ctx *compileCtx) compilePinningLoop(id int) {
ctx.pushOp(&CallInjectionComplete2{id: id})
}
// If expr will produce a string literal allocate the string into the target
// program's address space.
func (ctx *compileCtx) maybeMaterialize(expr ast.Expr) {
// If expr will produce a string literal or a pointer to a literal allocate
// it into the target program's address space.
func (ctx *compileCtx) maybeMaterialize(expr ast.Expr) error {
if isStringLiteralThatNeedsAlloc(expr) {
ctx.compileAllocLiteralString()
return nil
}
expr = removeParen(expr)
addrof, isunary := expr.(*ast.UnaryExpr)
if !isunary || addrof.Op != token.AND {
return nil
}
lit, iscomplit := addrof.X.(*ast.CompositeLit)
if !iscomplit {
return nil
}
dtyp, err := ctx.FindTypeExpr(lit.Type)
if err != nil {
return err
}
switch godwarf.ResolveTypedef(dtyp).(type) {
case *godwarf.StructType, *godwarf.ArrayType:
if _, isaddrof := ctx.ops[len(ctx.ops)-1].(*AddrOf); isaddrof {
ctx.ops = ctx.ops[:len(ctx.ops)-1]
} else {
ctx.pushOp(&PointerDeref{&ast.StarExpr{X: &ast.Ident{Name: "unallocated-literal"}}})
}
ctx.compileSpecialCall("runtime.mallocgc", []ast.Expr{
&ast.BasicLit{Kind: token.INT, Value: "1"},
lit.Type,
&ast.Ident{Name: "true"},
}, []Op{
&PushConst{Value: constant.MakeInt64(1)},
&PushRuntimeType{dtyp},
&PushConst{Value: constant.MakeBool(true)},
}, specialCallDoPinning)
ctx.pushOp(&TypeCast{
DwarfType: godwarf.FakePointerType(dtyp, int64(ctx.PtrSize())),
Node: &ast.CallExpr{
Fun: lit.Type,
Args: []ast.Expr{&ast.Ident{Name: "new allocation"}}}})
xderef := &ast.StarExpr{X: &ast.Ident{Name: "new-allocation"}}
xset := &ast.Ident{Name: "literal-allocation"}
ctx.pushOp(&Dup{}) // stack after: [ ptrToRealLiteral, ptrToRealLiteral, fakeLiteral ]
ctx.pushOp(&PointerDeref{xderef}) // stack after: [ realLiteral, ptrToRealLiteral, fakeLiteral ]
ctx.pushOp(&Roll{2}) // stack after: [ fakeLiteral, realLiteral, ptrToRealLiteral ]
ctx.pushOp(&Roll{1}) // stack after: [ realLiteral, fakeLiteral, ptrToRealLiteral ]
ctx.pushOp(&SetValue{Rhe: xset}) // stack after: [ ptrToRealLiteral ]
return nil
default:
// either *godwarf.MapType, *godwarf.SliceType or *godwarf.FuncType
return fmt.Errorf("allocating a literal of type %s not implemented", dtyp.String())
}
}
func Listing(depth []int, ops []Op) string {

View File

@ -347,3 +347,11 @@ type PushRuntimeType struct {
}
func (*PushRuntimeType) depthCheck() (npop, npush int) { return 0, 1 }
// PushNewFakeVariable pushes a new debugger-allocated variable on to the
// stack with the given type.
type PushNewFakeVariable struct {
Type godwarf.Type
}
func (*PushNewFakeVariable) depthCheck() (npop, npush int) { return 0, 1 }

View File

@ -190,7 +190,11 @@ func (mem *compositeMemory) WriteMemory(addr uint64, data []byte) (int, error) {
return 0, errors.New("write out of bounds")
}
if mem.regs.ChangeFunc == nil {
return 0, errors.New("can not write registers")
for _, piece := range mem.pieces {
if piece.Kind == op.RegPiece {
return 0, errors.New("can not write registers")
}
}
}
copy(mem.data[addr:], data)
@ -216,7 +220,6 @@ func (mem *compositeMemory) WriteMemory(addr uint64, data []byte) (int, error) {
return donesz + n, err
}
case op.ImmPiece:
//TODO(aarzilli): maybe return an error if the user tried to change the value?
// nothing to do
default:
panic("unsupported piece kind")

View File

@ -669,6 +669,9 @@ func getEvalExpressionTestCases() []varTest {
{"mnil[\"Malone\"]", false, "", "", "", errors.New("key not found")},
{"m1[80:]", false, "", "", "", errors.New("map index out of bounds")},
{"mlarge", false, "map[main.largestruct]main.largestruct [{name: \"one\", v: [256]uint8 [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+192 more]}: {name: \"oneval\", v: [256]uint8 [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...+192 more]}, ]", "map[main.largestruct]main.largestruct [...]", "map[main.largestruct]main.largestruct", nil},
{"m3[main.astruct{1,1}]", false, "42", "42", "int", nil},
{"m4[main.astruct{2,2}]", false, "main.astruct {A: 22, B: 22}", "main.astruct {A: 22, B: 22}", "main.astruct", nil},
{"m3[main.astruct{3,3}]", false, "", "", "", errors.New("key not found")},
// interfaces
{"err1", true, "error(*main.astruct) *{A: 1, B: 2}", "error(*main.astruct) 0x…", "error", nil},
@ -982,6 +985,12 @@ func TestEvalExpression(t *testing.T) {
// this type of eval is unsupported with the current version of Go.
return
}
if err != nil && err.Error() == "expression *ast.CompositeLit not implemented" {
if !goversion.VersionAfterOrEqual(runtime.Version(), 1, 23) || runtime.GOARCH == "386" {
// composite literals not supported before 1.22
return
}
}
if tc.err == nil {
assertNoError(err, t, fmt.Sprintf("EvalExpression(%s) returned an error", tc.name))
assertVariable(t, variable, tc)
@ -1403,11 +1412,11 @@ func TestCallFunction(t *testing.T) {
}
var testcases123 = []testCaseCallFunction{
{`mul2(main.a2struct{Y: 3})`, []string{":int:6"}, nil, 1},
{`mul2(main.a2struct{4})`, []string{":int:8"}, nil, 1},
{`mul2(main.a2struct{Y: 3})`, []string{":int:6"}, nil, 0},
{`mul2(main.a2struct{4})`, []string{":int:8"}, nil, 0},
{`mul2ptr(&main.a2struct{Y: 3})`, []string{":int:6"}, nil, 1},
{`mul2ptr(&main.a2struct{1})`, []string{":int:2"}, nil, 1},
{`m[main.intpair{3, 1}]`, []string{`:string:"three,one"`}, nil, 1},
{`m[main.intpair{3, 1}]`, []string{`:string:"three,one"`}, nil, 0},
}
withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {