@@ -23,8 +23,8 @@ import (
2323)
2424
2525// This is the entrypoint of the child process used by
26- // TestTracebackSystem. It prints a crash report to stdout.
27- func crash () {
26+ // TestTracebackSystem/panic . It prints a crash report to stdout.
27+ func crashViaPanic () {
2828 // Ensure that we get pc=0x%x values in the traceback.
2929 debug .SetTraceback ("system" )
3030 writeSentinel (os .Stdout )
@@ -37,6 +37,21 @@ func crash() {
3737 select {}
3838}
3939
40+ // This is the entrypoint of the child process used by
41+ // TestTracebackSystem/trap. It prints a crash report to stdout.
42+ func crashViaTrap () {
43+ // Ensure that we get pc=0x%x values in the traceback.
44+ debug .SetTraceback ("system" )
45+ writeSentinel (os .Stdout )
46+ debug .SetCrashOutput (os .Stdout , debug.CrashOptions {})
47+
48+ go func () {
49+ // This call is typically inlined.
50+ trap1 ()
51+ }()
52+ select {}
53+ }
54+
4055func child1 () {
4156 child2 ()
4257}
@@ -85,6 +100,20 @@ func child7() {
85100 panic ("oops" )
86101}
87102
103+ func trap1 () {
104+ trap2 ()
105+ }
106+
107+ var sinkPtr * int
108+
109+ func trap2 () {
110+ trap3 (sinkPtr )
111+ }
112+
113+ func trap3 (i * int ) {
114+ * i = 42
115+ }
116+
88117// TestTracebackSystem tests that the syntax of crash reports produced
89118// by GOTRACEBACK=system (see traceback2) contains a complete,
90119// parseable list of program counters for the running goroutine that
@@ -100,46 +129,75 @@ func TestTracebackSystem(t *testing.T) {
100129 t .Skip ("Can't read source code for this file on Android" )
101130 }
102131
103- // Fork+exec the crashing process.
104- exe , err := os .Executable ()
105- if err != nil {
106- t .Fatal (err )
107- }
108- cmd := testenv .Command (t , exe )
109- cmd .Env = append (cmd .Environ (), entrypointVar + "=crash" )
110- var stdout , stderr bytes.Buffer
111- cmd .Stdout = & stdout
112- cmd .Stderr = & stderr
113- cmd .Run () // expected to crash
114- t .Logf ("stderr:\n %s\n stdout: %s\n " , stderr .Bytes (), stdout .Bytes ())
115- crash := stdout .String ()
116-
117- // If the only line is the sentinel, it wasn't a crash.
118- if strings .Count (crash , "\n " ) < 2 {
119- t .Fatalf ("child process did not produce a crash report" )
132+ tests := []struct {
133+ name string
134+ want string
135+ }{
136+ {
137+ name : "panic" ,
138+ want : `redacted.go:0: runtime.gopanic
139+ traceback_system_test.go:100: runtime_test.child7: panic("oops")
140+ traceback_system_test.go:83: runtime_test.child6: child7() // appears in stack trace
141+ traceback_system_test.go:74: runtime_test.child5: child6() // appears in stack trace
142+ traceback_system_test.go:68: runtime_test.child4: child5()
143+ traceback_system_test.go:64: runtime_test.child3: child4()
144+ traceback_system_test.go:60: runtime_test.child2: child3()
145+ traceback_system_test.go:56: runtime_test.child1: child2()
146+ traceback_system_test.go:35: runtime_test.crashViaPanic.func1: child1()
147+ redacted.go:0: runtime.goexit
148+ ` ,
149+ },
150+ {
151+ // Test panic via trap. x/telemetry is aware that trap
152+ // PCs follow runtime.sigpanic and need to be
153+ // incremented to offset the decrement done by
154+ // CallersFrames.
155+ name : "trap" ,
156+ want : `redacted.go:0: runtime.gopanic
157+ redacted.go:0: runtime.panicmem
158+ redacted.go:0: runtime.sigpanic
159+ traceback_system_test.go:114: runtime_test.trap3: *i = 42
160+ traceback_system_test.go:110: runtime_test.trap2: trap3(sinkPtr)
161+ traceback_system_test.go:104: runtime_test.trap1: trap2()
162+ traceback_system_test.go:50: runtime_test.crashViaTrap.func1: trap1()
163+ redacted.go:0: runtime.goexit
164+ ` ,
165+ },
120166 }
121167
122- // Parse the PCs out of the child's crash report.
123- pcs , err := parseStackPCs (crash )
124- if err != nil {
125- t .Fatal (err )
126- }
168+ for _ , tc := range tests {
169+ t .Run (tc .name , func (t * testing.T ) {
170+ // Fork+exec the crashing process.
171+ exe , err := os .Executable ()
172+ if err != nil {
173+ t .Fatal (err )
174+ }
175+ cmd := testenv .Command (t , exe )
176+ cmd .Env = append (cmd .Environ (), entrypointVar + "=" + tc .name )
177+ var stdout , stderr bytes.Buffer
178+ cmd .Stdout = & stdout
179+ cmd .Stderr = & stderr
180+ cmd .Run () // expected to crash
181+ t .Logf ("stderr:\n %s\n stdout: %s\n " , stderr .Bytes (), stdout .Bytes ())
182+ crash := stdout .String ()
183+
184+ // If the only line is the sentinel, it wasn't a crash.
185+ if strings .Count (crash , "\n " ) < 2 {
186+ t .Fatalf ("child process did not produce a crash report" )
187+ }
127188
128- // Unwind the stack using this executable's symbol table.
129- got := formatStack (pcs )
130- want := `redacted.go:0: runtime.gopanic
131- traceback_system_test.go:85: runtime_test.child7: panic("oops")
132- traceback_system_test.go:68: runtime_test.child6: child7() // appears in stack trace
133- traceback_system_test.go:59: runtime_test.child5: child6() // appears in stack trace
134- traceback_system_test.go:53: runtime_test.child4: child5()
135- traceback_system_test.go:49: runtime_test.child3: child4()
136- traceback_system_test.go:45: runtime_test.child2: child3()
137- traceback_system_test.go:41: runtime_test.child1: child2()
138- traceback_system_test.go:35: runtime_test.crash.func1: child1()
139- redacted.go:0: runtime.goexit
140- `
141- if strings .TrimSpace (got ) != strings .TrimSpace (want ) {
142- t .Errorf ("got:\n %swant:\n %s" , got , want )
189+ // Parse the PCs out of the child's crash report.
190+ pcs , err := parseStackPCs (crash )
191+ if err != nil {
192+ t .Fatal (err )
193+ }
194+
195+ // Unwind the stack using this executable's symbol table.
196+ got := formatStack (pcs )
197+ if strings .TrimSpace (got ) != strings .TrimSpace (tc .want ) {
198+ t .Errorf ("got:\n %swant:\n %s" , got , tc .want )
199+ }
200+ })
143201 }
144202}
145203
@@ -154,6 +212,35 @@ redacted.go:0: runtime.goexit
154212//
155213// (Copied from golang.org/x/telemetry/crashmonitor.parseStackPCs.)
156214func parseStackPCs (crash string ) ([]uintptr , error ) {
215+ // getSymbol parses the symbol name out of a line of the form:
216+ // SYMBOL(ARGS)
217+ //
218+ // Note: SYMBOL may contain parens "pkg.(*T).method". However, type
219+ // parameters are always replaced with ..., so they cannot introduce
220+ // more parens. e.g., "pkg.(*T[...]).method".
221+ //
222+ // ARGS can contain parens. We want the first paren that is not
223+ // immediately preceded by a ".".
224+ //
225+ // TODO(prattmic): This is mildly complicated and is only used to find
226+ // runtime.sigpanic, so perhaps simplify this by checking explicitly
227+ // for sigpanic.
228+ getSymbol := func (line string ) (string , error ) {
229+ var prev rune
230+ for i , c := range line {
231+ if line [i ] != '(' {
232+ prev = c
233+ continue
234+ }
235+ if prev == '.' {
236+ prev = c
237+ continue
238+ }
239+ return line [:i ], nil
240+ }
241+ return "" , fmt .Errorf ("no symbol for stack frame: %s" , line )
242+ }
243+
157244 // getPC parses the PC out of a line of the form:
158245 // \tFILE:LINE +0xRELPC sp=... fp=... pc=...
159246 getPC := func (line string ) (uint64 , error ) {
@@ -170,6 +257,9 @@ func parseStackPCs(crash string) ([]uintptr, error) {
170257 childSentinel = sentinel ()
171258 on = false // are we in the first running goroutine?
172259 lines = strings .Split (crash , "\n " )
260+ symLine = true // within a goroutine, every other line is a symbol or file/line/pc location, starting with symbol.
261+ currSymbol string
262+ prevSymbol string // symbol of the most recent previous frame with a PC.
173263 )
174264 for i := 0 ; i < len (lines ); i ++ {
175265 line := lines [i ]
@@ -212,21 +302,76 @@ func parseStackPCs(crash string) ([]uintptr, error) {
212302 // Note: SYMBOL may contain parens "pkg.(*T).method"
213303 // The RELPC is sometimes missing.
214304
215- // Skip the symbol(args) line.
216- i ++
217- if i == len ( lines ) {
218- break
219- }
220- line = lines [ i ]
305+ if symLine {
306+ var err error
307+ currSymbol , err = getSymbol ( line )
308+ if err != nil {
309+ return nil , fmt . Errorf ( "error extracting symbol: %v" , err )
310+ }
221311
222- // Parse the PC, and correct for the parent and child's
223- // different mappings of the text section.
224- pc , err := getPC (line )
225- if err != nil {
226- // Inlined frame, perhaps; skip it.
227- continue
312+ symLine = false // Next line is FILE:LINE.
313+ } else {
314+ // Parse the PC, and correct for the parent and child's
315+ // different mappings of the text section.
316+ pc , err := getPC (line )
317+ if err != nil {
318+ // Inlined frame, perhaps; skip it.
319+
320+ // Done with this frame. Next line is a new frame.
321+ //
322+ // Don't update prevSymbol; we only want to
323+ // track frames with a PC.
324+ currSymbol = ""
325+ symLine = true
326+ continue
327+ }
328+
329+ pc = pc - parentSentinel + childSentinel
330+
331+ // If the previous frame was sigpanic, then this frame
332+ // was a trap (e.g., SIGSEGV).
333+ //
334+ // Typically all middle frames are calls, and report
335+ // the "return PC". That is, the instruction following
336+ // the CALL where the callee will eventually return to.
337+ //
338+ // runtime.CallersFrames is aware of this property and
339+ // will decrement each PC by 1 to "back up" to the
340+ // location of the CALL, which is the actual line
341+ // number the user expects.
342+ //
343+ // This does not work for traps, as a trap is not a
344+ // call, so the reported PC is not the return PC, but
345+ // the actual PC of the trap.
346+ //
347+ // runtime.Callers is aware of this and will
348+ // intentionally increment trap PCs in order to correct
349+ // for the decrement performed by
350+ // runtime.CallersFrames. See runtime.tracebackPCs and
351+ // runtume.(*unwinder).symPC.
352+ //
353+ // We must emulate the same behavior, otherwise we will
354+ // report the location of the instruction immediately
355+ // prior to the trap, which may be on a different line,
356+ // or even a different inlined functions.
357+ //
358+ // TODO(prattmic): The runtime applies the same trap
359+ // behavior for other "injected calls", see injectCall
360+ // in runtime.(*unwinder).next. Do we want to handle
361+ // those as well? I don't believe we'd ever see
362+ // runtime.asyncPreempt or runtime.debugCallV2 in a
363+ // typical crash.
364+ if prevSymbol == "runtime.sigpanic" {
365+ pc ++
366+ }
367+
368+ pcs = append (pcs , uintptr (pc ))
369+
370+ // Done with this frame. Next line is a new frame.
371+ prevSymbol = currSymbol
372+ currSymbol = ""
373+ symLine = true
228374 }
229- pcs = append (pcs , uintptr (pc - parentSentinel + childSentinel ))
230375 }
231376 return pcs , nil
232377}
0 commit comments