...
Run Format

Source file src/runtime/runtime-gdb_test.go

Documentation: runtime

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package runtime_test
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"go/build"
    11  	"internal/testenv"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"regexp"
    17  	"runtime"
    18  	"strconv"
    19  	"strings"
    20  	"testing"
    21  )
    22  
    23  func checkGdbEnvironment(t *testing.T) {
    24  	testenv.MustHaveGoBuild(t)
    25  	switch runtime.GOOS {
    26  	case "darwin":
    27  		t.Skip("gdb does not work on darwin")
    28  	case "netbsd":
    29  		t.Skip("gdb does not work with threads on NetBSD; see https://golang.org/issue/22893 and https://gnats.netbsd.org/52548")
    30  	case "windows":
    31  		t.Skip("gdb tests fail on Windows: https://golang.org/issue/22687")
    32  	case "linux":
    33  		if runtime.GOARCH == "ppc64" {
    34  			t.Skip("skipping gdb tests on linux/ppc64; see https://golang.org/issue/17366")
    35  		}
    36  		if runtime.GOARCH == "mips" {
    37  			t.Skip("skipping gdb tests on linux/mips; see https://golang.org/issue/25939")
    38  		}
    39  	case "aix":
    40  		t.Skip("gdb does not work on AIX; see https://golang.org/issue/28558")
    41  	case "freebsd":
    42  		t.Skip("skipping gdb tests on FreeBSD; see https://golang.org/issue/29508")
    43  	}
    44  	if final := os.Getenv("GOROOT_FINAL"); final != "" && runtime.GOROOT() != final {
    45  		t.Skip("gdb test can fail with GOROOT_FINAL pending")
    46  	}
    47  }
    48  
    49  func checkGdbVersion(t *testing.T) {
    50  	// Issue 11214 reports various failures with older versions of gdb.
    51  	out, err := exec.Command("gdb", "--version").CombinedOutput()
    52  	if err != nil {
    53  		t.Skipf("skipping: error executing gdb: %v", err)
    54  	}
    55  	re := regexp.MustCompile(`([0-9]+)\.([0-9]+)`)
    56  	matches := re.FindSubmatch(out)
    57  	if len(matches) < 3 {
    58  		t.Skipf("skipping: can't determine gdb version from\n%s\n", out)
    59  	}
    60  	major, err1 := strconv.Atoi(string(matches[1]))
    61  	minor, err2 := strconv.Atoi(string(matches[2]))
    62  	if err1 != nil || err2 != nil {
    63  		t.Skipf("skipping: can't determine gdb version: %v, %v", err1, err2)
    64  	}
    65  	if major < 7 || (major == 7 && minor < 7) {
    66  		t.Skipf("skipping: gdb version %d.%d too old", major, minor)
    67  	}
    68  	t.Logf("gdb version %d.%d", major, minor)
    69  }
    70  
    71  func checkGdbPython(t *testing.T) {
    72  	if runtime.GOOS == "solaris" && testenv.Builder() != "solaris-amd64-smartosbuildlet" {
    73  		t.Skip("skipping gdb python tests on solaris; see golang.org/issue/20821")
    74  	}
    75  
    76  	cmd := exec.Command("gdb", "-nx", "-q", "--batch", "-iex", "python import sys; print('go gdb python support')")
    77  	out, err := cmd.CombinedOutput()
    78  
    79  	if err != nil {
    80  		t.Skipf("skipping due to issue running gdb: %v", err)
    81  	}
    82  	if strings.TrimSpace(string(out)) != "go gdb python support" {
    83  		t.Skipf("skipping due to lack of python gdb support: %s", out)
    84  	}
    85  }
    86  
    87  const helloSource = `
    88  import "fmt"
    89  import "runtime"
    90  var gslice []string
    91  func main() {
    92  	mapvar := make(map[string]string, 13)
    93  	mapvar["abc"] = "def"
    94  	mapvar["ghi"] = "jkl"
    95  	strvar := "abc"
    96  	ptrvar := &strvar
    97  	slicevar := make([]string, 0, 16)
    98  	slicevar = append(slicevar, mapvar["abc"])
    99  	fmt.Println("hi")
   100  	runtime.KeepAlive(ptrvar)
   101  	_ = ptrvar
   102  	gslice = slicevar
   103  	runtime.KeepAlive(mapvar)
   104  }  // END_OF_PROGRAM
   105  `
   106  
   107  func lastLine(src []byte) int {
   108  	eop := []byte("END_OF_PROGRAM")
   109  	for i, l := range bytes.Split(src, []byte("\n")) {
   110  		if bytes.Contains(l, eop) {
   111  			return i
   112  		}
   113  	}
   114  	return 0
   115  }
   116  
   117  func TestGdbPython(t *testing.T) {
   118  	testGdbPython(t, false)
   119  }
   120  
   121  func TestGdbPythonCgo(t *testing.T) {
   122  	if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" || runtime.GOARCH == "mips64" {
   123  		testenv.SkipFlaky(t, 18784)
   124  	}
   125  	testGdbPython(t, true)
   126  }
   127  
   128  func testGdbPython(t *testing.T, cgo bool) {
   129  	if cgo && !build.Default.CgoEnabled {
   130  		t.Skip("skipping because cgo is not enabled")
   131  	}
   132  
   133  	checkGdbEnvironment(t)
   134  	t.Parallel()
   135  	checkGdbVersion(t)
   136  	checkGdbPython(t)
   137  
   138  	dir, err := ioutil.TempDir("", "go-build")
   139  	if err != nil {
   140  		t.Fatalf("failed to create temp directory: %v", err)
   141  	}
   142  	defer os.RemoveAll(dir)
   143  
   144  	var buf bytes.Buffer
   145  	buf.WriteString("package main\n")
   146  	if cgo {
   147  		buf.WriteString(`import "C"` + "\n")
   148  	}
   149  	buf.WriteString(helloSource)
   150  
   151  	src := buf.Bytes()
   152  
   153  	err = ioutil.WriteFile(filepath.Join(dir, "main.go"), src, 0644)
   154  	if err != nil {
   155  		t.Fatalf("failed to create file: %v", err)
   156  	}
   157  	nLines := lastLine(src)
   158  
   159  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe")
   160  	cmd.Dir = dir
   161  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   162  	if err != nil {
   163  		t.Fatalf("building source %v\n%s", err, out)
   164  	}
   165  
   166  	args := []string{"-nx", "-q", "--batch",
   167  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   168  		"-ex", "set startup-with-shell off",
   169  	}
   170  	if cgo {
   171  		// When we build the cgo version of the program, the system's
   172  		// linker is used. Some external linkers, like GNU gold,
   173  		// compress the .debug_gdb_scripts into .zdebug_gdb_scripts.
   174  		// Until gold and gdb can work together, temporarily load the
   175  		// python script directly.
   176  		args = append(args,
   177  			"-ex", "source "+filepath.Join(runtime.GOROOT(), "src", "runtime", "runtime-gdb.py"),
   178  		)
   179  	} else {
   180  		args = append(args,
   181  			"-ex", "info auto-load python-scripts",
   182  		)
   183  	}
   184  	args = append(args,
   185  		"-ex", "set python print-stack full",
   186  		"-ex", "br main.go:15",
   187  		"-ex", "run",
   188  		"-ex", "echo BEGIN info goroutines\n",
   189  		"-ex", "info goroutines",
   190  		"-ex", "echo END\n",
   191  		"-ex", "echo BEGIN print mapvar\n",
   192  		"-ex", "print mapvar",
   193  		"-ex", "echo END\n",
   194  		"-ex", "echo BEGIN print strvar\n",
   195  		"-ex", "print strvar",
   196  		"-ex", "echo END\n",
   197  		"-ex", "echo BEGIN info locals\n",
   198  		"-ex", "info locals",
   199  		"-ex", "echo END\n",
   200  		"-ex", "echo BEGIN goroutine 1 bt\n",
   201  		"-ex", "goroutine 1 bt",
   202  		"-ex", "echo END\n",
   203  		"-ex", "echo BEGIN goroutine 2 bt\n",
   204  		"-ex", "goroutine 2 bt",
   205  		"-ex", "echo END\n",
   206  		"-ex", "clear main.go:15", // clear the previous break point
   207  		"-ex", fmt.Sprintf("br main.go:%d", nLines), // new break point at the end of main
   208  		"-ex", "c",
   209  		"-ex", "echo BEGIN goroutine 1 bt at the end\n",
   210  		"-ex", "goroutine 1 bt",
   211  		"-ex", "echo END\n",
   212  		filepath.Join(dir, "a.exe"),
   213  	)
   214  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   215  	t.Logf("gdb output: %s\n", got)
   216  
   217  	firstLine := bytes.SplitN(got, []byte("\n"), 2)[0]
   218  	if string(firstLine) != "Loading Go Runtime support." {
   219  		// This can happen when using all.bash with
   220  		// GOROOT_FINAL set, because the tests are run before
   221  		// the final installation of the files.
   222  		cmd := exec.Command(testenv.GoToolPath(t), "env", "GOROOT")
   223  		cmd.Env = []string{}
   224  		out, err := cmd.CombinedOutput()
   225  		if err != nil && bytes.Contains(out, []byte("cannot find GOROOT")) {
   226  			t.Skipf("skipping because GOROOT=%s does not exist", runtime.GOROOT())
   227  		}
   228  
   229  		_, file, _, _ := runtime.Caller(1)
   230  
   231  		t.Logf("package testing source file: %s", file)
   232  		t.Fatalf("failed to load Go runtime support: %s\n%s", firstLine, got)
   233  	}
   234  
   235  	// Extract named BEGIN...END blocks from output
   236  	partRe := regexp.MustCompile(`(?ms)^BEGIN ([^\n]*)\n(.*?)\nEND`)
   237  	blocks := map[string]string{}
   238  	for _, subs := range partRe.FindAllSubmatch(got, -1) {
   239  		blocks[string(subs[1])] = string(subs[2])
   240  	}
   241  
   242  	infoGoroutinesRe := regexp.MustCompile(`\*\s+\d+\s+running\s+`)
   243  	if bl := blocks["info goroutines"]; !infoGoroutinesRe.MatchString(bl) {
   244  		t.Fatalf("info goroutines failed: %s", bl)
   245  	}
   246  
   247  	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"}$`)
   248  	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"}$`)
   249  	if bl := blocks["print mapvar"]; !printMapvarRe1.MatchString(bl) &&
   250  		!printMapvarRe2.MatchString(bl) {
   251  		t.Fatalf("print mapvar failed: %s", bl)
   252  	}
   253  
   254  	strVarRe := regexp.MustCompile(`^\$[0-9]+ = (0x[0-9a-f]+\s+)?"abc"$`)
   255  	if bl := blocks["print strvar"]; !strVarRe.MatchString(bl) {
   256  		t.Fatalf("print strvar failed: %s", bl)
   257  	}
   258  
   259  	// The exact format of composite values has changed over time.
   260  	// For issue 16338: ssa decompose phase split a slice into
   261  	// a collection of scalar vars holding its fields. In such cases
   262  	// the DWARF variable location expression should be of the
   263  	// form "var.field" and not just "field".
   264  	// However, the newer dwarf location list code reconstituted
   265  	// aggregates from their fields and reverted their printing
   266  	// back to its original form.
   267  	// Only test that all variables are listed in 'info locals' since
   268  	// different versions of gdb print variables in different
   269  	// order and with differing amount of information and formats.
   270  
   271  	if bl := blocks["info locals"]; !strings.Contains(bl, "slicevar") ||
   272  		!strings.Contains(bl, "mapvar") ||
   273  		!strings.Contains(bl, "strvar") {
   274  		t.Fatalf("info locals failed: %s", bl)
   275  	}
   276  
   277  	btGoroutine1Re := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?main\.main.+at`)
   278  	if bl := blocks["goroutine 1 bt"]; !btGoroutine1Re.MatchString(bl) {
   279  		t.Fatalf("goroutine 1 bt failed: %s", bl)
   280  	}
   281  
   282  	btGoroutine2Re := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?runtime.+at`)
   283  	if bl := blocks["goroutine 2 bt"]; !btGoroutine2Re.MatchString(bl) {
   284  		t.Fatalf("goroutine 2 bt failed: %s", bl)
   285  	}
   286  	btGoroutine1AtTheEndRe := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?main\.main.+at`)
   287  	if bl := blocks["goroutine 1 bt at the end"]; !btGoroutine1AtTheEndRe.MatchString(bl) {
   288  		t.Fatalf("goroutine 1 bt at the end failed: %s", bl)
   289  	}
   290  }
   291  
   292  const backtraceSource = `
   293  package main
   294  
   295  //go:noinline
   296  func aaa() bool { return bbb() }
   297  
   298  //go:noinline
   299  func bbb() bool { return ccc() }
   300  
   301  //go:noinline
   302  func ccc() bool { return ddd() }
   303  
   304  //go:noinline
   305  func ddd() bool { return f() }
   306  
   307  //go:noinline
   308  func eee() bool { return true }
   309  
   310  var f = eee
   311  
   312  func main() {
   313  	_ = aaa()
   314  }
   315  `
   316  
   317  // TestGdbBacktrace tests that gdb can unwind the stack correctly
   318  // using only the DWARF debug info.
   319  func TestGdbBacktrace(t *testing.T) {
   320  	if runtime.GOOS == "netbsd" {
   321  		testenv.SkipFlaky(t, 15603)
   322  	}
   323  
   324  	checkGdbEnvironment(t)
   325  	t.Parallel()
   326  	checkGdbVersion(t)
   327  
   328  	dir, err := ioutil.TempDir("", "go-build")
   329  	if err != nil {
   330  		t.Fatalf("failed to create temp directory: %v", err)
   331  	}
   332  	defer os.RemoveAll(dir)
   333  
   334  	// Build the source code.
   335  	src := filepath.Join(dir, "main.go")
   336  	err = ioutil.WriteFile(src, []byte(backtraceSource), 0644)
   337  	if err != nil {
   338  		t.Fatalf("failed to create file: %v", err)
   339  	}
   340  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe")
   341  	cmd.Dir = dir
   342  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   343  	if err != nil {
   344  		t.Fatalf("building source %v\n%s", err, out)
   345  	}
   346  
   347  	// Execute gdb commands.
   348  	args := []string{"-nx", "-batch",
   349  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   350  		"-ex", "set startup-with-shell off",
   351  		"-ex", "break main.eee",
   352  		"-ex", "run",
   353  		"-ex", "backtrace",
   354  		"-ex", "continue",
   355  		filepath.Join(dir, "a.exe"),
   356  	}
   357  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   358  
   359  	// Check that the backtrace matches the source code.
   360  	bt := []string{
   361  		"eee",
   362  		"ddd",
   363  		"ccc",
   364  		"bbb",
   365  		"aaa",
   366  		"main",
   367  	}
   368  	for i, name := range bt {
   369  		s := fmt.Sprintf("#%v.*main\\.%v", i, name)
   370  		re := regexp.MustCompile(s)
   371  		if found := re.Find(got) != nil; !found {
   372  			t.Errorf("could not find '%v' in backtrace", s)
   373  			t.Fatalf("gdb output:\n%v", string(got))
   374  		}
   375  	}
   376  }
   377  
   378  const autotmpTypeSource = `
   379  package main
   380  
   381  type astruct struct {
   382  	a, b int
   383  }
   384  
   385  func main() {
   386  	var iface interface{} = map[string]astruct{}
   387  	var iface2 interface{} = []astruct{}
   388  	println(iface, iface2)
   389  }
   390  `
   391  
   392  // TestGdbAutotmpTypes ensures that types of autotmp variables appear in .debug_info
   393  // See bug #17830.
   394  func TestGdbAutotmpTypes(t *testing.T) {
   395  	checkGdbEnvironment(t)
   396  	t.Parallel()
   397  	checkGdbVersion(t)
   398  
   399  	dir, err := ioutil.TempDir("", "go-build")
   400  	if err != nil {
   401  		t.Fatalf("failed to create temp directory: %v", err)
   402  	}
   403  	defer os.RemoveAll(dir)
   404  
   405  	// Build the source code.
   406  	src := filepath.Join(dir, "main.go")
   407  	err = ioutil.WriteFile(src, []byte(autotmpTypeSource), 0644)
   408  	if err != nil {
   409  		t.Fatalf("failed to create file: %v", err)
   410  	}
   411  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-gcflags=all=-N -l", "-o", "a.exe")
   412  	cmd.Dir = dir
   413  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   414  	if err != nil {
   415  		t.Fatalf("building source %v\n%s", err, out)
   416  	}
   417  
   418  	// Execute gdb commands.
   419  	args := []string{"-nx", "-batch",
   420  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   421  		"-ex", "set startup-with-shell off",
   422  		"-ex", "break main.main",
   423  		"-ex", "run",
   424  		"-ex", "step",
   425  		"-ex", "info types astruct",
   426  		filepath.Join(dir, "a.exe"),
   427  	}
   428  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   429  
   430  	sgot := string(got)
   431  
   432  	// Check that the backtrace matches the source code.
   433  	types := []string{
   434  		"[]main.astruct;",
   435  		"bucket<string,main.astruct>;",
   436  		"hash<string,main.astruct>;",
   437  		"main.astruct;",
   438  		"hash<string,main.astruct> * map[string]main.astruct;",
   439  	}
   440  	for _, name := range types {
   441  		if !strings.Contains(sgot, name) {
   442  			t.Errorf("could not find %s in 'info typrs astruct' output", name)
   443  			t.Fatalf("gdb output:\n%v", sgot)
   444  		}
   445  	}
   446  }
   447  
   448  const constsSource = `
   449  package main
   450  
   451  const aConstant int = 42
   452  const largeConstant uint64 = ^uint64(0)
   453  const minusOne int64 = -1
   454  
   455  func main() {
   456  	println("hello world")
   457  }
   458  `
   459  
   460  func TestGdbConst(t *testing.T) {
   461  	checkGdbEnvironment(t)
   462  	t.Parallel()
   463  	checkGdbVersion(t)
   464  
   465  	dir, err := ioutil.TempDir("", "go-build")
   466  	if err != nil {
   467  		t.Fatalf("failed to create temp directory: %v", err)
   468  	}
   469  	defer os.RemoveAll(dir)
   470  
   471  	// Build the source code.
   472  	src := filepath.Join(dir, "main.go")
   473  	err = ioutil.WriteFile(src, []byte(constsSource), 0644)
   474  	if err != nil {
   475  		t.Fatalf("failed to create file: %v", err)
   476  	}
   477  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-gcflags=all=-N -l", "-o", "a.exe")
   478  	cmd.Dir = dir
   479  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   480  	if err != nil {
   481  		t.Fatalf("building source %v\n%s", err, out)
   482  	}
   483  
   484  	// Execute gdb commands.
   485  	args := []string{"-nx", "-batch",
   486  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   487  		"-ex", "set startup-with-shell off",
   488  		"-ex", "break main.main",
   489  		"-ex", "run",
   490  		"-ex", "print main.aConstant",
   491  		"-ex", "print main.largeConstant",
   492  		"-ex", "print main.minusOne",
   493  		"-ex", "print 'runtime.mSpanInUse'",
   494  		"-ex", "print 'runtime._PageSize'",
   495  		filepath.Join(dir, "a.exe"),
   496  	}
   497  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   498  
   499  	sgot := strings.ReplaceAll(string(got), "\r\n", "\n")
   500  
   501  	t.Logf("output %q", sgot)
   502  
   503  	if !strings.Contains(sgot, "\n$1 = 42\n$2 = 18446744073709551615\n$3 = -1\n$4 = 1 '\\001'\n$5 = 8192") {
   504  		t.Fatalf("output mismatch")
   505  	}
   506  }
   507  
   508  const panicSource = `
   509  package main
   510  
   511  import "runtime/debug"
   512  
   513  func main() {
   514  	debug.SetTraceback("crash")
   515  	crash()
   516  }
   517  
   518  func crash() {
   519  	panic("panic!")
   520  }
   521  `
   522  
   523  // TestGdbPanic tests that gdb can unwind the stack correctly
   524  // from SIGABRTs from Go panics.
   525  func TestGdbPanic(t *testing.T) {
   526  	checkGdbEnvironment(t)
   527  	t.Parallel()
   528  	checkGdbVersion(t)
   529  
   530  	dir, err := ioutil.TempDir("", "go-build")
   531  	if err != nil {
   532  		t.Fatalf("failed to create temp directory: %v", err)
   533  	}
   534  	defer os.RemoveAll(dir)
   535  
   536  	// Build the source code.
   537  	src := filepath.Join(dir, "main.go")
   538  	err = ioutil.WriteFile(src, []byte(panicSource), 0644)
   539  	if err != nil {
   540  		t.Fatalf("failed to create file: %v", err)
   541  	}
   542  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe")
   543  	cmd.Dir = dir
   544  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   545  	if err != nil {
   546  		t.Fatalf("building source %v\n%s", err, out)
   547  	}
   548  
   549  	// Execute gdb commands.
   550  	args := []string{"-nx", "-batch",
   551  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   552  		"-ex", "set startup-with-shell off",
   553  		"-ex", "run",
   554  		"-ex", "backtrace",
   555  		filepath.Join(dir, "a.exe"),
   556  	}
   557  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   558  
   559  	// Check that the backtrace matches the source code.
   560  	bt := []string{
   561  		`crash`,
   562  		`main`,
   563  	}
   564  	for _, name := range bt {
   565  		s := fmt.Sprintf("(#.* .* in )?main\\.%v", name)
   566  		re := regexp.MustCompile(s)
   567  		if found := re.Find(got) != nil; !found {
   568  			t.Errorf("could not find '%v' in backtrace", s)
   569  			t.Fatalf("gdb output:\n%v", string(got))
   570  		}
   571  	}
   572  }
   573  

View as plain text