// 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 "aix":
|
t.Skip("gdb does not work on AIX; see https://golang.org/issue/28558")
|
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 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)
|
}
|
|
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")
|
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{
|
"[]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")
|
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")
|
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))
|
}
|
}
|
}
|