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) + } + } +}