Skip to content

Commit ee97216

Browse files
committed
runtime: handle inlined calls in runtime.Callers
The `skip` argument passed to runtime.Caller and runtime.Callers should be interpreted as the number of logical calls to skip (rather than the number of physical stack frames to skip). This changes runtime.Callers to skip inlined calls in addition to physical stack frames. The result value of runtime.Callers is a slice of program counters ([]uintptr) representing physical stack frames. If the `skip` parameter to runtime.Callers skips part-way into a physical frame, there is no convenient way to encode that in the resulting slice. To avoid changing the API in an incompatible way, our solution is to store the number of skipped logical calls of the first frame in the _second_ uintptr returned by runtime.Callers. Since this number is a small integer, we encode it as a valid PC value into a small symbol called: runtime.skipPleaseUseCallersFrames For example, if f() calls g(), g() calls `runtime.Callers(2, pcs)`, and g() is inlined into f, then the frame for f will be partially skipped, resulting in the following slice: pcs = []uintptr{pc_in_f, runtime.skipPleaseUseCallersFrames+1, ...} We store the skip PC in pcs[1] instead of pcs[0] so that `pcs[i:]` will truncate the captured stack trace rather than grow it for all i. Updates #19348. Change-Id: I1c56f89ac48c29e6f52a5d085567c6d77d499cf1 Reviewed-on: https://go-review.googlesource.com/37854 Run-TryBot: David Lazar <lazard@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Austin Clements <austin@google.com>
1 parent f3f5b10 commit ee97216

File tree

3 files changed

+153
-8
lines changed

3 files changed

+153
-8
lines changed

src/runtime/asm.s

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,24 @@ GLOBL runtime·no_pointers_stackmap(SB),RODATA, $8
1414

1515
GLOBL runtime·mheap_(SB), NOPTR, $0
1616
GLOBL runtime·memstats(SB), NOPTR, $0
17+
18+
// NaCl requires that these skips be verifiable machine code.
19+
#ifdef GOARCH_amd64
20+
#define SKIP4 BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90
21+
#endif
22+
#ifdef GOARCH_386
23+
#define SKIP4 BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90
24+
#endif
25+
#ifdef GOARCH_amd64p32
26+
#define SKIP4 BYTE $0x90; BYTE $0x90; BYTE $0x90; BYTE $0x90
27+
#endif
28+
#ifndef SKIP4
29+
#define SKIP4 WORD $0
30+
#endif
31+
32+
#define SKIP16 SKIP4; SKIP4; SKIP4; SKIP4
33+
#define SKIP64 SKIP16; SKIP16; SKIP16; SKIP16
34+
35+
// This function must be sizeofSkipFunction bytes.
36+
TEXT runtime·skipPleaseUseCallersFrames(SB),NOSPLIT,$0-0
37+
SKIP64; SKIP64; SKIP64; SKIP64

src/runtime/traceback.go

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,25 @@ func tracebackdefers(gp *g, callback func(*stkframe, unsafe.Pointer) bool, v uns
114114
}
115115
}
116116

