...
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 golang.org/issue/22893 and 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 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 "freebsd":
    40  		t.Skip("skipping gdb tests on FreeBSD; see https://golang.org/issue/29508")
    41  	}
    42  	if final := os.Getenv("GOROOT_FINAL"); final != "" && runtime.GOROOT() != final {
    43  		t.Skip("gdb test can fail with GOROOT_FINAL pending")
    44  	}
    45  }
    46  
    47  func checkGdbVersion(t *testing.T) {
    48  	// Issue 11214 reports various failures with older versions of gdb.
    49  	out, err := exec.Command("gdb", "--version").CombinedOutput()
    50  	if err != nil {
    51  		t.Skipf("skipping: error executing gdb: %v", err)
    52  	}
    53  	re := regexp.MustCompile(`([0-9]+)\.([0-9]+)`)
    54  	matches := re.FindSubmatch(out)
    55  	if len(matches) < 3 {
    56  		t.Skipf("skipping: can't determine gdb version from\n%s\n", out)
    57  	}
    58  	major, err1 := strconv.Atoi(string(matches[1]))
    59  	minor, err2 := strconv.Atoi(string(matches[2]))
    60  	if err1 != nil || err2 != nil {
    61  		t.Skipf("skipping: can't determine gdb version: %v, %v", err1, err2)
    62  	}
    63  	if major < 7 || (major == 7 && minor < 7) {
    64  		t.Skipf("skipping: gdb version %d.%d too old", major, minor)
    65  	}
    66  	t.Logf("gdb version %d.%d", major, minor)
    67  }
    68  
    69  func checkGdbPython(t *testing.T) {
    70  	if runtime.GOOS == "solaris" && testenv.Builder() != "solaris-amd64-smartosbuildlet" {
    71  		t.Skip("skipping gdb python tests on solaris; see golang.org/issue/20821")
    72  	}
    73  
    74  	cmd := exec.Command("gdb", "-nx", "-q", "--batch", "-iex", "python import sys; print('go gdb python support')")
    75  	out, err := cmd.CombinedOutput()
    76  
    77  	if err != nil {
    78  		t.Skipf("skipping due to issue running gdb: %v", err)
    79  	}
    80  	if strings.TrimSpace(string(out)) != "go gdb python support" {
    81  		t.Skipf("skipping due to lack of python gdb support: %s", out)
    82  	}
    83  }
    84  
    85  const helloSource = `
    86  import "fmt"
    87  import "runtime"
    88  var gslice []string
    89  func main() {
    90  	mapvar := make(map[string]string, 13)
    91  	mapvar["abc"] = "def"
    92  	mapvar["ghi"] = "jkl"
    93  	strvar := "abc"
    94  	ptrvar := &strvar
    95  	slicevar := make([]string, 0, 16)
    96  	slicevar = append(slicevar, mapvar["abc"])
    97  	fmt.Println("hi")
    98  	runtime.KeepAlive(ptrvar)
    99  	_ = ptrvar
   100  	gslice = slicevar
   101  	runtime.KeepAlive(mapvar)
   102  }  // END_OF_PROGRAM
   103  `
   104  
   105  func lastLine(src []byte) int {
   106  	eop := []byte("END_OF_PROGRAM")
   107  	for i, l := range bytes.Split(src, []byte("\n")) {
   108  		if bytes.Contains(l, eop) {
   109  			return i
   110  		}
   111  	}
   112  	return 0
   113  }
   114  
   115  func TestGdbPython(t *testing.T) {
   116  	testGdbPython(t, false)
   117  }
   118  
   119  func TestGdbPythonCgo(t *testing.T) {
   120  	if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" || runtime.GOARCH == "mips64" {
   121  		testenv.SkipFlaky(t, 18784)
   122  	}
   123  	testGdbPython(t, true)
   124  }
   125  
   126  func testGdbPython(t *testing.T, cgo bool) {
   127  	if cgo && !build.Default.CgoEnabled {
   128  		t.Skip("skipping because cgo is not enabled")
   129  	}
   130  
   131  	checkGdbEnvironment(t)
   132  	t.Parallel()
   133  	checkGdbVersion(t)
   134  	checkGdbPython(t)
   135  
   136  	dir, err := ioutil.TempDir("", "go-build")
   137  	if err != nil {
   138  		t.Fatalf("failed to create temp directory: %v", err)
   139  	}
   140  	defer os.RemoveAll(dir)
   141  
   142  	var buf bytes.Buffer
   143  	buf.WriteString("package main\n")
   144  	if cgo {
   145  		buf.WriteString(`import "C"` + "\n")
   146  	}
   147  	buf.WriteString(helloSource)
   148  
   149  	src := buf.Bytes()
   150  
   151  	err = ioutil.WriteFile(filepath.Join(dir, "main.go"), src, 0644)
   152  	if err != nil {
   153  		t.Fatalf("failed to create file: %v", err)
   154  	}
   155  	nLines := lastLine(src)
   156  
   157  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe")
   158  	cmd.Dir = dir
   159  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   160  	if err != nil {
   161  		t.Fatalf("building source %v\n%s", err, out)
   162  	}
   163  
   164  	args := []string{"-nx", "-q", "--batch",
   165  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   166  		"-ex", "set startup-with-shell off",
   167  	}
   168  	if cgo {
   169  		// When we build the cgo version of the program, the system's
   170  		// linker is used. Some external linkers, like GNU gold,
   171  		// compress the .debug_gdb_scripts into .zdebug_gdb_scripts.
   172  		// Until gold and gdb can work together, temporarily load the
   173  		// python script directly.
   174  		args = append(args,
   175  			"-ex", "source "+filepath.Join(runtime.GOROOT(), "src", "runtime", "runtime-gdb.py"),
   176  		)
   177  	} else {
   178  		args = append(args,
   179  			"-ex", "info auto-load python-scripts",
   180  		)
   181  	}
   182  	args = append(args,
   183  		"-ex", "set python print-stack full",
   184  		"-ex", "br fmt.Println",
   185  		"-ex", "run",
   186  		"-ex", "echo BEGIN info goroutines\n",
   187  		"-ex", "info goroutines",
   188  		"-ex", "echo END\n",
   189  		"-ex", "up", // up from fmt.Println to main
   190  		"-ex", "echo BEGIN print mapvar\n",
   191  		"-ex", "print mapvar",
   192  		"-ex", "echo END\n",
   193  		"-ex", "echo BEGIN print strvar\n",
   194  		"-ex", "print strvar",
   195  		"-ex", "echo END\n",
   196  		"-ex", "echo BEGIN info locals\n",
   197  		"-ex", "info locals",
   198  		"-ex", "echo END\n",
   199  		"-ex", "down", // back to fmt.Println (goroutine 2 below only works at bottom of stack.  TODO: fix that)
   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 fmt.Println", // 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(`\Q = map[string]string = {["abc"] = "def", ["ghi"] = "jkl"}\E$`)
   248  	printMapvarRe2 := regexp.MustCompile(`\Q = map[string]string = {["ghi"] = "jkl", ["abc"] = "def"}\E$`)
   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(`\Q = "abc"\E$`)
   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  
   268  	infoLocalsRe := regexp.MustCompile(`slicevar *= *\[\]string *= *{"def"}`)
   269  	if bl := blocks["info locals"]; !infoLocalsRe.MatchString(bl) {
   270  		t.Fatalf("info locals failed: %s", bl)
   271  	}
   272  
   273  	btGoroutine1Re := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?fmt\.Println.+at`)
   274  	if bl := blocks["goroutine 1 bt"]; !btGoroutine1Re.MatchString(bl) {
   275  		t.Fatalf("goroutine 1 bt failed: %s", bl)
   276  	}
   277  
   278  	btGoroutine2Re := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?runtime.+at`)
   279  	if bl := blocks["goroutine 2 bt"]; !btGoroutine2Re.MatchString(bl) {
   280  		t.Fatalf("goroutine 2 bt failed: %s", bl)
   281  	}
   282  	btGoroutine1AtTheEndRe := regexp.MustCompile(`(?m)^#0\s+(0x[0-9a-f]+\s+in\s+)?main\.main.+at`)
   283  	if bl := blocks["goroutine 1 bt at the end"]; !btGoroutine1AtTheEndRe.MatchString(bl) {
   284  		t.Fatalf("goroutine 1 bt at the end failed: %s", bl)
   285  	}
   286  }
   287  
   288  const backtraceSource = `
   289  package main
   290  
   291  //go:noinline
   292  func aaa() bool { return bbb() }
   293  
   294  //go:noinline
   295  func bbb() bool { return ccc() }
   296  
   297  //go:noinline
   298  func ccc() bool { return ddd() }
   299  
   300  //go:noinline
   301  func ddd() bool { return f() }
   302  
   303  //go:noinline
   304  func eee() bool { return true }
   305  
   306  var f = eee
   307  
   308  func main() {
   309  	_ = aaa()
   310  }
   311  `
   312  
   313  // TestGdbBacktrace tests that gdb can unwind the stack correctly
   314  // using only the DWARF debug info.
   315  func TestGdbBacktrace(t *testing.T) {
   316  	if runtime.GOOS == "netbsd" {
   317  		testenv.SkipFlaky(t, 15603)
   318  	}
   319  
   320  	checkGdbEnvironment(t)
   321  	t.Parallel()
   322  	checkGdbVersion(t)
   323  
   324  	dir, err := ioutil.TempDir("", "go-build")
   325  	if err != nil {
   326  		t.Fatalf("failed to create temp directory: %v", err)
   327  	}
   328  	defer os.RemoveAll(dir)
   329  
   330  	// Build the source code.
   331  	src := filepath.Join(dir, "main.go")
   332  	err = ioutil.WriteFile(src, []byte(backtraceSource), 0644)
   333  	if err != nil {
   334  		t.Fatalf("failed to create file: %v", err)
   335  	}
   336  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe")
   337  	cmd.Dir = dir
   338  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   339  	if err != nil {
   340  		t.Fatalf("building source %v\n%s", err, out)
   341  	}
   342  
   343  	// Execute gdb commands.
   344  	args := []string{"-nx", "-batch",
   345  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   346  		"-ex", "set startup-with-shell off",
   347  		"-ex", "break main.eee",
   348  		"-ex", "run",
   349  		"-ex", "backtrace",
   350  		"-ex", "continue",
   351  		filepath.Join(dir, "a.exe"),
   352  	}
   353  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   354  
   355  	// Check that the backtrace matches the source code.
   356  	bt := []string{
   357  		"eee",
   358  		"ddd",
   359  		"ccc",
   360  		"bbb",
   361  		"aaa",
   362  		"main",
   363  	}
   364  	for i, name := range bt {
   365  		s := fmt.Sprintf("#%v.*main\\.%v", i, name)
   366  		re := regexp.MustCompile(s)
   367  		if found := re.Find(got) != nil; !found {
   368  			t.Errorf("could not find '%v' in backtrace", s)
   369  			t.Fatalf("gdb output:\n%v", string(got))
   370  		}
   371  	}
   372  }
   373  
   374  const autotmpTypeSource = `
   375  package main
   376  
   377  type astruct struct {
   378  	a, b int
   379  }
   380  
   381  func main() {
   382  	var iface interface{} = map[string]astruct{}
   383  	var iface2 interface{} = []astruct{}
   384  	println(iface, iface2)
   385  }
   386  `
   387  
   388  // TestGdbAutotmpTypes ensures that types of autotmp variables appear in .debug_info
   389  // See bug #17830.
   390  func TestGdbAutotmpTypes(t *testing.T) {
   391  	checkGdbEnvironment(t)
   392  	t.Parallel()
   393  	checkGdbVersion(t)
   394  
   395  	dir, err := ioutil.TempDir("", "go-build")
   396  	if err != nil {
   397  		t.Fatalf("failed to create temp directory: %v", err)
   398  	}
   399  	defer os.RemoveAll(dir)
   400  
   401  	// Build the source code.
   402  	src := filepath.Join(dir, "main.go")
   403  	err = ioutil.WriteFile(src, []byte(autotmpTypeSource), 0644)
   404  	if err != nil {
   405  		t.Fatalf("failed to create file: %v", err)
   406  	}
   407  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-gcflags=all=-N -l", "-o", "a.exe")
   408  	cmd.Dir = dir
   409  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   410  	if err != nil {
   411  		t.Fatalf("building source %v\n%s", err, out)
   412  	}
   413  
   414  	// Execute gdb commands.
   415  	args := []string{"-nx", "-batch",
   416  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   417  		"-ex", "set startup-with-shell off",
   418  		"-ex", "break main.main",
   419  		"-ex", "run",
   420  		"-ex", "step",
   421  		"-ex", "info types astruct",
   422  		filepath.Join(dir, "a.exe"),
   423  	}
   424  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   425  
   426  	sgot := string(got)
   427  
   428  	// Check that the backtrace matches the source code.
   429  	types := []string{
   430  		"struct []main.astruct;",
   431  		"struct bucket<string,main.astruct>;",
   432  		"struct hash<string,main.astruct>;",
   433  		"struct main.astruct;",
   434  		"typedef struct hash<string,main.astruct> * map[string]main.astruct;",
   435  	}
   436  	for _, name := range types {
   437  		if !strings.Contains(sgot, name) {
   438  			t.Errorf("could not find %s in 'info typrs astruct' output", name)
   439  			t.Fatalf("gdb output:\n%v", sgot)
   440  		}
   441  	}
   442  }
   443  
   444  const constsSource = `
   445  package main
   446  
   447  const aConstant int = 42
   448  const largeConstant uint64 = ^uint64(0)
   449  const minusOne int64 = -1
   450  
   451  func main() {
   452  	println("hello world")
   453  }
   454  `
   455  
   456  func TestGdbConst(t *testing.T) {
   457  	checkGdbEnvironment(t)
   458  	t.Parallel()
   459  	checkGdbVersion(t)
   460  
   461  	dir, err := ioutil.TempDir("", "go-build")
   462  	if err != nil {
   463  		t.Fatalf("failed to create temp directory: %v", err)
   464  	}
   465  	defer os.RemoveAll(dir)
   466  
   467  	// Build the source code.
   468  	src := filepath.Join(dir, "main.go")
   469  	err = ioutil.WriteFile(src, []byte(constsSource), 0644)
   470  	if err != nil {
   471  		t.Fatalf("failed to create file: %v", err)
   472  	}
   473  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-gcflags=all=-N -l", "-o", "a.exe")
   474  	cmd.Dir = dir
   475  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   476  	if err != nil {
   477  		t.Fatalf("building source %v\n%s", err, out)
   478  	}
   479  
   480  	// Execute gdb commands.
   481  	args := []string{"-nx", "-batch",
   482  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   483  		"-ex", "set startup-with-shell off",
   484  		"-ex", "break main.main",
   485  		"-ex", "run",
   486  		"-ex", "print main.aConstant",
   487  		"-ex", "print main.largeConstant",
   488  		"-ex", "print main.minusOne",
   489  		"-ex", "print 'runtime._MSpanInUse'",
   490  		"-ex", "print 'runtime._PageSize'",
   491  		filepath.Join(dir, "a.exe"),
   492  	}
   493  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   494  
   495  	sgot := strings.Replace(string(got), "\r\n", "\n", -1)
   496  
   497  	t.Logf("output %q", sgot)
   498  
   499  	if !strings.Contains(sgot, "\n$1 = 42\n$2 = 18446744073709551615\n$3 = -1\n$4 = 1 '\\001'\n$5 = 8192") {
   500  		t.Fatalf("output mismatch")
   501  	}
   502  }
   503  
   504  const panicSource = `
   505  package main
   506  
   507  import "runtime/debug"
   508  
   509  func main() {
   510  	debug.SetTraceback("crash")
   511  	crash()
   512  }
   513  
   514  func crash() {
   515  	panic("panic!")
   516  }
   517  `
   518  
   519  // TestGdbPanic tests that gdb can unwind the stack correctly
   520  // from SIGABRTs from Go panics.
   521  func TestGdbPanic(t *testing.T) {
   522  	checkGdbEnvironment(t)
   523  	t.Parallel()
   524  	checkGdbVersion(t)
   525  
   526  	dir, err := ioutil.TempDir("", "go-build")
   527  	if err != nil {
   528  		t.Fatalf("failed to create temp directory: %v", err)
   529  	}
   530  	defer os.RemoveAll(dir)
   531  
   532  	// Build the source code.
   533  	src := filepath.Join(dir, "main.go")
   534  	err = ioutil.WriteFile(src, []byte(panicSource), 0644)
   535  	if err != nil {
   536  		t.Fatalf("failed to create file: %v", err)
   537  	}
   538  	cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe")
   539  	cmd.Dir = dir
   540  	out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
   541  	if err != nil {
   542  		t.Fatalf("building source %v\n%s", err, out)
   543  	}
   544  
   545  	// Execute gdb commands.
   546  	args := []string{"-nx", "-batch",
   547  		"-iex", "add-auto-load-safe-path " + filepath.Join(runtime.GOROOT(), "src", "runtime"),
   548  		"-ex", "set startup-with-shell off",
   549  		"-ex", "run",
   550  		"-ex", "backtrace",
   551  		filepath.Join(dir, "a.exe"),
   552  	}
   553  	got, _ := exec.Command("gdb", args...).CombinedOutput()
   554  
   555  	// Check that the backtrace matches the source code.
   556  	bt := []string{
   557  		`crash`,
   558  		`main`,
   559  	}
   560  	for _, name := range bt {
   561  		s := fmt.Sprintf("(#.* .* in )?main\\.%v", name)
   562  		re := regexp.MustCompile(s)
   563  		if found := re.Find(got) != nil; !found {
   564  			t.Errorf("could not find '%v' in backtrace", s)
   565  			t.Fatalf("gdb output:\n%v", string(got))
   566  		}
   567  	}
   568  }
   569  

View as plain text