...
Run Format

Source file src/runtime/runtime-gdb_test.go

Documentation: runtime

  // 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")
  		}
  	}
  	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))
  		}
  	}
  }
  

View as plain text