go/src/runtime/runtime-gdb_test.go
Than McIntosh 8d767ff38d runtime: increase GDB version testing requirement to 10 from 7.7
Bump the required version of GDB up to 10 from 7.7 in the runtime GDB
tests, so as to ensure that we have something that can handle DWARF 5
when running tests. In theory there is some DWARF 5 support on the
version 9 release branch, but we get "Dwarf Error: DW_FORM_addrx"
errors for some archs on builders where GDB 9.2 is installed.

Updates #26379.

Change-Id: I1b7b45f8e4dd1fafccf22f2dda0124458ecf7cba
Reviewed-on: https://go-review.googlesource.com/c/go/+/656836
Auto-Submit: Ian Lance Taylor <iant@google.com>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
2025-03-11 20:07:53 -07:00

871 lines
25 KiB
Go

// 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"
"flag"
"fmt"
"internal/abi"
"internal/goexperiment"
"internal/testenv"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"time"
)
// NOTE: In some configurations, GDB will segfault when sent a SIGWINCH signal.
// Some runtime tests send SIGWINCH to the entire process group, so those tests
// must never run in parallel with GDB tests.
//
// See issue 39021 and https://sourceware.org/bugzilla/show_bug.cgi?id=26056.
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 "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")
}
// Disable GDB tests on alpine until issue #54352 resolved.
if strings.HasSuffix(testenv.Builder(), "-alpine") {
t.Skip("skipping gdb tests on alpine; see https://golang.org/issue/54352")
}
case "freebsd":
t.Skip("skipping gdb tests on FreeBSD; see https://golang.org/issue/29508")
case "aix":
if testing.Short() {
t.Skip("skipping gdb tests on AIX; see https://golang.org/issue/35710")
}
case "plan9":
t.Skip("there is no gdb on Plan 9")
}
}
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)
}
// The Go toolchain now generates DWARF 5 by default, which needs
// a GDB version of 10 or above.
if major < 10 {
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" || runtime.GOOS == "illumos" {
t.Skip("skipping gdb python tests on illumos and solaris; see golang.org/issue/20821")
}
args := []string{"-nx", "-q", "--batch", "-iex", "python import sys; print('go gdb python support')"}
gdbArgsFixup(args)
cmd := exec.Command("gdb", args...)
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)
}
}
// checkCleanBacktrace checks that the given backtrace is well formed and does
// not contain any error messages from GDB.
func checkCleanBacktrace(t *testing.T, backtrace string) {
backtrace = strings.TrimSpace(backtrace)
lines := strings.Split(backtrace, "\n")
if len(lines) == 0 {
t.Fatalf("empty backtrace")
}
for i, l := range lines {
if !strings.HasPrefix(l, fmt.Sprintf("#%v ", i)) {
t.Fatalf("malformed backtrace at line %v: %v", i, l)
}
}
// TODO(mundaym): check for unknown frames (e.g. "??").
}
// checkPtraceScope checks the value of the kernel parameter ptrace_scope,
// skips the test when gdb cannot attach to the target process via ptrace.
// See issue 69932
//
// 0 - Default attach security permissions.
// 1 - Restricted attach. Only child processes plus normal permissions.
// 2 - Admin-only attach. Only executables with CAP_SYS_PTRACE.
// 3 - No attach. No process may call ptrace at all. Irrevocable.
func checkPtraceScope(t *testing.T) {
if runtime.GOOS != "linux" {
return
}
// If the Linux kernel does not have the YAMA module enabled,
// there will be no ptrace_scope file, which does not affect the tests.
path := "/proc/sys/kernel/yama/ptrace_scope"
if _, err := os.Stat(path); os.IsNotExist(err) {
return
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read file: %v", err)
}
value, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
t.Fatalf("failed converting value to int: %v", err)
}
switch value {
case 3:
t.Skip("skipping ptrace: Operation not permitted")
case 2:
if os.Geteuid() != 0 {
t.Skip("skipping ptrace: Operation not permitted with non-root user")
}
}
}
// NOTE: the maps below are allocated larger than abi.MapBucketCount
// to ensure that they are not "optimized out".
var helloSource = `
import "fmt"
import "runtime"
var gslice []string
// TODO(prattmic): Stack allocated maps initialized inline appear "optimized out" in GDB.
var smallmapvar map[string]string
func main() {
smallmapvar = make(map[string]string)
mapvar := make(map[string]string, ` + strconv.FormatInt(abi.OldMapBucketCount+9, 10) + `)
slicemap := make(map[string][]string,` + strconv.FormatInt(abi.OldMapBucketCount+3, 10) + `)
chanint := make(chan int, 10)
chanstr := make(chan string, 10)
chanint <- 99
chanint <- 11
chanstr <- "spongepants"
chanstr <- "squarebob"
smallmapvar["abc"] = "def"
mapvar["abc"] = "def"
mapvar["ghi"] = "jkl"
slicemap["a"] = []string{"b","c","d"}
slicemap["e"] = []string{"f","g","h"}
strvar := "abc"
ptrvar := &strvar
slicevar := make([]string, 0, 16)
slicevar = append(slicevar, mapvar["abc"])
fmt.Println("hi")
runtime.KeepAlive(ptrvar)
_ = ptrvar // set breakpoint here
gslice = slicevar
fmt.Printf("%v, %v, %v\n", slicemap, <-chanint, <-chanstr)
runtime.KeepAlive(smallmapvar)
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 gdbArgsFixup(args []string) {
if runtime.GOOS != "windows" {
return
}
// On Windows, some gdb flavors expect -ex and -iex arguments
// containing spaces to be double quoted.
var quote bool
for i, arg := range args {
if arg == "-iex" || arg == "-ex" {
quote = true
} else if quote {
if strings.ContainsRune(arg, ' ') {
args[i] = `"` + arg + `"`
}
quote = false
}
}
}
func TestGdbPython(t *testing.T) {
testGdbPython(t, false)
}
func TestGdbPythonCgo(t *testing.T) {
if strings.HasPrefix(runtime.GOARCH, "mips") {
testenv.SkipFlaky(t, 37794)
}
testGdbPython(t, true)
}
func testGdbPython(t *testing.T, cgo bool) {
if cgo {
testenv.MustHaveCGO(t)
}
checkGdbEnvironment(t)
t.Parallel()
checkGdbVersion(t)
checkGdbPython(t)
checkPtraceScope(t)
dir := t.TempDir()
var buf bytes.Buffer
buf.WriteString("package main\n")
if cgo {
buf.WriteString(`import "C"` + "\n")
}
buf.WriteString(helloSource)
src := buf.Bytes()
// Locate breakpoint line
var bp int
lines := bytes.Split(src, []byte("\n"))
for i, line := range lines {
if bytes.Contains(line, []byte("breakpoint")) {
bp = i
break
}
}
err := os.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", "main.go")
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(testenv.GOROOT(t), "src", "runtime"),
"-ex", "set startup-with-shell off",
"-ex", "set print thread-events 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(testenv.GOROOT(t), "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", fmt.Sprintf("br main.go:%d", bp),
"-ex", "run",
"-ex", "echo BEGIN info goroutines\n",
"-ex", "info goroutines",
"-ex", "echo END\n",
"-ex", "echo BEGIN print smallmapvar\n",
"-ex", "print smallmapvar",
"-ex", "echo END\n",
"-ex", "echo BEGIN print mapvar\n",
"-ex", "print mapvar",
"-ex", "echo END\n",
"-ex", "echo BEGIN print slicemap\n",
"-ex", "print slicemap",
"-ex", "echo END\n",
"-ex", "echo BEGIN print strvar\n",
"-ex", "print strvar",
"-ex", "echo END\n",
"-ex", "echo BEGIN print chanint\n",
"-ex", "print chanint",
"-ex", "echo END\n",
"-ex", "echo BEGIN print chanstr\n",
"-ex", "print chanstr",
"-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 all bt\n",
"-ex", "goroutine all 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"),
)
gdbArgsFixup(args)
got, err := exec.Command("gdb", args...).CombinedOutput()
t.Logf("gdb output:\n%s", got)
if err != nil {
t.Fatalf("gdb exited with error: %v", err)
}
got = bytes.ReplaceAll(got, []byte("\r\n"), []byte("\n")) // normalize line endings
// 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)
}
printSmallMapvarRe := regexp.MustCompile(`^\$[0-9]+ = map\[string\]string = {\[(0x[0-9a-f]+\s+)?"abc"\] = (0x[0-9a-f]+\s+)?"def"}$`)
if bl := blocks["print smallmapvar"]; !printSmallMapvarRe.MatchString(bl) {
t.Fatalf("print smallmapvar 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)
}
// 2 orders, and possible differences in spacing.
sliceMapSfx1 := `map[string][]string = {["e"] = []string = {"f", "g", "h"}, ["a"] = []string = {"b", "c", "d"}}`
sliceMapSfx2 := `map[string][]string = {["a"] = []string = {"b", "c", "d"}, ["e"] = []string = {"f", "g", "h"}}`
if bl := strings.ReplaceAll(blocks["print slicemap"], " ", " "); !strings.HasSuffix(bl, sliceMapSfx1) && !strings.HasSuffix(bl, sliceMapSfx2) {
t.Fatalf("print slicemap failed: %s", bl)
}
chanIntSfx := `chan int = {99, 11}`
if bl := strings.ReplaceAll(blocks["print chanint"], " ", " "); !strings.HasSuffix(bl, chanIntSfx) {
t.Fatalf("print chanint failed: %s", bl)
}
chanStrSfx := `chan string = {"spongepants", "squarebob"}`
if bl := strings.ReplaceAll(blocks["print chanstr"], " ", " "); !strings.HasSuffix(bl, chanStrSfx) {
t.Fatalf("print chanstr 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)
}
// Check that the backtraces are well formed.
checkCleanBacktrace(t, blocks["goroutine 1 bt"])
checkCleanBacktrace(t, blocks["goroutine 1 bt at the end"])
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)
}
if bl := blocks["goroutine all bt"]; !btGoroutine1Re.MatchString(bl) {
t.Fatalf("goroutine all 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)
}
if flag.Lookup("test.parallel").Value.(flag.Getter).Get().(int) < 2 {
// It is possible that this test will hang for a long time due to an
// apparent GDB bug reported in https://go.dev/issue/37405.
// If test parallelism is high enough, that might be ok: the other parallel
// tests will finish, and then this test will finish right before it would
// time out. However, if test are running sequentially, a hang in this test
// would likely cause the remaining tests to run out of time.
testenv.SkipFlaky(t, 37405)
}
checkGdbEnvironment(t)
t.Parallel()
checkGdbVersion(t)
checkPtraceScope(t)
dir := t.TempDir()
// Build the source code.
src := filepath.Join(dir, "main.go")
err := os.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", "main.go")
cmd.Dir = dir
out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
if err != nil {
t.Fatalf("building source %v\n%s", err, out)
}
// Execute gdb commands.
start := time.Now()
args := []string{"-nx", "-batch",
"-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"),
"-ex", "set startup-with-shell off",
"-ex", "break main.eee",
"-ex", "run",
"-ex", "backtrace",
"-ex", "continue",
filepath.Join(dir, "a.exe"),
}
gdbArgsFixup(args)
cmd = testenv.Command(t, "gdb", args...)
// Work around the GDB hang reported in https://go.dev/issue/37405.
// Sometimes (rarely), the GDB process hangs completely when the Go program
// exits, and we suspect that the bug is on the GDB side.
//
// The default Cancel function added by testenv.Command will mark the test as
// failed if it is in danger of timing out, but we want to instead mark it as
// skipped. Change the Cancel function to kill the process and merely log
// instead of failing the test.
//
// (This approach does not scale: if the test parallelism is less than or
// equal to the number of tests that run right up to the deadline, then the
// remaining parallel tests are likely to time out. But as long as it's just
// this one flaky test, it's probably fine..?)
//
// If there is no deadline set on the test at all, relying on the timeout set
// by testenv.Command will cause the test to hang indefinitely, but that's
// what “no deadline” means, after all — and it's probably the right behavior
// anyway if someone is trying to investigate and fix the GDB bug.
cmd.Cancel = func() error {
t.Logf("GDB command timed out after %v: %v", time.Since(start), cmd)
return cmd.Process.Kill()
}
got, err := cmd.CombinedOutput()
t.Logf("gdb output:\n%s", got)
if err != nil {
switch {
case bytes.Contains(got, []byte("internal-error: wait returned unexpected status 0x0")):
// GDB bug: https://sourceware.org/bugzilla/show_bug.cgi?id=28551
testenv.SkipFlaky(t, 43068)
case bytes.Contains(got, []byte("Couldn't get registers: No such process.")),
bytes.Contains(got, []byte("Unable to fetch general registers.: No such process.")),
bytes.Contains(got, []byte("reading register pc (#64): No such process.")):
// GDB bug: https://sourceware.org/bugzilla/show_bug.cgi?id=9086
testenv.SkipFlaky(t, 50838)
case bytes.Contains(got, []byte("waiting for new child: No child processes.")):
// GDB bug: Sometimes it fails to wait for a clone child.
testenv.SkipFlaky(t, 60553)
case bytes.Contains(got, []byte(" exited normally]\n")):
// GDB bug: Sometimes the inferior exits fine,
// but then GDB hangs.
testenv.SkipFlaky(t, 37405)
}
t.Fatalf("gdb exited with error: %v", err)
}
// 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.Fatalf("could not find '%v' in backtrace", s)
}
}
}
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)
checkPtraceScope(t)
if runtime.GOOS == "aix" && testing.Short() {
t.Skip("TestGdbAutotmpTypes is too slow on aix/ppc64")
}
dir := t.TempDir()
// Build the source code.
src := filepath.Join(dir, "main.go")
err := os.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", "main.go")
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(testenv.GOROOT(t), "src", "runtime"),
"-ex", "set startup-with-shell off",
// Some gdb may set scheduling-locking as "step" by default. This prevents background tasks
// (e.g GC) from completing which may result in a hang when executing the step command.
// See #49852.
"-ex", "set scheduler-locking off",
"-ex", "break main.main",
"-ex", "run",
"-ex", "step",
"-ex", "info types astruct",
filepath.Join(dir, "a.exe"),
}
gdbArgsFixup(args)
got, err := exec.Command("gdb", args...).CombinedOutput()
t.Logf("gdb output:\n%s", got)
if err != nil {
t.Fatalf("gdb exited with error: %v", err)
}
sgot := string(got)
// Check that the backtrace matches the source code.
types := []string{
"[]main.astruct",
"main.astruct",
}
if goexperiment.SwissMap {
types = append(types, []string{
"groupReference<string,main.astruct>",
"table<string,main.astruct>",
"map<string,main.astruct>",
"map<string,main.astruct> * map[string]main.astruct",
}...)
} else {
types = append(types, []string{
"bucket<string,main.astruct>",
"hash<string,main.astruct>",
"hash<string,main.astruct> * map[string]main.astruct",
}...)
}
for _, name := range types {
if !strings.Contains(sgot, name) {
t.Fatalf("could not find %q in 'info typrs astruct' output", name)
}
}
}
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)
checkPtraceScope(t)
dir := t.TempDir()
// Build the source code.
src := filepath.Join(dir, "main.go")
err := os.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", "main.go")
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(testenv.GOROOT(t), "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"),
}
gdbArgsFixup(args)
got, err := exec.Command("gdb", args...).CombinedOutput()
t.Logf("gdb output:\n%s", got)
if err != nil {
t.Fatalf("gdb exited with error: %v", err)
}
sgot := strings.ReplaceAll(string(got), "\r\n", "\n")
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)
checkPtraceScope(t)
if runtime.GOOS == "windows" {
t.Skip("no signals on windows")
}
dir := t.TempDir()
// Build the source code.
src := filepath.Join(dir, "main.go")
err := os.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", "main.go")
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(testenv.GOROOT(t), "src", "runtime"),
"-ex", "set startup-with-shell off",
"-ex", "run",
"-ex", "backtrace",
filepath.Join(dir, "a.exe"),
}
gdbArgsFixup(args)
got, err := exec.Command("gdb", args...).CombinedOutput()
t.Logf("gdb output:\n%s", got)
if err != nil {
t.Fatalf("gdb exited with error: %v", err)
}
// 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.Fatalf("could not find '%v' in backtrace", s)
}
}
}
const InfCallstackSource = `
package main
import "C"
import "time"
func loop() {
for i := 0; i < 1000; i++ {
time.Sleep(time.Millisecond*5)
}
}
func main() {
go loop()
time.Sleep(time.Second * 1)
}
`
// TestGdbInfCallstack tests that gdb can unwind the callstack of cgo programs
// on arm64 platforms without endless frames of function 'crossfunc1'.
// https://golang.org/issue/37238
func TestGdbInfCallstack(t *testing.T) {
checkGdbEnvironment(t)
testenv.MustHaveCGO(t)
if runtime.GOARCH != "arm64" {
t.Skip("skipping infinite callstack test on non-arm64 arches")
}
t.Parallel()
checkGdbVersion(t)
checkPtraceScope(t)
dir := t.TempDir()
// Build the source code.
src := filepath.Join(dir, "main.go")
err := os.WriteFile(src, []byte(InfCallstackSource), 0644)
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "a.exe", "main.go")
cmd.Dir = dir
out, err := testenv.CleanCmdEnv(cmd).CombinedOutput()
if err != nil {
t.Fatalf("building source %v\n%s", err, out)
}
// Execute gdb commands.
// 'setg_gcc' is the first point where we can reproduce the issue with just one 'run' command.
args := []string{"-nx", "-batch",
"-iex", "add-auto-load-safe-path " + filepath.Join(testenv.GOROOT(t), "src", "runtime"),
"-ex", "set startup-with-shell off",
"-ex", "break setg_gcc",
"-ex", "run",
"-ex", "backtrace 3",
"-ex", "disable 1",
"-ex", "continue",
filepath.Join(dir, "a.exe"),
}
gdbArgsFixup(args)
got, err := exec.Command("gdb", args...).CombinedOutput()
t.Logf("gdb output:\n%s", got)
if err != nil {
t.Fatalf("gdb exited with error: %v", err)
}
// Check that the backtrace matches
// We check the 3 inner most frames only as they are present certainly, according to gcc_<OS>_arm64.c
bt := []string{
`setg_gcc`,
`crosscall1`,
`threadentry`,
}
for i, name := range bt {
s := fmt.Sprintf("#%v.*%v", i, name)
re := regexp.MustCompile(s)
if found := re.Find(got) != nil; !found {
t.Fatalf("could not find '%v' in backtrace", s)
}
}
}