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 <thanm@google.com>
Run-TryBot: Than McIntosh <thanm@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: David Chase <drchase@google.com>
This commit is contained in:
Than McIntosh 2021-11-10 10:44:00 -05:00
parent e9ef931e06
commit 79e03a9281
4 changed files with 268 additions and 70 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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 <order>:<varparam>
// where order is the order within the child DIE list of the
// param, and <varparam> 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 <order>:<varparam>
// where order is the order within the child DIE list of the param,
// and <varparam> 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)
}
}
}