From 79e03a9281ba03f9f79904f074e2e343f2140bdd Mon Sep 17 00:00:00 2001 From: Than McIntosh Date: Wed, 10 Nov 2021 10:44:00 -0500 Subject: [PATCH] cmd/compile: include register-resident output params in DWARF-gen During the register ABI work, a change was made in CL 302071 to "stackframe" to treat register-resident output parameter (PARAMOUT) variables that same as locals, which meant that if they were unused, we'd delete them from the "Dcl" slice. This has the effect of making them invisible to DWARF generation later on in the pipeline, meaning that we don't get DIEs for them in the debug info. This patch fixes the problem by capturing these params prior to optimization and then adding them back in for consideration when we're processing the params/locals of a function during DWARF generation. Fixes #48573. Change-Id: I2b32882911c18f91c3e3d009486517522d262685 Reviewed-on: https://go-review.googlesource.com/c/go/+/362618 Trust: Than McIntosh Run-TryBot: Than McIntosh TryBot-Result: Go Bot Reviewed-by: David Chase --- src/cmd/compile/internal/dwarfgen/dwarf.go | 13 + src/cmd/compile/internal/ssa/debug.go | 40 ++-- src/cmd/compile/internal/ssagen/ssa.go | 19 +- src/cmd/link/internal/ld/dwarf_test.go | 266 +++++++++++++++++---- 4 files changed, 268 insertions(+), 70 deletions(-) diff --git a/src/cmd/compile/internal/dwarfgen/dwarf.go b/src/cmd/compile/internal/dwarfgen/dwarf.go index 3007262db9..e249a52e57 100644 --- a/src/cmd/compile/internal/dwarfgen/dwarf.go +++ b/src/cmd/compile/internal/dwarfgen/dwarf.go @@ -150,6 +150,19 @@ func createDwarfVars(fnsym *obj.LSym, complexOK bool, fn *ir.Func, apDecls []*ir dcl := apDecls if fnsym.WasInlined() { dcl = preInliningDcls(fnsym) + } else { + // The backend's stackframe pass prunes away entries from the + // fn's Dcl list, including PARAMOUT nodes that correspond to + // output params passed in registers. Add back in these + // entries here so that we can process them properly during + // DWARF-gen. See issue 48573 for more details. + debugInfo := fn.DebugInfo.(*ssa.FuncDebug) + for _, n := range debugInfo.RegOutputParams { + if n.Class != ir.PPARAMOUT || !n.IsOutputParamInRegisters() { + panic("invalid ir.Name on debugInfo.RegOutputParams list") + } + dcl = append(dcl, n) + } } // If optimization is enabled, the list above will typically be diff --git a/src/cmd/compile/internal/ssa/debug.go b/src/cmd/compile/internal/ssa/debug.go index fed152efba..aad59fa24e 100644 --- a/src/cmd/compile/internal/ssa/debug.go +++ b/src/cmd/compile/internal/ssa/debug.go @@ -34,6 +34,9 @@ type FuncDebug struct { VarSlots [][]SlotID // The location list data, indexed by VarID. Must be processed by PutLocationList. LocationLists [][]byte + // Register-resident output parameters for the function. This is filled in at + // SSA generation time. + RegOutputParams []*ir.Name // Filled in by the user. Translates Block and Value ID to PC. GetPC func(ID, ID) int64 @@ -548,10 +551,10 @@ func PopulateABIInRegArgOps(f *Func) { f.Entry.Values = append(newValues, f.Entry.Values...) } -// BuildFuncDebug returns debug information for f. +// BuildFuncDebug debug information for f, placing the results in "rval". // f must be fully processed, so that each Value is where it will be when // machine code is emitted. -func BuildFuncDebug(ctxt *obj.Link, f *Func, loggingEnabled bool, stackOffset func(LocalSlot) int32) *FuncDebug { +func BuildFuncDebug(ctxt *obj.Link, f *Func, loggingEnabled bool, stackOffset func(LocalSlot) int32, rval *FuncDebug) { if f.RegAlloc == nil { f.Fatalf("BuildFuncDebug on func %v that has not been fully processed", f) } @@ -661,12 +664,11 @@ func BuildFuncDebug(ctxt *obj.Link, f *Func, loggingEnabled bool, stackOffset fu blockLocs := state.liveness() state.buildLocationLists(blockLocs) - return &FuncDebug{ - Slots: state.slots, - VarSlots: state.varSlots, - Vars: state.vars, - LocationLists: state.lists, - } + // Populate "rval" with what we've computed. + rval.Slots = state.slots + rval.VarSlots = state.varSlots + rval.Vars = state.vars + rval.LocationLists = state.lists } // liveness walks the function in control flow order, calculating the start @@ -1593,7 +1595,7 @@ func isNamedRegParam(p abi.ABIParamAssignment) bool { return true } -// BuildFuncDebugNoOptimized constructs a FuncDebug object with +// BuildFuncDebugNoOptimized populates a FuncDebug object "rval" with // entries corresponding to the register-resident input parameters for // the function "f"; it is used when we are compiling without // optimization but the register ABI is enabled. For each reg param, @@ -1601,8 +1603,7 @@ func isNamedRegParam(p abi.ABIParamAssignment) bool { // the input register, and the second element holds the stack location // of the param (the assumption being that when optimization is off, // each input param reg will be spilled in the prolog. -func BuildFuncDebugNoOptimized(ctxt *obj.Link, f *Func, loggingEnabled bool, stackOffset func(LocalSlot) int32) *FuncDebug { - fd := FuncDebug{} +func BuildFuncDebugNoOptimized(ctxt *obj.Link, f *Func, loggingEnabled bool, stackOffset func(LocalSlot) int32, rval *FuncDebug) { pri := f.ABISelf.ABIAnalyzeFuncType(f.Type.FuncType()) @@ -1616,7 +1617,7 @@ func BuildFuncDebugNoOptimized(ctxt *obj.Link, f *Func, loggingEnabled bool, sta } } if numRegParams == 0 { - return &fd + return } state := debugState{f: f} @@ -1626,7 +1627,7 @@ func BuildFuncDebugNoOptimized(ctxt *obj.Link, f *Func, loggingEnabled bool, sta } // Allocate location lists. - fd.LocationLists = make([][]byte, numRegParams) + rval.LocationLists = make([][]byte, numRegParams) // Locate the value corresponding to the last spill of // an input register. @@ -1642,10 +1643,10 @@ func BuildFuncDebugNoOptimized(ctxt *obj.Link, f *Func, loggingEnabled bool, sta n := inp.Name.(*ir.Name) sl := LocalSlot{N: n, Type: inp.Type, Off: 0} - fd.Vars = append(fd.Vars, n) - fd.Slots = append(fd.Slots, sl) - slid := len(fd.VarSlots) - fd.VarSlots = append(fd.VarSlots, []SlotID{SlotID(slid)}) + rval.Vars = append(rval.Vars, n) + rval.Slots = append(rval.Slots, sl) + slid := len(rval.VarSlots) + rval.VarSlots = append(rval.VarSlots, []SlotID{SlotID(slid)}) if afterPrologVal == ID(-1) { // This can happen for degenerate functions with infinite @@ -1662,7 +1663,7 @@ func BuildFuncDebugNoOptimized(ctxt *obj.Link, f *Func, loggingEnabled bool, sta // Param is arriving in one or more registers. We need a 2-element // location expression for it. First entry in location list // will correspond to lifetime in input registers. - list, sizeIdx := setupLocList(ctxt, f, fd.LocationLists[pidx], + list, sizeIdx := setupLocList(ctxt, f, rval.LocationLists[pidx], BlockStart.ID, afterPrologVal) if list == nil { pidx++ @@ -1727,8 +1728,7 @@ func BuildFuncDebugNoOptimized(ctxt *obj.Link, f *Func, loggingEnabled bool, sta // fill in size ctxt.Arch.ByteOrder.PutUint16(list[sizeIdx:], uint16(len(list)-sizeIdx-2)) - fd.LocationLists[pidx] = list + rval.LocationLists[pidx] = list pidx++ } - return &fd } diff --git a/src/cmd/compile/internal/ssagen/ssa.go b/src/cmd/compile/internal/ssagen/ssa.go index 0853242e6f..d6407af334 100644 --- a/src/cmd/compile/internal/ssagen/ssa.go +++ b/src/cmd/compile/internal/ssagen/ssa.go @@ -484,6 +484,19 @@ func buildssa(fn *ir.Func, worker int) *ssa.Func { var params *abi.ABIParamResultInfo params = s.f.ABISelf.ABIAnalyze(fn.Type(), true) + // The backend's stackframe pass prunes away entries from the fn's + // Dcl list, including PARAMOUT nodes that correspond to output + // params passed in registers. Walk the Dcl list and capture these + // nodes to a side list, so that we'll have them available during + // DWARF-gen later on. See issue 48573 for more details. + var debugInfo ssa.FuncDebug + for _, n := range fn.Dcl { + if n.Class == ir.PPARAMOUT && n.IsOutputParamInRegisters() { + debugInfo.RegOutputParams = append(debugInfo.RegOutputParams, n) + } + } + fn.DebugInfo = &debugInfo + // Generate addresses of local declarations s.decladdrs = map[*ir.Name]*ssa.Value{} for _, n := range fn.Dcl { @@ -7003,12 +7016,12 @@ func genssa(f *ssa.Func, pp *objw.Progs) { if base.Ctxt.Flag_locationlists { var debugInfo *ssa.FuncDebug + debugInfo = e.curfn.DebugInfo.(*ssa.FuncDebug) if e.curfn.ABI == obj.ABIInternal && base.Flag.N != 0 { - debugInfo = ssa.BuildFuncDebugNoOptimized(base.Ctxt, f, base.Debug.LocationLists > 1, StackOffset) + ssa.BuildFuncDebugNoOptimized(base.Ctxt, f, base.Debug.LocationLists > 1, StackOffset, debugInfo) } else { - debugInfo = ssa.BuildFuncDebug(base.Ctxt, f, base.Debug.LocationLists > 1, StackOffset) + ssa.BuildFuncDebug(base.Ctxt, f, base.Debug.LocationLists > 1, StackOffset, debugInfo) } - e.curfn.DebugInfo = debugInfo bstart := s.bstart idToIdx := make([]int, f.NumBlocks()) for i, b := range f.Blocks { diff --git a/src/cmd/link/internal/ld/dwarf_test.go b/src/cmd/link/internal/ld/dwarf_test.go index db9002491e..9a163488e6 100644 --- a/src/cmd/link/internal/ld/dwarf_test.go +++ b/src/cmd/link/internal/ld/dwarf_test.go @@ -1635,6 +1635,66 @@ func TestIssue42484(t *testing.T) { f.Close() } +// processParams examines the formal parameter children of subprogram +// DIE "die" using the explorer "ex" and returns a string that +// captures the name, order, and classification of the subprogram's +// input and output parameters. For example, for the go function +// +// func foo(i1 int, f1 float64) (string, bool) { +// +// this function would return a string something like +// +// i1:0:1 f1:1:1 ~r0:2:2 ~r1:3:2 +// +// where each chunk above is of the form NAME:ORDER:INOUTCLASSIFICATION +// +func processParams(die *dwarf.Entry, ex *examiner) string { + // Values in the returned map are of the form : + // where order is the order within the child DIE list of the + // param, and is an integer: + // + // -1: varparm attr not found + // 1: varparm found with value false + // 2: varparm found with value true + // + foundParams := make(map[string]string) + + // Walk ABCs's children looking for params. + abcIdx := ex.idxFromOffset(die.Offset) + childDies := ex.Children(abcIdx) + idx := 0 + for _, child := range childDies { + if child.Tag == dwarf.TagFormalParameter { + // NB: a setting of DW_AT_variable_parameter indicates + // that the param in question is an output parameter; we + // want to see this attribute set to TRUE for all Go + // return params. It would be OK to have it missing for + // input parameters, but for the moment we verify that the + // attr is present but set to false. + st := -1 + if vp, ok := child.Val(dwarf.AttrVarParam).(bool); ok { + if vp { + st = 2 + } else { + st = 1 + } + } + if name, ok := child.Val(dwarf.AttrName).(string); ok { + foundParams[name] = fmt.Sprintf("%d:%d", idx, st) + idx++ + } + } + } + + found := make([]string, 0, len(foundParams)) + for k, v := range foundParams { + found = append(found, fmt.Sprintf("%s:%s", k, v)) + } + sort.Strings(found) + + return fmt.Sprintf("%+v", found) +} + func TestOutputParamAbbrevAndAttr(t *testing.T) { testenv.MustHaveGoBuild(t) @@ -1694,56 +1754,15 @@ func main() { t.Fatalf("unexpected tag %v on main.ABC DIE", abcdie.Tag) } - // A setting of DW_AT_variable_parameter indicates that the - // param in question is an output parameter; we want to see this - // attribute set to TRUE for all Go return params. It would be - // OK to have it missing for input parameters, but for the moment - // we verify that the attr is present but set to false. - - // Values in this map are of the form : - // where order is the order within the child DIE list of the param, - // and is an integer: - // - // -1: varparm attr not found - // 1: varparm found with value false - // 2: varparm found with value true - // - foundParams := make(map[string]string) - - // Walk ABCs's children looking for params. - abcIdx := ex.idxFromOffset(abcdie.Offset) - childDies := ex.Children(abcIdx) - idx := 0 - for _, child := range childDies { - if child.Tag == dwarf.TagFormalParameter { - st := -1 - if vp, ok := child.Val(dwarf.AttrVarParam).(bool); ok { - if vp { - st = 2 - } else { - st = 1 - } - } - if name, ok := child.Val(dwarf.AttrName).(string); ok { - foundParams[name] = fmt.Sprintf("%d:%d", idx, st) - idx++ - } - } - } - - // Digest the result. - found := make([]string, 0, len(foundParams)) - for k, v := range foundParams { - found = append(found, fmt.Sprintf("%s:%s", k, v)) - } - sort.Strings(found) + // Call a helper to collect param info. + found := processParams(abcdie, &ex) // Make sure we see all of the expected params in the proper - // order, that they have the varparam attr, and the varparm is set - // for the returns. + // order, that they have the varparam attr, and the varparam is + // set for the returns. expected := "[c1:0:1 c2:1:1 c3:2:1 d1:3:1 d2:4:1 d3:5:1 d4:6:1 f1:7:1 f2:8:1 f3:9:1 g1:10:1 r1:11:2 r2:12:2 r3:13:2 r4:14:2 r5:15:2 r6:16:2]" - if fmt.Sprintf("%+v", found) != expected { - t.Errorf("param check failed, wanted %s got %s\n", + if found != expected { + t.Errorf("param check failed, wanted:\n%s\ngot:\n%s\n", expected, found) } } @@ -1849,3 +1868,156 @@ func main() { } } } + +func TestOptimizedOutParamHandling(t *testing.T) { + testenv.MustHaveGoBuild(t) + + if runtime.GOOS == "plan9" { + t.Skip("skipping on plan9; no DWARF symbol table in executables") + } + t.Parallel() + + // This test is intended to verify that the compiler emits DWARF + // DIE entries for all input and output parameters, and that: + // + // - attributes are set correctly for output params, + // - things appear in the proper order + // - things work properly for both register-resident + // params and params passed on the stack + // - things work for both referenced and unreferenced params + // - things work for named return values un-named return vals + // + // The scenarios below don't cover all possible permutations and + // combinations, but they hit a bunch of the high points. + + const prog = ` +package main + +// First testcase. All input params in registers, all params used. + +//go:noinline +func tc1(p1, p2 int, p3 string) (int, string) { + return p1 + p2, p3 + "foo" +} + +// Second testcase. Some params in registers, some on stack. + +//go:noinline +func tc2(p1 int, p2 [128]int, p3 string) (int, string, [128]int) { + return p1 + p2[p1], p3 + "foo", [128]int{p1} +} + +// Third testcase. Named return params. + +//go:noinline +func tc3(p1 int, p2 [128]int, p3 string) (r1 int, r2 bool, r3 string, r4 [128]int) { + if p1 == 101 { + r1 = p1 + p2[p1] + r2 = p3 == "foo" + r4 = [128]int{p1} + return + } else { + return p1 - p2[p1+3], false, "bar", [128]int{p1 + 2} + } +} + +// Fourth testcase. Some thing are used, some are unused. + +//go:noinline +func tc4(p1, p1un int, p2, p2un [128]int, p3, p3un string) (r1 int, r1un int, r2 bool, r3 string, r4, r4un [128]int) { + if p1 == 101 { + r1 = p1 + p2[p2[0]] + r2 = p3 == "foo" + r4 = [128]int{p1} + return + } else { + return p1, -1, true, "plex", [128]int{p1 + 2}, [128]int{-1} + } +} + +func main() { + { + r1, r2 := tc1(3, 4, "five") + println(r1, r2) + } + { + x := [128]int{9} + r1, r2, r3 := tc2(3, x, "five") + println(r1, r2, r3[0]) + } + { + x := [128]int{9} + r1, r2, r3, r4 := tc3(3, x, "five") + println(r1, r2, r3, r4[0]) + } + { + x := [128]int{3} + y := [128]int{7} + r1, r1u, r2, r3, r4, r4u := tc4(0, 1, x, y, "a", "b") + println(r1, r1u, r2, r3, r4[0], r4u[1]) + } + +} +` + dir := t.TempDir() + f := gobuild(t, dir, prog, DefaultOpt) + defer f.Close() + + d, err := f.DWARF() + if err != nil { + t.Fatalf("error reading DWARF: %v", err) + } + + rdr := d.Reader() + ex := examiner{} + if err := ex.populate(rdr); err != nil { + t.Fatalf("error reading DWARF: %v", err) + } + + testcases := []struct { + tag string + expected string + }{ + { + tag: "tc1", + expected: "[p1:0:1 p2:1:1 p3:2:1 ~r0:3:2 ~r1:4:2]", + }, + { + tag: "tc2", + expected: "[p1:0:1 p2:1:1 p3:2:1 ~r0:3:2 ~r1:4:2 ~r2:5:2]", + }, + { + tag: "tc3", + expected: "[p1:0:1 p2:1:1 p3:2:1 r1:3:2 r2:4:2 r3:5:2 r4:6:2]", + }, + { + tag: "tc4", + expected: "[p1:0:1 p1un:1:1 p2:2:1 p2un:3:1 p3:4:1 p3un:5:1 r1:6:2 r1un:7:2 r2:8:2 r3:9:2 r4:10:2 r4un:11:2]", + }, + } + + for _, tc := range testcases { + // Locate the proper DIE + which := fmt.Sprintf("main.%s", tc.tag) + tcs := ex.Named(which) + if len(tcs) == 0 { + t.Fatalf("unable to locate DIE for " + which) + } + if len(tcs) != 1 { + t.Fatalf("more than one " + which + " DIE") + } + die := tcs[0] + + // Vet the DIE + if die.Tag != dwarf.TagSubprogram { + t.Fatalf("unexpected tag %v on "+which+" DIE", die.Tag) + } + + // Examine params for this subprogram. + foundParams := processParams(die, &ex) + if foundParams != tc.expected { + t.Errorf("check failed for testcase %s -- wanted:\n%s\ngot:%s\n", + tc.tag, tc.expected, foundParams) + } + } +} -- 2.50.0