Skip to content

Commit 591193b

Browse files
committed
cmd/compile: enhance debug_test for infinite loops
ssa/debug_test.go already had a step limit; this exposes it to individual tests, and it is then set low for the infinite loop tests. That however is not enough; in an infinite loop debuggers see an unchanging line number, and therefore keep trying until they see a different one. To do this, the concept of a "bogus" line number is introduced, and on output single-instruction infinite loops are detected and a hardware nop with correct line number is inserted into the loop; the branch itself receives a bogus line number. This breaks up the endless stream of same line number and causes both gdb and delve to not hang; Delve complains about the incorrect line number while gdb does a sort of odd step-to-nowhere that then steps back to the loop. Since repeats are suppressed in the reference file, a single line is shown there. (The wrong line number mentioned in previous message was an artifact of debug_test.go, not Delve, and is now fixed.) The bogus line number exposed in Delve is less than wonderful, but compared to hanging, it is better. Fixes #30664. Change-Id: I30c927cf8869a84c6c9b84033ee44d7044aab552 Reviewed-on: https://go-review.googlesource.com/c/go/+/168477 Run-TryBot: David Chase <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 3089d18 commit 591193b

File tree

7 files changed

+81
-11
lines changed

7 files changed

+81
-11
lines changed

src/cmd/compile/internal/gc/ssa.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5315,6 +5315,12 @@ func genssa(f *ssa.Func, pp *Progs) {
53155315
}
53165316
}
53175317
}
5318+
// If this is an empty infinite loop, stick a hardware NOP in there so that debuggers are less confused.
5319+
if s.bstart[b.ID] == s.pp.next && len(b.Succs) == 1 && b.Succs[0].Block() == b {
5320+
p := thearch.Ginsnop(s.pp)
5321+
p.Pos = p.Pos.WithIsStmt()
5322+
b.Pos = b.Pos.WithBogusLine() // Debuggers are not good about infinite loops, force a change in line number
5323+
}
53185324
// Emit control flow instructions for block
53195325
var next *ssa.Block
53205326
if i < len(f.Blocks)-1 && Debug['N'] == 0 {

src/cmd/compile/internal/ssa/debug_test.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,33 +156,34 @@ func TestNexting(t *testing.T) {
156156

157157
subTest(t, debugger+"-dbg-race", "i22600", dbgFlags, append(moreargs, "-race")...)
158158

159-
optSubTest(t, debugger+"-opt", "hist", optFlags, moreargs...)
160-
optSubTest(t, debugger+"-opt", "scopes", optFlags, moreargs...)
159+
optSubTest(t, debugger+"-opt", "hist", optFlags, 1000, moreargs...)
160+
optSubTest(t, debugger+"-opt", "scopes", optFlags, 1000, moreargs...)
161+
optSubTest(t, debugger+"-opt", "infloop", optFlags, 10, moreargs...)
161162
}
162163

163164
// subTest creates a subtest that compiles basename.go with the specified gcflags and additional compiler arguments,
164165
// then runs the debugger on the resulting binary, with any comment-specified actions matching tag triggered.
165166
func subTest(t *testing.T, tag string, basename string, gcflags string, moreargs ...string) {
166167
t.Run(tag+"-"+basename, func(t *testing.T) {
167-
testNexting(t, basename, tag, gcflags, moreargs...)
168+
testNexting(t, basename, tag, gcflags, 1000, moreargs...)
168169
})
169170
}
170171

171172
// optSubTest is the same as subTest except that it skips the test if the runtime and libraries
172173
// were not compiled with optimization turned on. (The skip may not be necessary with Go 1.10 and later)
173-
func optSubTest(t *testing.T, tag string, basename string, gcflags string, moreargs ...string) {
174+
func optSubTest(t *testing.T, tag string, basename string, gcflags string, count int, moreargs ...string) {
174175
// If optimized test is run with unoptimized libraries (compiled with -N -l), it is very likely to fail.
175176
// This occurs in the noopt builders (for example).
176177
t.Run(tag+"-"+basename, func(t *testing.T) {
177178
if *force || optimizedLibs {
178-
testNexting(t, basename, tag, gcflags, moreargs...)
179+
testNexting(t, basename, tag, gcflags, count, moreargs...)
179180
} else {
180181
t.Skip("skipping for unoptimized stdlib/runtime")
181182
}
182183
})
183184
}
184185

185-
func testNexting(t *testing.T, base, tag, gcflags string, moreArgs ...string) {
186+
func testNexting(t *testing.T, base, tag, gcflags string, count int, moreArgs ...string) {
186187
// (1) In testdata, build sample.go into test-sample.<tag>
187188
// (2) Run debugger gathering a history
188189
// (3) Read expected history from testdata/sample.<tag>.nexts
@@ -219,7 +220,7 @@ func testNexting(t *testing.T, base, tag, gcflags string, moreArgs ...string) {
219220
} else {
220221
dbg = newGdb(tag, exe)
221222
}
222-
h1 := runDbgr(dbg, 1000)
223+
h1 := runDbgr(dbg, count)
223224
if *dryrun {
224225
fmt.Printf("# Tag for above is %s\n", dbg.tag())
225226
return
@@ -261,6 +262,7 @@ func runDbgr(dbg dbgr, maxNext int) *nextHist {
261262
break
262263
}
263264
}
265+
dbg.quit()
264266
h := dbg.hist()
265267
return h
266268
}
@@ -298,7 +300,7 @@ func (t tstring) String() string {
298300
}
299301

300302
type pos struct {
301-
line uint16
303+
line uint32
302304
file uint8 // Artifact of plans to implement differencing instead of calling out to diff.
303305
}
304306

@@ -386,7 +388,7 @@ func (h *nextHist) add(file, line, text string) bool {
386388
}
387389
}
388390
l := len(h.ps)
389-
p := pos{line: uint16(li), file: fi}
391+
p := pos{line: uint32(li), file: fi}
390392

391393
if l == 0 || *repeats || h.ps[l-1] != p {
392394
h.ps = append(h.ps, p)
@@ -721,6 +723,15 @@ func varsToPrint(line, lookfor string) []string {
721723
func (s *gdbState) quit() {
722724
response := s.ioState.writeRead("q\n")
723725
if strings.Contains(response.o, "Quit anyway? (y or n)") {
726+
defer func() {
727+
if r := recover(); r != nil {
728+
if s, ok := r.(string); !(ok && strings.Contains(s, "'Y\n'")) {
729+
// Not the panic that was expected.
730+
fmt.Printf("Expected a broken pipe panic, but saw the following panic instead")
731+
panic(r)
732+
}
733+
}
734+
}()
724735
s.ioState.writeRead("Y\n")
725736
}
726737
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
./testdata/infloop.go
2+
6: func test() {
3+
8: go func() {}()
4+
10: for {
5+
1048575:
6+
10: for {
7+
1048575:
8+
10: for {
9+
1048575:
10+
10: for {
11+
1048575:
12+
10: for {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
src/cmd/compile/internal/ssa/testdata/infloop.go
2+
6: func test() {
3+
8: go func() {}()
4+
10: for {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package main
2+
3+
var sink int
4+
5+
//go:noinline
6+
func test() {
7+
// This is for #30167, incorrect line numbers in an infinite loop
8+
go func() {}()
9+
10+
for {
11+
}
12+
}
13+
14+
func main() {
15+
test()
16+
}

src/cmd/internal/src/pos.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ type lico uint32
304304
// TODO: Prologue and epilogue are perhaps better handled as psuedoops for the assembler,
305305
// because they have almost no interaction with other uses of the position.
306306
const (
307-
lineBits, lineMax = 20, 1<<lineBits - 1
307+
lineBits, lineMax = 20, 1<<lineBits - 2
308+
bogusLine = 1<<lineBits - 1 // Not a line number; used to disruopt infinite loops
308309
isStmtBits, isStmtMax = 2, 1<<isStmtBits - 1
309310
xlogueBits, xlogueMax = 2, 1<<xlogueBits - 1
310311
colBits, colMax = 32 - lineBits - xlogueBits - isStmtBits, 1<<colBits - 1
@@ -355,6 +356,16 @@ const (
355356
PosEpilogueBegin
356357
)
357358

359+
func makeLicoRaw(line, col uint) lico {
360+
return lico(line<<lineShift | col<<colShift)
361+
}
362+
363+
// This is a not-position that will not be elided.
364+
// Depending on the debugger (gdb or delve) it may or may not be displayed.
365+
func makeBogusLico() lico {
366+
return makeLicoRaw(bogusLine, 0).withIsStmt()
367+
}
368+
358369
func makeLico(line, col uint) lico {
359370
if line > lineMax {
360371
// cannot represent line, use max. line so we have some information
@@ -365,7 +376,7 @@ func makeLico(line, col uint) lico {
365376
col = colMax
366377
}
367378
// default is not-sure-if-statement
368-
return lico(line<<lineShift | col<<colShift)
379+
return makeLicoRaw(line, col)
369380
}
370381

371382
func (x lico) Line() uint { return uint(x) >> lineShift }

src/cmd/internal/src/xpos.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ func (p XPos) WithIsStmt() XPos {
6060
return p
6161
}
6262

63+
// WithBogusLine returns a bogus line that won't match any recorded for the source code.
64+
// Its use is to disrupt the statements within an infinite loop so that the debugger
65+
// will not itself loop infinitely waiting for the line number to change.
66+
// gdb chooses not to display the bogus line; delve shows it with a complaint, but the
67+
// alternative behavior is to hang.
68+
func (p XPos) WithBogusLine() XPos {
69+
p.lico = makeBogusLico()
70+
return p
71+
}
72+
6373
// WithXlogue returns the same location but marked with DWARF function prologue/epilogue
6474
func (p XPos) WithXlogue(x PosXlogue) XPos {
6575
p.lico = p.lico.withXlogue(x)

0 commit comments

Comments
 (0)