// 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/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) } 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" || 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. "??"). } // 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 func main() { mapvar := make(map[string]string, ` + strconv.FormatInt(abi.MapBucketCount+9, 10) + `) slicemap := make(map[string][]string,` + strconv.FormatInt(abi.MapBucketCount+3, 10) + `) chanint := make(chan int, 10) chanstr := make(chan string, 10) chanint <- 99 chanint <- 11 chanstr <- "spongepants" chanstr <- "squarebob" 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(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) 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 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) } 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) 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) 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", "bucket", "hash", "main.astruct", "hash * 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) 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) 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) 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__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) } } }