| // 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 golang.org/issue/22893 and 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 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) | |
| } | |
| } | |
| 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") | |
| 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", | |
| } | |
| 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 fmt.Println", | |
| "-ex", "run", | |
| "-ex", "echo BEGIN info goroutines\n", | |
| "-ex", "info goroutines", | |
| "-ex", "echo END\n", | |
| "-ex", "up", // up from fmt.Println to main | |
| "-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", "down", // back to fmt.Println (goroutine 2 below only works at bottom of stack. TODO: fix that) | |
| "-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 fmt.Println", // 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(`\Q = map[string]string = {["abc"] = "def", ["ghi"] = "jkl"}\E$`) | |
| printMapvarRe2 := regexp.MustCompile(`\Q = map[string]string = {["ghi"] = "jkl", ["abc"] = "def"}\E$`) | |
| if bl := blocks["print mapvar"]; !printMapvarRe1.MatchString(bl) && | |
| !printMapvarRe2.MatchString(bl) { | |
| t.Fatalf("print mapvar failed: %s", bl) | |
| } | |
| strVarRe := regexp.MustCompile(`\Q = "abc"\E$`) | |
| 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. | |
| infoLocalsRe := regexp.MustCompile(`slicevar *= *\[\]string *= *{"def"}`) | |
| if bl := blocks["info locals"]; !infoLocalsRe.MatchString(bl) { | |
| t.Fatalf("info locals failed: %s", bl) | |
| } | |
| btGoroutine1Re := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?fmt\.Println.+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") | |
| 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) | |
| 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") | |
| 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{ | |
| "struct []main.astruct;", | |
| "struct bucket<string,main.astruct>;", | |
| "struct hash<string,main.astruct>;", | |
| "struct main.astruct;", | |
| "typedef struct 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") | |
| 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.Replace(string(got), "\r\n", "\n", -1) | |
| 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") | |
| 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)) | |
| } | |
| } | |
| } |