// Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package runtime_test import ( "bytes" "fmt" "internal/testenv" "os" "os/exec" "path/filepath" "regexp" "runtime" "strconv" "strings" "testing" ) // NOTE: In some configurations, GDB will segfault when sent a SIGWINCH signal. // Some runtime tests send SIGWINCH to the entire process group, so those tests // must never run in parallel with GDB tests. // // See issue 39021 and https://sourceware.org/bugzilla/show_bug.cgi?id=26056. func checkGdbEnvironment(t *testing.T) { testenv.MustHaveGoBuild(t) switch runtime.GOOS { case "darwin": t.Skip("gdb does not work on darwin") case "netbsd": t.Skip("gdb does not work with threads on NetBSD; see https://golang.org/issue/22893 and https://gnats.netbsd.org/52548") case "windows": t.Skip("gdb tests fail on Windows: https://golang.org/issue/22687") case "linux": if runtime.GOARCH == "ppc64" { t.Skip("skipping gdb tests on linux/ppc64; see https://golang.org/issue/17366") } if runtime.GOARCH == "mips" { t.Skip("skipping gdb tests on linux/mips; see https://golang.org/issue/25939") } case "freebsd": t.Skip("skipping gdb tests on FreeBSD; see https://golang.org/issue/29508") case "aix": if testing.Short() { t.Skip("skipping gdb tests on AIX; see https://golang.org/issue/35710") } case "plan9": t.Skip("there is no gdb on Plan 9") } if final := os.Getenv("GOROOT_FINAL"); final != "" && testenv.GOROOT(t) != final { t.Skip("gdb test can fail with GOROOT_FINAL pending") } } func checkGdbVersion(t *testing.T) { // Issue 11214 reports various failures with older versions of gdb. out, err := exec.Command("gdb", "--version").CombinedOutput() if err != nil { t.Skipf("skipping: error executing gdb: %v", err) } re := regexp.MustCompile(`([0-9]+)\.([0-9]+)`) matches := re.FindSubmatch(out) if len(matches) < 3 { t.Skipf("skipping: can't determine gdb version from\n%s\n", out) } major, err1 := strconv.Atoi(string(matches[1])) minor, err2 := strconv.Atoi(string(matches[2])) if err1 != nil || err2 != nil { t.Skipf("skipping: can't determine gdb version: %v, %v", err1, err2) } if major < 7 || (major == 7 && minor < 7) { t.Skipf("skipping: gdb version %d.%d too old", major, minor) } t.Logf("gdb version %d.%d", major, minor) } func checkGdbPython(t *testing.T) { if runtime.GOOS == "solaris" || runtime.GOOS == "illumos" { t.Skip("skipping gdb python tests on illumos and solaris; see golang.org/issue/20821") } cmd := exec.Command("gdb", "-nx", "-q", "--batch", "-iex", "python import sys; print('go gdb python support')") out, err := cmd.CombinedOutput() if err != nil { t.Skipf("skipping due to issue running gdb: %v", err) } if strings.TrimSpace(string(out)) != "go gdb python support" { t.Skipf("skipping due to lack of python gdb support: %s", out) } } // checkCleanBacktrace checks that the given backtrace is well formed and does // not contain any error messages from GDB. func checkCleanBacktrace(t *testing.T, backtrace string) { backtrace = strings.TrimSpace(backtrace) lines := strings.Split(backtrace, "\n") if len(lines) == 0 { t.Fatalf("empty backtrace") } for i, l := range lines { if !strings.HasPrefix(l, fmt.Sprintf("#%v ", i)) { t.Fatalf("malformed backtrace at line %v: %v", i, l) } } // TODO(mundaym): check for unknown frames (e.g. "??"). } const helloSource = ` import "fmt" import "runtime" var gslice []string func main() { mapvar := make(map[string]string, 13) slicemap := make(map[string][]string,11) chanint := make(chan int, 10) chanstr := make(chan string, 10) chanint <- 99 chanint <- 11 chanstr <- "spongepants" chanstr <- "squarebob" mapvar["abc"] = "def" mapvar["ghi"] = "jkl" slicemap["a"] = []string{"b","c","d"} slicemap["e"] = []string{"f","g","h"} strvar := "abc" ptrvar := &strvar slicevar := make([]string, 0, 16) slicevar = append(slicevar, mapvar["abc"]) fmt.Println("hi") runtime.KeepAlive(ptrvar) _ = ptrvar // set breakpoint here gslice = slicevar fmt.Printf("%v, %v, %v\n", slicemap, <-chanint, <-chanstr) runtime.KeepAlive(mapvar) } // END_OF_PROGRAM ` func lastLine(src []byte) int { eop := []byte("END_OF_PROGRAM") for i, l := range bytes.Split(src, []byte("\n")) { if bytes.Contains(l, eop) { return i } } return 0 } func TestGdbPython(t *testing.T) { testGdbPython(t, false) } func TestGdbPythonCgo(t *testing.T) { if strings.HasPrefix(runtime.GOARCH, "mips") { testenv.SkipFlaky(t, 37794) } testGdbPython(t, true) } func testGdbPython(t *testing.T, cgo bool) { if cgo { testenv.MustHaveCGO(t) } checkGdbEnvironment(t) t.Parallel() checkGdbVersion(t) checkGdbPython(t) dir := t.TempDir() var buf bytes.Buffer buf.WriteString("package main\n") if cgo { buf.WriteString(`import "C"` + "\n") } buf.WriteString(helloSource) src := buf.Bytes() // Locate breakpoint line var bp int lines := bytes.Split(src, []byte("\n")) for i, line := range lines { if bytes.Contains(line, []byte("breakpoint")) { bp = i break } } err := os.WriteFile(filepath.Join(dir, "main.go"), src, 0644) if err != nil { t.Fatalf("failed to create file: %v", err) } nLines := lastLine(src) cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go") cmd.Dir = dir out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() if err != nil { t.Fatalf("building source %v\n%s", err, out) } args := []string{"-nx", "-q", "--batch", "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), "-ex", "set startup-with-shell off", "-ex", "set print thread-events off", } if cgo { // When we build the cgo version of the program, the system's // linker is used. Some external linkers, like GNU gold, // compress the .debug_gdb_scripts into .zdebug_gdb_scripts. // Until gold and gdb can work together, temporarily load the // python script directly. args = append(args, "-ex", "source "+filepath.Join(testenv.GOROOT(t), "src", "runtime", "runtime-gdb.py"), ) } else { args = append(args, "-ex", "info auto-load python-scripts", ) } args = append(args, "-ex", "set python print-stack full", "-ex", fmt.Sprintf("br main.go:%d", bp), "-ex", "run", "-ex", "echo BEGIN info goroutines\n", "-ex", "info goroutines", "-ex", "echo END\n", "-ex", "echo BEGIN print mapvar\n", "-ex", "print mapvar", "-ex", "echo END\n", "-ex", "echo BEGIN print slicemap\n", "-ex", "print slicemap", "-ex", "echo END\n", "-ex", "echo BEGIN print strvar\n", "-ex", "print strvar", "-ex", "echo END\n", "-ex", "echo BEGIN print chanint\n", "-ex", "print chanint", "-ex", "echo END\n", "-ex", "echo BEGIN print chanstr\n", "-ex", "print chanstr", "-ex", "echo END\n", "-ex", "echo BEGIN info locals\n", "-ex", "info locals", "-ex", "echo END\n", "-ex", "echo BEGIN goroutine 1 bt\n", "-ex", "goroutine 1 bt", "-ex", "echo END\n", "-ex", "echo BEGIN goroutine all bt\n", "-ex", "goroutine all bt", "-ex", "echo END\n", "-ex", "clear main.go:15", // clear the previous break point "-ex", fmt.Sprintf("br main.go:%d", nLines), // new break point at the end of main "-ex", "c", "-ex", "echo BEGIN goroutine 1 bt at the end\n", "-ex", "goroutine 1 bt", "-ex", "echo END\n", filepath.Join(dir, "a.exe"), ) got, err := exec.Command("gdb", args...).CombinedOutput() t.Logf("gdb output:\n%s", got) if err != nil { t.Fatalf("gdb exited with error: %v", err) } firstLine, _, _ := bytes.Cut(got, []byte("\n")) if string(firstLine) != "Loading Go Runtime support." { // This can happen when using all.bash with // GOROOT_FINAL set, because the tests are run before // the final installation of the files. cmd := exec.Command(testenv.GoToolPath(t), "env", "GOROOT") cmd.Env = []string{} out, err := cmd.CombinedOutput() if err != nil && bytes.Contains(out, []byte("cannot find GOROOT")) { t.Skipf("skipping because GOROOT=%s does not exist", testenv.GOROOT(t)) } _, file, _, _ := runtime.Caller(1) t.Logf("package testing source file: %s", file) t.Fatalf("failed to load Go runtime support: %s\n%s", firstLine, got) } // Extract named BEGIN...END blocks from output partRe := regexp.MustCompile(`(?ms)^BEGIN ([^\n]*)\n(.*?)\nEND`) blocks := map[string]string{} for _, subs := range partRe.FindAllSubmatch(got, -1) { blocks[string(subs[1])] = string(subs[2]) } infoGoroutinesRe := regexp.MustCompile(`\*\s+\d+\s+running\s+`) if bl := blocks["info goroutines"]; !infoGoroutinesRe.MatchString(bl) { t.Fatalf("info goroutines failed: %s", bl) } printMapvarRe1 := regexp.MustCompile(`^\$[0-9]+ = map\[string\]string = {\[(0x[0-9a-f]+\s+)?"abc"\] = (0x[0-9a-f]+\s+)?"def", \[(0x[0-9a-f]+\s+)?"ghi"\] = (0x[0-9a-f]+\s+)?"jkl"}$`) printMapvarRe2 := regexp.MustCompile(`^\$[0-9]+ = map\[string\]string = {\[(0x[0-9a-f]+\s+)?"ghi"\] = (0x[0-9a-f]+\s+)?"jkl", \[(0x[0-9a-f]+\s+)?"abc"\] = (0x[0-9a-f]+\s+)?"def"}$`) if bl := blocks["print mapvar"]; !printMapvarRe1.MatchString(bl) && !printMapvarRe2.MatchString(bl) { t.Fatalf("print mapvar failed: %s", bl) } // 2 orders, and possible differences in spacing. sliceMapSfx1 := `map[string][]string = {["e"] = []string = {"f", "g", "h"}, ["a"] = []string = {"b", "c", "d"}}` sliceMapSfx2 := `map[string][]string = {["a"] = []string = {"b", "c", "d"}, ["e"] = []string = {"f", "g", "h"}}` if bl := strings.ReplaceAll(blocks["print slicemap"], " ", " "); !strings.HasSuffix(bl, sliceMapSfx1) && !strings.HasSuffix(bl, sliceMapSfx2) { t.Fatalf("print slicemap failed: %s", bl) } chanIntSfx := `chan int = {99, 11}` if bl := strings.ReplaceAll(blocks["print chanint"], " ", " "); !strings.HasSuffix(bl, chanIntSfx) { t.Fatalf("print chanint failed: %s", bl) } chanStrSfx := `chan string = {"spongepants", "squarebob"}` if bl := strings.ReplaceAll(blocks["print chanstr"], " ", " "); !strings.HasSuffix(bl, chanStrSfx) { t.Fatalf("print chanstr failed: %s", bl) } strVarRe := regexp.MustCompile(`^\$[0-9]+ = (0x[0-9a-f]+\s+)?"abc"$`) if bl := blocks["print strvar"]; !strVarRe.MatchString(bl) { t.Fatalf("print strvar failed: %s", bl) } // The exact format of composite values has changed over time. // For issue 16338: ssa decompose phase split a slice into // a collection of scalar vars holding its fields. In such cases // the DWARF variable location expression should be of the // form "var.field" and not just "field". // However, the newer dwarf location list code reconstituted // aggregates from their fields and reverted their printing // back to its original form. // Only test that all variables are listed in 'info locals' since // different versions of gdb print variables in different // order and with differing amount of information and formats. if bl := blocks["info locals"]; !strings.Contains(bl, "slicevar") || !strings.Contains(bl, "mapvar") || !strings.Contains(bl, "strvar") { t.Fatalf("info locals failed: %s", bl) } // Check that the backtraces are well formed. checkCleanBacktrace(t, blocks["goroutine 1 bt"]) checkCleanBacktrace(t, blocks["goroutine 1 bt at the end"]) btGoroutine1Re := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?main\.main.+at`) if bl := blocks["goroutine 1 bt"]; !btGoroutine1Re.MatchString(bl) { t.Fatalf("goroutine 1 bt failed: %s", bl) } if bl := blocks["goroutine all bt"]; !btGoroutine1Re.MatchString(bl) { t.Fatalf("goroutine all bt failed: %s", bl) } btGoroutine1AtTheEndRe := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?main\.main.+at`) if bl := blocks["goroutine 1 bt at the end"]; !btGoroutine1AtTheEndRe.MatchString(bl) { t.Fatalf("goroutine 1 bt at the end failed: %s", bl) } } const backtraceSource = ` package main //go:noinline func aaa() bool { return bbb() } //go:noinline func bbb() bool { return ccc() } //go:noinline func ccc() bool { return ddd() } //go:noinline func ddd() bool { return f() } //go:noinline func eee() bool { return true } var f = eee func main() { _ = aaa() } ` // TestGdbBacktrace tests that gdb can unwind the stack correctly // using only the DWARF debug info. func TestGdbBacktrace(t *testing.T) { if runtime.GOOS == "netbsd" { testenv.SkipFlaky(t, 15603) } checkGdbEnvironment(t) t.Parallel() checkGdbVersion(t) dir := t.TempDir() // Build the source code. src := filepath.Join(dir, "main.go") err := os.WriteFile(src, []byte(backtraceSource), 0644) if err != nil { t.Fatalf("failed to create file: %v", err) } cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go") cmd.Dir = dir out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() if err != nil { t.Fatalf("building source %v\n%s", err, out) } // Execute gdb commands. args := []string{"-nx", "-batch", "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), "-ex", "set startup-with-shell off", "-ex", "break main.eee", "-ex", "run", "-ex", "backtrace", "-ex", "continue", filepath.Join(dir, "a.exe"), } got, err := testenv.RunWithTimeout(t, exec.Command("gdb", args...)) t.Logf("gdb output:\n%s", got) if err != nil { if bytes.Contains(got, []byte("internal-error: wait returned unexpected status 0x0")) { // GDB bug: https://sourceware.org/bugzilla/show_bug.cgi?id=28551 testenv.SkipFlaky(t, 43068) } if bytes.Contains(got, []byte("Couldn't get registers: No such process.")) { // GDB bug: https://sourceware.org/bugzilla/show_bug.cgi?id=9086 testenv.SkipFlaky(t, 50838) } if bytes.Contains(got, []byte(" exited normally]\n")) { // GDB bug: Sometimes the inferior exits fine, // but then GDB hangs. testenv.SkipFlaky(t, 37405) } t.Fatalf("gdb exited with error: %v", err) } // Check that the backtrace matches the source code. bt := []string{ "eee", "ddd", "ccc", "bbb", "aaa", "main", } for i, name := range bt { s := fmt.Sprintf("#%v.*main\\.%v", i, name) re := regexp.MustCompile(s) if found := re.Find(got) != nil; !found { t.Fatalf("could not find '%v' in backtrace", s) } } } const autotmpTypeSource = ` package main type astruct struct { a, b int } func main() { var iface interface{} = map[string]astruct{} var iface2 interface{} = []astruct{} println(iface, iface2) } ` // TestGdbAutotmpTypes ensures that types of autotmp variables appear in .debug_info // See bug #17830. func TestGdbAutotmpTypes(t *testing.T) { checkGdbEnvironment(t) t.Parallel() checkGdbVersion(t) if runtime.GOOS == "aix" && testing.Short() { t.Skip("TestGdbAutotmpTypes is too slow on aix/ppc64") } dir := t.TempDir() // Build the source code. src := filepath.Join(dir, "main.go") err := os.WriteFile(src, []byte(autotmpTypeSource), 0644) if err != nil { t.Fatalf("failed to create file: %v", err) } cmd := exec.Command(testenv.GoToolPath(t), "build", "-gcflags=all=-N -l", "-o", "a.exe", "main.go") cmd.Dir = dir out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() if err != nil { t.Fatalf("building source %v\n%s", err, out) } // Execute gdb commands. args := []string{"-nx", "-batch", "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), "-ex", "set startup-with-shell off", // Some gdb may set scheduling-locking as "step" by default. This prevents background tasks // (e.g GC) from completing which may result in a hang when executing the step command. // See #49852. "-ex", "set scheduler-locking off", "-ex", "break main.main", "-ex", "run", "-ex", "step", "-ex", "info types astruct", filepath.Join(dir, "a.exe"), } got, err := exec.Command("gdb", args...).CombinedOutput() t.Logf("gdb output:\n%s", got) if err != nil { t.Fatalf("gdb exited with error: %v", err) } sgot := string(got) // Check that the backtrace matches the source code. types := []string{ "[]main.astruct;", "bucket;", "hash;", "main.astruct;", "hash * map[string]main.astruct;", } for _, name := range types { if !strings.Contains(sgot, name) { t.Fatalf("could not find %s in 'info typrs astruct' output", name) } } } const constsSource = ` package main const aConstant int = 42 const largeConstant uint64 = ^uint64(0) const minusOne int64 = -1 func main() { println("hello world") } ` func TestGdbConst(t *testing.T) { checkGdbEnvironment(t) t.Parallel() checkGdbVersion(t) dir := t.TempDir() // Build the source code. src := filepath.Join(dir, "main.go") err := os.WriteFile(src, []byte(constsSource), 0644) if err != nil { t.Fatalf("failed to create file: %v", err) } cmd := exec.Command(testenv.GoToolPath(t), "build", "-gcflags=all=-N -l", "-o", "a.exe", "main.go") cmd.Dir = dir out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() if err != nil { t.Fatalf("building source %v\n%s", err, out) } // Execute gdb commands. args := []string{"-nx", "-batch", "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), "-ex", "set startup-with-shell off", "-ex", "break main.main", "-ex", "run", "-ex", "print main.aConstant", "-ex", "print main.largeConstant", "-ex", "print main.minusOne", "-ex", "print 'runtime.mSpanInUse'", "-ex", "print 'runtime._PageSize'", filepath.Join(dir, "a.exe"), } got, err := exec.Command("gdb", args...).CombinedOutput() t.Logf("gdb output:\n%s", got) if err != nil { t.Fatalf("gdb exited with error: %v", err) } sgot := strings.ReplaceAll(string(got), "\r\n", "\n") if !strings.Contains(sgot, "\n$1 = 42\n$2 = 18446744073709551615\n$3 = -1\n$4 = 1 '\\001'\n$5 = 8192") { t.Fatalf("output mismatch") } } const panicSource = ` package main import "runtime/debug" func main() { debug.SetTraceback("crash") crash() } func crash() { panic("panic!") } ` // TestGdbPanic tests that gdb can unwind the stack correctly // from SIGABRTs from Go panics. func TestGdbPanic(t *testing.T) { checkGdbEnvironment(t) t.Parallel() checkGdbVersion(t) dir := t.TempDir() // Build the source code. src := filepath.Join(dir, "main.go") err := os.WriteFile(src, []byte(panicSource), 0644) if err != nil { t.Fatalf("failed to create file: %v", err) } cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go") cmd.Dir = dir out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() if err != nil { t.Fatalf("building source %v\n%s", err, out) } // Execute gdb commands. args := []string{"-nx", "-batch", "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), "-ex", "set startup-with-shell off", "-ex", "run", "-ex", "backtrace", filepath.Join(dir, "a.exe"), } got, err := exec.Command("gdb", args...).CombinedOutput() t.Logf("gdb output:\n%s", got) if err != nil { t.Fatalf("gdb exited with error: %v", err) } // Check that the backtrace matches the source code. bt := []string{ `crash`, `main`, } for _, name := range bt { s := fmt.Sprintf("(#.* .* in )?main\\.%v", name) re := regexp.MustCompile(s) if found := re.Find(got) != nil; !found { t.Fatalf("could not find '%v' in backtrace", s) } } } const InfCallstackSource = ` package main import "C" import "time" func loop() { for i := 0; i < 1000; i++ { time.Sleep(time.Millisecond*5) } } func main() { go loop() time.Sleep(time.Second * 1) } ` // TestGdbInfCallstack tests that gdb can unwind the callstack of cgo programs // on arm64 platforms without endless frames of function 'crossfunc1'. // https://golang.org/issue/37238 func TestGdbInfCallstack(t *testing.T) { checkGdbEnvironment(t) testenv.MustHaveCGO(t) if runtime.GOARCH != "arm64" { t.Skip("skipping infinite callstack test on non-arm64 arches") } t.Parallel() checkGdbVersion(t) dir := t.TempDir() // Build the source code. src := filepath.Join(dir, "main.go") err := os.WriteFile(src, []byte(InfCallstackSource), 0644) if err != nil { t.Fatalf("failed to create file: %v", err) } cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go") cmd.Dir = dir out, err := testenv.CleanCmdEnv(cmd).CombinedOutput() if err != nil { t.Fatalf("building source %v\n%s", err, out) } // Execute gdb commands. // 'setg_gcc' is the first point where we can reproduce the issue with just one 'run' command. args := []string{"-nx", "-batch", "-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"), "-ex", "set startup-with-shell off", "-ex", "break setg_gcc", "-ex", "run", "-ex", "backtrace 3", "-ex", "disable 1", "-ex", "continue", filepath.Join(dir, "a.exe"), } got, err := exec.Command("gdb", args...).CombinedOutput() t.Logf("gdb output:\n%s", got) if err != nil { t.Fatalf("gdb exited with error: %v", err) } // Check that the backtrace matches // We check the 3 inner most frames only as they are present certainly, according to gcc__arm64.c bt := []string{ `setg_gcc`, `crosscall1`, `threadentry`, } for i, name := range bt { s := fmt.Sprintf("#%v.*%v", i, name) re := regexp.MustCompile(s) if found := re.Find(got) != nil; !found { t.Fatalf("could not find '%v' in backtrace", s) } } }