Skip to content

Commit f52bede

Browse files
committed
runtime: create an API for unwinding inlined frames
We've replicated the code to expand inlined frames in many places in the runtime at this point. This CL adds a simple iterator API that abstracts this out. We also use this to try out a new idea for structuring tests of runtime internals: rather than exporting this whole internal data type and API, we write the test in package runtime and import the few bits of std we need. The idea is that, for tests of internals, it's easier to inject public APIs from std than it is to export non-public APIs from runtime. This is discussed more in #55108. For #54466. Change-Id: Iebccc04ff59a1509694a8ac0e0d3984e49121339 Reviewed-on: https://go-review.googlesource.com/c/go/+/466096 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Michael Pratt <[email protected]> Run-TryBot: Austin Clements <[email protected]>
1 parent dcb4c1c commit f52bede

File tree

7 files changed

+340
-9
lines changed

7 files changed

+340
-9
lines changed

src/runtime/import_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This file and importx_test.go make it possible to write tests in the runtime
6+
// package, which is generally more convenient for testing runtime internals.
7+
// For tests that mostly touch public APIs, it's generally easier to write them
8+
// in the runtime_test package and export any runtime internals via
9+
// export_test.go.
10+
//
11+
// There are a few limitations on runtime package tests that this bridges:
12+
//
13+
// 1. Tests use the signature "XTest<name>(t T)". Since runtime can't import
14+
// testing, test functions can't use testing.T, so instead we have the T
15+
// interface, which *testing.T satisfies. And we start names with "XTest"
16+
// because otherwise go test will complain about Test functions with the wrong
17+
// signature. To actually expose these as test functions, this file contains
18+
// trivial wrappers.
19+
//
20+
// 2. Runtime package tests can't directly import other std packages, so we
21+
// inject any necessary functions from std.
22+
23+
// TODO: Generate this
24+
25+
package runtime_test
26+
27+
import (
28+
"fmt"
29+
"internal/testenv"
30+
"runtime"
31+
"testing"
32+
)
33+
34+
func init() {
35+
runtime.FmtSprintf = fmt.Sprintf
36+
runtime.TestenvOptimizationOff = testenv.OptimizationOff
37+
}
38+
39+
func TestInlineUnwinder(t *testing.T) {
40+
runtime.XTestInlineUnwinder(t)
41+
}

src/runtime/importx_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// See import_test.go. This is the half that lives in the runtime package.
6+
7+
// TODO: Generate this
8+
9+
package runtime
10+
11+
type TestingT interface {
12+
Cleanup(func())
13+
Error(args ...any)
14+
Errorf(format string, args ...any)
15+
Fail()
16+
FailNow()
17+
Failed() bool
18+
Fatal(args ...any)
19+
Fatalf(format string, args ...any)
20+
Helper()
21+
Log(args ...any)
22+
Logf(format string, args ...any)
23+
Name() string
24+
Setenv(key, value string)
25+
Skip(args ...any)
26+
SkipNow()
27+
Skipf(format string, args ...any)
28+
Skipped() bool
29+
TempDir() string
30+
}
31+
32+
var FmtSprintf func(format string, a ...any) string
33+
var TestenvOptimizationOff func() bool

