@@ -23,8 +23,8 @@ import (
23
23
)
24
24
25
25
// 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 () {
28
28
// Ensure that we get pc=0x%x values in the traceback.
29
29
debug .SetTraceback ("system" )
30
30
writeSentinel (os .Stdout )
@@ -37,6 +37,21 @@ func crash() {
37
37
select {}
38
38
}
39
39
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
+
40
55
func child1 () {
41
56
child2 ()
42
57
}
@@ -85,6 +100,20 @@ func child7() {
85
100
panic ("oops" )
86
101
}
87
102
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
+
88
117
// TestTracebackSystem tests that the syntax of crash reports produced
89
118
// by GOTRACEBACK=system (see traceback2) contains a complete,
90
119
// parseable list of program counters for the running goroutine that
@@ -100,46 +129,75 @@ func TestTracebackSystem(t *testing.T) {
100
129
t .Skip ("Can't read source code for this file on Android" )
101
130
}
102
131
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
+ },
120
166
}
121
167
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
+ }
127
188
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
+ })
143
201
}
144
202
}
145
203
@@ -154,6 +212,35 @@ redacted.go:0: runtime.goexit
154
212
//
155
213
// (Copied from golang.org/x/telemetry/crashmonitor.parseStackPCs.)
156
214
func 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
+
157
244
// getPC parses the PC out of a line of the form:
158
245
// \tFILE:LINE +0xRELPC sp=... fp=... pc=...
159
246
getPC := func (line string ) (uint64 , error ) {
@@ -170,6 +257,9 @@ func parseStackPCs(crash string) ([]uintptr, error) {
170
257
childSentinel = sentinel ()
171
258
on = false // are we in the first running goroutine?
172
259
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.
173
263
)
174
264
for i := 0 ; i < len (lines ); i ++ {
175
265
line := lines [i ]
@@ -212,21 +302,76 @@ func parseStackPCs(crash string) ([]uintptr, error) {
212
302
// Note: SYMBOL may contain parens "pkg.(*T).method"
213
303
// The RELPC is sometimes missing.
214
304
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
+ }
221
311
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
228
374
}
229
- pcs = append (pcs , uintptr (pc - parentSentinel + childSentinel ))
230
375
}
231
376
return pcs , nil
232
377
}
0 commit comments