| // 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" |
| "go/build" |
| "internal/testenv" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strconv" |
| "strings" |
| "testing" |
| ) |
| |
| 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") |
| } |
| if final := os.Getenv("GOROOT_FINAL"); final != "" && runtime.GOROOT() != 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" && testenv.Builder() != "solaris-amd64-smartosbuildlet" { |
| t.Skip("skipping gdb python tests on 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) |
| mapvar["abc"] = "def" |
| mapvar["ghi"] = "jkl" |
| strvar := "abc" |
| ptrvar := &strvar |
| slicevar := make([]string, 0, 16) |
| slicevar = append(slicevar, mapvar["abc"]) |
| fmt.Println("hi") |
| runtime.KeepAlive(ptrvar) |
| _ = ptrvar |
| gslice = slicevar |
| 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 runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" || runtime.GOARCH == "mips64" { |
| testenv.SkipFlaky(t, 18784) |
| } |
| testGdbPython(t, true) |
| } |
| |
| func testGdbPython(t *testing.T, cgo bool) { |
| if cgo && !build.Default.CgoEnabled { |
| t.Skip("skipping because cgo is not enabled") |
| } |
| |
| checkGdbEnvironment(t) |
| t.Parallel() |
| checkGdbVersion(t) |
| checkGdbPython(t) |
| |
| dir, err := ioutil.TempDir("", "go-build") |
| if err != nil { |
| t.Fatalf("failed to create temp directory: %v", err) |
| } |
| defer os.RemoveAll(dir) |
| |
| var buf bytes.Buffer |
| buf.WriteString("package main\n") |
| if cgo { |
| buf.WriteString(`import "C"` + "\n") |
| } |
| buf.WriteString(helloSource) |
| |
| src := buf.Bytes() |
| |
| err = ioutil.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(runtime.GOROOT(), "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(runtime.GOROOT(), "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", "br main.go:15", |
| "-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 strvar\n", |
| "-ex", "print strvar", |
| "-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 2 bt\n", |
| "-ex", "goroutine 2 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, _ := exec.Command("gdb", args...).CombinedOutput() |
| t.Logf("gdb output: %s\n", got) |
| |
| firstLine := bytes.SplitN(got, []byte("\n"), 2)[0] |
| 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", runtime.GOROOT()) |
| } |
| |
| _, 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) |
| } |
| |
| 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 2 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) |
| } |
| |
| btGoroutine2Re := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?runtime.+at`) |
| if bl := blocks["goroutine 2 bt"]; !btGoroutine2Re.MatchString(bl) { |
| t.Fatalf("goroutine 2 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, err := ioutil.TempDir("", "go-build") |
| if err != nil { |
| t.Fatalf("failed to create temp directory: %v", err) |
| } |
| defer os.RemoveAll(dir) |
| |
| // Build the source code. |
| src := filepath.Join(dir, "main.go") |
| err = ioutil.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(runtime.GOROOT(), "src", "runtime"), |
| "-ex", "set startup-with-shell off", |
| "-ex", "break main.eee", |
| "-ex", "run", |
| "-ex", "backtrace", |
| "-ex", "continue", |
| filepath.Join(dir, "a.exe"), |
| } |
| got, _ := exec.Command("gdb", args...).CombinedOutput() |
| |
| // 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.Errorf("could not find '%v' in backtrace", s) |
| t.Fatalf("gdb output:\n%v", string(got)) |
| } |
| } |
| } |
| |
| 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, err := ioutil.TempDir("", "go-build") |
| if err != nil { |
| t.Fatalf("failed to create temp directory: %v", err) |
| } |
| defer os.RemoveAll(dir) |
| |
| // Build the source code. |
| src := filepath.Join(dir, "main.go") |
| err = ioutil.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(runtime.GOROOT(), "src", "runtime"), |
| "-ex", "set startup-with-shell off", |
| "-ex", "break main.main", |
| "-ex", "run", |
| "-ex", "step", |
| "-ex", "info types astruct", |
| filepath.Join(dir, "a.exe"), |
| } |
| got, _ := exec.Command("gdb", args...).CombinedOutput() |
| |
| sgot := string(got) |
| |
| // Check that the backtrace matches the source code. |
| types := []string{ |
| "[]main.astruct;", |
| "bucket<string,main.astruct>;", |
| "hash<string,main.astruct>;", |
| "main.astruct;", |
| "hash<string,main.astruct> * map[string]main.astruct;", |
| } |
| for _, name := range types { |
| if !strings.Contains(sgot, name) { |
| t.Errorf("could not find %s in 'info typrs astruct' output", name) |
| t.Fatalf("gdb output:\n%v", sgot) |
| } |
| } |
| } |
| |
| 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, err := ioutil.TempDir("", "go-build") |
| if err != nil { |
| t.Fatalf("failed to create temp directory: %v", err) |
| } |
| defer os.RemoveAll(dir) |
| |
| // Build the source code. |
| src := filepath.Join(dir, "main.go") |
| err = ioutil.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(runtime.GOROOT(), "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, _ := exec.Command("gdb", args...).CombinedOutput() |
| |
| sgot := strings.ReplaceAll(string(got), "\r\n", "\n") |
| |
| t.Logf("output %q", sgot) |
| |
| 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, err := ioutil.TempDir("", "go-build") |
| if err != nil { |
| t.Fatalf("failed to create temp directory: %v", err) |
| } |
| defer os.RemoveAll(dir) |
| |
| // Build the source code. |
| src := filepath.Join(dir, "main.go") |
| err = ioutil.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(runtime.GOROOT(), "src", "runtime"), |
| "-ex", "set startup-with-shell off", |
| "-ex", "run", |
| "-ex", "backtrace", |
| filepath.Join(dir, "a.exe"), |
| } |
| got, _ := exec.Command("gdb", args...).CombinedOutput() |
| |
| // 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.Errorf("could not find '%v' in backtrace", s) |
| t.Fatalf("gdb output:\n%v", string(got)) |
| } |
| } |
| } |