src/runtime/runtime2.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,8 @@ type _func struct {
924924
// Pseudo-Func that is returned for PCs that occur in inlined code.
925925
// A *Func can be either a *_func or a *funcinl, and they are distinguished
926926
// by the first uintptr.
927+
//
928+
// TODO(austin): Can we merge this with inlinedCall?
927929
type funcinl struct {
928930
ones uint32 // set to ^0 to distinguish from _func
929931
entry uintptr // entry of the real (the "outermost") frame

src/runtime/string.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,10 @@ func hasPrefix(s, prefix string) bool {
345345
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
346346
}
347347

348+
func hasSuffix(s, suffix string) bool {
349+
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
350+
}
351+
348352
const (
349353
maxUint64 = ^uint64(0)
350354
maxInt64 = int64(maxUint64 >> 1)

src/runtime/symtab.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,30 @@ func findfunc(pc uintptr) funcInfo {
898898
return funcInfo{(*_func)(unsafe.Pointer(&datap.pclntable[funcoff])), datap}
899899
}
900900

901+
// A srcFunc represents a logical function in the source code. This may
902+
// correspond to an actual symbol in the binary text, or it may correspond to a
903+
// source function that has been inlined.
904+
type srcFunc struct {
905+
datap *moduledata
906+
nameOff int32
907+
startLine int32
908+
funcID funcID
909+
}
910+
911+
func (f funcInfo) srcFunc() srcFunc {
912+
if !f.valid() {
913+
return srcFunc{}
914+
}
915+
return srcFunc{f.datap, f.nameOff, f.startLine, f.funcID}
916+
}
917+
918+
func (s srcFunc) name() string {
919+
if s.datap == nil {
920+
return ""
921+
}
922+
return s.datap.funcName(s.nameOff)
923+
}
924+
901925
type pcvalueCache struct {
902926
entries [2][8]pcvalueCacheEnt
903927
}
@@ -1207,12 +1231,3 @@ func stackmapdata(stkmap *stackmap, n int32) bitvector {
12071231
}
12081232
return bitvector{stkmap.nbit, addb(&stkmap.bytedata[0], uintptr(n*((stkmap.nbit+7)>>3)))}
12091233
}
1210-
1211-
// inlinedCall is the encoding of entries in the FUNCDATA_InlTree table.
1212-
type inlinedCall struct {
1213-
funcID funcID // type of the called function
1214-
_ [3]byte
1215-
nameOff int32 // offset into pclntab for name of called function
1216-
parentPc int32 // position of an instruction whose source position is the call site (offset from entry)
1217-
startLine int32 // line number of start of function (func keyword/TEXT directive)
1218-
}

src/runtime/symtabinl.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package runtime
6+
7+
// inlinedCall is the encoding of entries in the FUNCDATA_InlTree table.
8+
type inlinedCall struct {
9+
funcID funcID // type of the called function
10+
_ [3]byte
11+
nameOff int32 // offset into pclntab for name of called function
12+
parentPc int32 // position of an instruction whose source position is the call site (offset from entry)
13+
startLine int32 // line number of start of function (func keyword/TEXT directive)
14+
}
15+
16+
// An inlineUnwinder iterates over the stack of inlined calls at a PC by
17+
// decoding the inline table. The last step of iteration is always the frame of
18+
// the physical function, so there's always at least one frame.
19+
//
20+
// This is typically used as:
21+
//
22+
// for u, uf := newInlineUnwinder(...); uf.valid(); uf = u.next(uf) { ... }
23+
//
24+
// Implementation note: This is used in contexts that disallow write barriers.
25+
// Hence, the constructor returns this by value and pointer receiver methods
26+
// must not mutate pointer fields. Also, we keep the mutable state in a separate
27+
// struct mostly to keep both structs SSA-able, which generates much better
28+
// code.
29+
type inlineUnwinder struct {
30+
f funcInfo
31+
cache *pcvalueCache
32+
inlTree *[1 << 20]inlinedCall
33+
}
34+
35+
// An inlineFrame is a position in an inlineUnwinder.
36+
type inlineFrame struct {
37+
// pc is the PC giving the file/line metadata of the current frame. This is
38+
// always a "call PC" (not a "return PC"). This is 0 when the iterator is
39+
// exhausted.
40+
pc uintptr
41+
42+
// index is the index of the current record in inlTree, or -1 if we are in
43+
// the outermost function.
44+
index int32
45+
}
46+
47+
// newInlineUnwinder creates an inlineUnwinder initially set to the inner-most
48+
// inlined frame at PC. PC should be a "call PC" (not a "return PC").
49+
//
50+
// This unwinder uses non-strict handling of PC because it's assumed this is
51+
// only ever used for symbolic debugging. If things go really wrong, it'll just
52+
// fall back to the outermost frame.
53+
func newInlineUnwinder(f funcInfo, pc uintptr, cache *pcvalueCache) (inlineUnwinder, inlineFrame) {
54+
inldata := funcdata(f, _FUNCDATA_InlTree)
55+
if inldata == nil {
56+
return inlineUnwinder{f: f}, inlineFrame{pc: pc, index: -1}
57+
}
58+
inlTree := (*[1 << 20]inlinedCall)(inldata)
59+
u := inlineUnwinder{f: f, cache: cache, inlTree: inlTree}
60+
return u, u.resolveInternal(pc)
61+
}
62+
63+
func (u *inlineUnwinder) resolveInternal(pc uintptr) inlineFrame {
64+
return inlineFrame{
65+
pc: pc,
66+
// Conveniently, this returns -1 if there's an error, which is the same
67+
// value we use for the outermost frame.
68+
index: pcdatavalue1(u.f, _PCDATA_InlTreeIndex, pc, u.cache, false),
69+
}
70+
}
71+
72+
func (uf inlineFrame) valid() bool {
73+
return uf.pc != 0
74+
}
75+
76+
// next returns the frame representing uf's logical caller.
77+
func (u *inlineUnwinder) next(uf inlineFrame) inlineFrame {
78+
if uf.index < 0 {
79+
uf.pc = 0
80+
return uf
81+
}
82+
parentPc := u.inlTree[uf.index].parentPc
83+
return u.resolveInternal(u.f.entry() + uintptr(parentPc))
84+
}
85+
86+
// isInlined returns whether uf is an inlined frame.
87+
func (u *inlineUnwinder) isInlined(uf inlineFrame) bool {
88+
return uf.index >= 0
89+
}
90+
91+
// srcFunc returns the srcFunc representing the given frame.
92+
func (u *inlineUnwinder) srcFunc(uf inlineFrame) srcFunc {
93+
if uf.index < 0 {
94+
return u.f.srcFunc()
95+
}
96+
t := &u.inlTree[uf.index]
97+
return srcFunc{
98+
u.f.datap,
99+
t.nameOff,
100+
t.startLine,
101+
t.funcID,
102+
}
103+
}
104+
105+
// fileLine returns the file name and line number of the call within the given
106+
// frame. As a convenience, for the innermost frame, it returns the file and
107+
// line of the PC this unwinder was started at (often this is a call to another
108+
// physical function).
109+
//
110+
// It returns "?", 0 if something goes wrong.
111+
func (u *inlineUnwinder) fileLine(uf inlineFrame) (file string, line int) {
112+
file, line32 := funcline1(u.f, uf.pc, false)
113+
return file, int(line32)
114+
}

src/runtime/symtabinl_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package runtime
6+
7+
import (
8+
"internal/abi"
9+
"runtime/internal/sys"
10+
)
11+
12+
func XTestInlineUnwinder(t TestingT) {
13+
if TestenvOptimizationOff() {
14+
t.Skip("skipping test with inlining optimizations disabled")
15+
}
16+
17+
pc1 := abi.FuncPCABIInternal(tiuTest)
18+
f := findfunc(pc1)
19+
if !f.valid() {
20+
t.Fatalf("failed to resolve tiuTest at PC %#x", pc1)
21+
}
22+
23+
want := map[string]int{
24+
"tiuInlined1:3 tiuTest:10": 0,
25+
"tiuInlined1:3 tiuInlined2:6 tiuTest:11": 0,
26+
"tiuInlined2:7 tiuTest:11": 0,
27+
"tiuTest:12": 0,
28+
}
29+
wantStart := map[string]int{
30+
"tiuInlined1": 2,
31+
"tiuInlined2": 5,
32+
"tiuTest": 9,
33+
}
34+
35+
// Iterate over the PCs in tiuTest and walk the inline stack for each.
36+
prevStack := "x"
37+
var cache pcvalueCache
38+
for pc := pc1; pc < pc1+1024 && findfunc(pc) == f; pc += sys.PCQuantum {
39+
stack := ""
40+
u, uf := newInlineUnwinder(f, pc, &cache)
41+
if file, _ := u.fileLine(uf); file == "?" {
42+
// We're probably in the trailing function padding, where findfunc
43+
// still returns f but there's no symbolic information. Just keep
44+
// going until we definitely hit the end. If we see a "?" in the
45+
// middle of unwinding, that's a real problem.
46+
//
47+
// TODO: If we ever have function end information, use that to make
48+
// this robust.
49+
continue
50+
}
51+
for ; uf.valid(); uf = u.next(uf) {
52+
file, line := u.fileLine(uf)
53+
const wantFile = "symtabinl_test.go"
54+
if !hasSuffix(file, wantFile) {
55+
t.Errorf("tiuTest+%#x: want file ...%s, got %s", pc-pc1, wantFile, file)
56+
}
57+
58+
sf := u.srcFunc(uf)
59+
60+
name := sf.name()
61+
const namePrefix = "runtime."
62+
if hasPrefix(name, namePrefix) {
63+
name = name[len(namePrefix):]
64+
}
65+
if !hasPrefix(name, "tiu") {
66+
t.Errorf("tiuTest+%#x: unexpected function %s", pc-pc1, name)
67+
}
68+
69+
start := int(sf.startLine) - tiuStart
70+
if start != wantStart[name] {
71+
t.Errorf("tiuTest+%#x: want startLine %d, got %d", pc-pc1, wantStart[name], start)
72+
}
73+
if sf.funcID != funcID_normal {
74+
t.Errorf("tiuTest+%#x: bad funcID %v", pc-pc1, sf.funcID)
75+
}
76+
77+
if len(stack) > 0 {
78+
stack += " "
79+
}
80+
stack += FmtSprintf("%s:%d", name, line-tiuStart)
81+
}
82+
83+
if stack != prevStack {
84+
prevStack = stack
85+
86+
t.Logf("tiuTest+%#x: %s", pc-pc1, stack)
87+
88+
if _, ok := want[stack]; ok {
89+
want[stack]++
90+
}
91+
}
92+
}
93+
94+
// Check that we got all the stacks we wanted.
95+
for stack, count := range want {
96+
if count == 0 {
97+
t.Errorf("missing stack %s", stack)
98+
}
99+
}
100+
}
101+
102+
func lineNumber() int {
103+
_, _, line, _ := Caller(1)
104+
return line // return 0 for error
105+
}
106+
107+
// Below here is the test data for XTestInlineUnwinder
108+
109+
var tiuStart = lineNumber() // +0
110+
var tiu1, tiu2, tiu3 int // +1
111+
func tiuInlined1() { // +2
112+
tiu1++ // +3
113+
} // +4
114+
func tiuInlined2() { // +5
115+
tiuInlined1() // +6
116+
tiu2++ // +7
117+
} // +8
118+
func tiuTest() { // +9
119+
tiuInlined1() // +10
120+
tiuInlined2() // +11
121+
tiu3++ // +12
122+
} // +13

0 commit comments

Comments
 (0)