117+
const sizeofSkipFunction = 256
118+
119+
// This function is defined in asm.s to be sizeofSkipFunction bytes long.
120+
func skipPleaseUseCallersFrames()
121+
117122
// Generic traceback. Handles runtime stack prints (pcbuf == nil),
118123
// the runtime.Callers function (pcbuf != nil), as well as the garbage
119124
// collector (callback != nil). A little clunky to merge these, but avoids
120125
// duplicating the code and all its subtlety.
126+
//
127+
// The skip argument is only valid with pcbuf != nil and counts the number
128+
// of logical frames to skip rather than physical frames (with inlining, a
129+
// PC in pcbuf can represent multiple calls). If a PC is partially skipped
130+
// and max > 1, pcbuf[1] will be runtime.skipPleaseUseCallersFrames+N where
131+
// N indicates the number of logical frames to skip in pcbuf[0].
121132
func gentraceback(pc0, sp0, lr0 uintptr, gp *g, skip int, pcbuf *uintptr, max int, callback func(*stkframe, unsafe.Pointer) bool, v unsafe.Pointer, flags uint) int {
133+
if skip > 0 && callback != nil {
134+
throw("gentraceback callback cannot be used with non-zero skip")
135+
}
122136
if goexitPC == 0 {
123137
throw("gentraceback before goexitPC initialization")
124138
}
@@ -318,19 +332,57 @@ func gentraceback(pc0, sp0, lr0 uintptr, gp *g, skip int, pcbuf *uintptr, max in
318332
_defer = _defer.link
319333
}
320334

321-
if skip > 0 {
322-
skip--
323-
goto skipped
324-
}
325-
326-
if pcbuf != nil {
327-
(*[1 << 20]uintptr)(unsafe.Pointer(pcbuf))[n] = frame.pc
328-
}
329335
if callback != nil {
330336
if !callback((*stkframe)(noescape(unsafe.Pointer(&frame))), v) {
331337
return n
332338
}
333339
}
340+
341+
if pcbuf != nil {
342+
if skip == 0 {
343+
(*[1 << 20]uintptr)(unsafe.Pointer(pcbuf))[n] = frame.pc
344+
} else {
345+
// backup to CALL instruction to read inlining info (same logic as below)
346+
tracepc := frame.pc
347+
if (n > 0 || flags&_TraceTrap == 0) && frame.pc > f.entry && !waspanic {
348+
tracepc--
349+
}
350+
inldata := funcdata(f, _FUNCDATA_InlTree)
351+
352+
// no inlining info, skip the physical frame
353+
if inldata == nil {
354+
skip--
355+
goto skipped
356+
}
357+
358+
ix := pcdatavalue(f, _PCDATA_InlTreeIndex, tracepc, &cache)
359+
inltree := (*[1 << 20]inlinedCall)(inldata)
360+
// skip the logical (inlined) frames
361+
logicalSkipped := 0
362+
for ix >= 0 && skip > 0 {
363+
skip--
364+
logicalSkipped++
365+
ix = inltree[ix].parent
366+
}
367+
368+
// skip the physical frame if there's more to skip
369+
if skip > 0 {
370+
skip--
371+
goto skipped
372+
}
373+
374+
// now we have a partially skipped frame
375+
(*[1 << 20]uintptr)(unsafe.Pointer(pcbuf))[n] = frame.pc
376+
377+
// if there's room, pcbuf[1] is a skip PC that encodes the number of skipped frames in pcbuf[0]
378+
if n+1 < max {
379+
n++
380+
skipPC := funcPC(skipPleaseUseCallersFrames) + uintptr(logicalSkipped)
381+
(*[1 << 20]uintptr)(unsafe.Pointer(pcbuf))[n] = skipPC
382+
}
383+
}
384+
}
385+
334386
if printing {
335387
// assume skip=0 for printing
336388
if (flags&_TraceRuntimeFrames) != 0 || showframe(f, gp, nprint == 0) {

test/inline_callers.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// run -gcflags -l=4
2+
3+
// Copyright 2017 The Go Authors. All rights reserved.
4+
// Use of this source code is governed by a BSD-style
5+
// license that can be found in the LICENSE file.
6+
7+
package main
8+
9+
import (
10+
"log"
11+
"runtime"
12+
)
13+
14+
var skip int
15+
var npcs int
16+
var pcs = make([]uintptr, 32)
17+
18+
func f() {
19+
g()
20+
}
21+
22+
func g() {
23+
h()
24+
}
25+
26+
func h() {
27+
npcs = runtime.Callers(skip, pcs)
28+
}
29+
30+
func testCallers(skp int) (frames []string) {
31+
skip = skp
32+
f()
33+
for i := 0; i < npcs; i++ {
34+
fn := runtime.FuncForPC(pcs[i])
35+
frames = append(frames, fn.Name())
36+
if fn.Name() == "main.main" {
37+
break
38+
}
39+
}
40+
return
41+
}
42+
43+
var expectedFrames [][]string = [][]string{
44+
0: {"runtime.Callers", "main.testCallers", "main.main"},
45+
1: {"main.testCallers", "main.main"},
46+
2: {"main.testCallers", "runtime.skipPleaseUseCallersFrames", "main.main"},
47+
3: {"main.testCallers", "runtime.skipPleaseUseCallersFrames", "main.main"},
48+
4: {"main.testCallers", "runtime.skipPleaseUseCallersFrames", "main.main"},
49+
5: {"main.main"},
50+
}
51+
52+
func same(xs, ys []string) bool {
53+
if len(xs) != len(ys) {
54+
return false
55+
}
56+
for i := range xs {
57+
if xs[i] != ys[i] {
58+
return false
59+
}
60+
}
61+
return true
62+
}
63+
64+
func main() {
65+
for i := 0; i <= 5; i++ {
66+
frames := testCallers(i)
67+
expected := expectedFrames[i]
68+
if !same(frames, expected) {
69+
log.Fatalf("testCallers(%d):\n got %v\n want %v", i, frames, expected)
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)