Repository: go-delve/delve

Issue:
Misleading hint when setting breakpoints for debugging child process
dlv version: 1.24.0
go version: 1.24.0
OS: Ubuntu 20.04.6 LTS
Arch: x64
What did I do? Use `target follow-exec -on` to debug the child process. Set breakpoints in the source code of the executable of the child process.
What did you expect to see? Successfully set breakpoints.
What did you see instead? `Command failed: location ... not found`

I tried to set breakpoints to debug the child process. But the misleading hint `Command failed: location ... not found` tempted me to choose `n`.

If I didn't try to press `Y` at the end of the following video, the title of the issue should be "unable to set breakpoints when debugging child process".

[![asciicast](https://asciinema.org/a/706784.svg)](https://asciinema.org/a/706784)

The reproduction repository is [Lslightly/delve-dbg-child-proc](https://github.com/Lslightly/delve-dbg-child-proc). README.md contains the steps to reproduce the following video.

The expression `when plugin is loaded` doesn't cover the scenario I think. Besides, `Command failed: location ... not found` can be avoided.



Discussion:
@Lslightly: ![Image](https://github.com/user-attachments/assets/1b1cb078-79f5-424b-ae94-c417d05e5023)

I try to debug dlv. It seems that the `pid` is incorrect after switching the target process. I'm not familiar with the source code.

The delve-init.txt to store the commands for dlv is in https://github.com/Lslightly/delve-dbg-child-proc/blob/main/delve-init.txt. You only need to input a `s` character and it will bring you to the child process.

## Debug dlv in VSCode

> The following content is about how to set up in VSCode.

The vscode launch.json for debugging dlv with [Lslightly/delve-dbg-child-proc](https://github.com/Lslightly/delve-dbg-child-proc?tab=readme-ov-file#delve-debug-child-process) is as follows:

```json
    "configurations": [
        {
            "name": "Launch Package",
            "type": "go",
            "request": "launch",
            "mode": "exec",
            "program": "dlv",
            "args": [
                "debug",
                "--init", "delve-init.txt",
                "--allow-non-terminal-interactive=true",
                ".",
            ],
            "cwd": "<change the path to the root of delve-dbg-child-proc repository>",
            "console": "integratedTerminal"
        }
    ]
```

@Lslightly: What could `err` be in the following code? https://github.com/go-delve/delve/blob/fc6a439f2206c796bb20f56f37f8e10eb26cb158/service/debugger/debugger.go#L1946

When `err` is `not found`, the `t.Group().Selected` target process can be tried I think instead of returning immediately?

Solution patch:
diff --git a/_fixtures/spawn.go b/_fixtures/spawn.go
index 75f5c418..7cc9d30b 100644
--- a/_fixtures/spawn.go
+++ b/_fixtures/spawn.go
@@ -44,5 +44,11 @@ func main() {
 	case "child":
 		traceme2(os.Args[2])
 
+	case "spawn2":
+		cmd := exec.Command(os.Args[2])
+		cmd.Stdout = os.Stdout
+		cmd.Start()
+		cmd.Wait()
+
 	}
 }
diff --git a/_fixtures/spawnchild.go b/_fixtures/spawnchild.go
new file mode 100644
index 00000000..0133d3e6
--- /dev/null
+++ b/_fixtures/spawnchild.go
@@ -0,0 +1,11 @@
+package main
+
+import "fmt"
+
+func traceme5() {
+	fmt.Println("hello world")
+}
+
+func main() {
+	traceme5()
+}
diff --git a/pkg/goversion/compat.go b/pkg/goversion/compat.go
index 7d802944..636119e2 100644
--- a/pkg/goversion/compat.go
+++ b/pkg/goversion/compat.go
@@ -1,12 +1,15 @@
 package goversion
 
 import (
+	"errors"
 	"fmt"
 	"runtime"
 
 	"github.com/go-delve/delve/pkg/logflags"
 )
 
+//lint:file-ignore ST1005 errors here can be capitalized
+
 var (
 	MinSupportedVersionOfGoMajor = 1
 	MinSupportedVersionOfGoMinor = 21
@@ -20,7 +23,7 @@ var (
 
 // Compatible checks that the version specified in the producer string is compatible with
 // this version of delve.
-func Compatible(producer string, warnonly bool) error {
+func Compatible(dwarfVer uint8, producer string, warnonly bool) error {
 	ver := ParseProducer(producer)
 	if ver.IsOldDevel() {
 		return nil
@@ -46,5 +49,15 @@ func Compatible(producer string, warnonly bool) error {
 		}
 		return fmt.Errorf(dlvTooOldErr, ver.String())
 	}
+	if dwarfVer >= 5 {
+		if !VersionAfterOrEqual(runtime.Version(), 1, 25) {
+			const errstr = "To debug executables using DWARFv5 or later Delve must be built with Go version 1.25.0 or later"
+			if warnonly {
+				logflags.WriteError(errstr)
+				return nil
+			}
+			return errors.New(errstr)
+		}
+	}
 	return nil
 }
diff --git a/pkg/locspec/locations.go b/pkg/locspec/locations.go
index 1080a5f0..c98103fd 100644
--- a/pkg/locspec/locations.go
+++ b/pkg/locspec/locations.go
@@ -24,6 +24,15 @@ type LocationSpec interface {
 	Find(t *proc.Target, processArgs []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool, substitutePathRules [][2]string) ([]api.Location, string, error)
 }
 
+// ErrLocationNotFound indicates that the location was not found.
+type ErrLocationNotFound struct {
+	Spec string
+}
+
+func (err *ErrLocationNotFound) Error() string {
+	return fmt.Sprintf("location %q not found", err.Spec)
+}
+
 // NormalLocationSpec represents a basic location spec.
 // This can be a file:line or func:line.
 type NormalLocationSpec struct {
@@ -394,7 +403,7 @@ func (loc *NormalLocationSpec) Find(t *proc.Target, processArgs []string, scope
 
 	if matching := len(candidateFiles) + len(candidateFuncs); matching == 0 {
 		if scope == nil {
-			return nil, "", fmt.Errorf("location %q not found", locStr)
+			return nil, "", &ErrLocationNotFound{Spec: locStr}
 		}
 		// if no result was found this locations string could be an
 		// expression that the user forgot to prefix with '*', try treating it as
@@ -402,7 +411,7 @@ func (loc *NormalLocationSpec) Find(t *proc.Target, processArgs []string, scope
 		addrSpec := &AddrLocationSpec{AddrExpr: locStr}
 		locs, subst, err := addrSpec.Find(t, processArgs, scope, locStr, includeNonExecutableLines, nil)
 		if err != nil {
-			return nil, "", fmt.Errorf("location %q not found", locStr)
+			return nil, "", &ErrLocationNotFound{Spec: locStr}
 		}
 		return locs, subst, nil
 	} else if matching > 1 {
diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go
index d76e585c..87a322db 100644
--- a/pkg/proc/bininfo.go
+++ b/pkg/proc/bininfo.go
@@ -1386,6 +1386,19 @@ func (bi *BinaryInfo) Producer() string {
 	return ""
 }
 
+// DwarfVersion returns the maximum DWARF version in the executable.
+func (bi *BinaryInfo) DwarfVersion() uint8 {
+	r := uint8(0)
+	for _, so := range bi.Images {
+		for _, cu := range so.compileUnits {
+			if cu.Version > r {
+				r = cu.Version
+			}
+		}
+	}
+	return r
+}
+
 // Type returns the Dwarf type entry at `offset`.
 func (image *Image) Type(offset dwarf.Offset) (godwarf.Type, error) {
 	return godwarf.ReadType(image.dwarf, image.index, offset, image.typeCache)
diff --git a/pkg/proc/ppc64le_arch.go b/pkg/proc/ppc64le_arch.go
index 19ea625e..122498da 100644
--- a/pkg/proc/ppc64le_arch.go
+++ b/pkg/proc/ppc64le_arch.go
@@ -39,6 +39,7 @@ func PPC64LEArch(goos string) *Arch {
 		LRRegNum:                         regnum.PPC64LE_LR,
 		asmRegisters:                     ppc64leAsmRegisters,
 		RegisterNameToDwarf:              nameToDwarfFunc(regnum.PPC64LENameToDwarf),
+		RegnumToString:                   regnum.PPC64LEToName,
 		debugCallMinStackSize:            320,
 		maxRegArgBytes:                   13*8 + 13*8,
 		argumentRegs:                     []int{regnum.PPC64LE_R0 + 3, regnum.PPC64LE_R0 + 4, regnum.PPC64LE_R0 + 5},
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 50d4c416..6bfe2477 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -254,7 +254,8 @@ func (d *Debugger) checkGoVersion() error {
 	if producer == "" {
 		return nil
 	}
-	return goversion.Compatible(producer, !d.config.CheckGoVersion)
+	dwarfVer := d.target.Selected.BinInfo().DwarfVersion()
+	return goversion.Compatible(dwarfVer, producer, !d.config.CheckGoVersion)
 }
 
 func (d *Debugger) TargetGoVersion() string {
@@ -1943,7 +1944,7 @@ func (d *Debugger) findLocation(goid int64, frame, deferredCall int, locStr stri
 		if s1 != "" {
 			subst = s1
 		}
-		if err != nil {
+		if err != nil && !errors.As(err, new(*locspec.ErrLocationNotFound)) {
 			return nil, "", err
 		}
 		for i := range locs {
@@ -1961,6 +1962,9 @@ func (d *Debugger) findLocation(goid int64, frame, deferredCall int, locStr stri
 		}
 		locations = append(locations, locs...)
 	}
+	if len(locations) == 0 {
+		return locations, subst, &locspec.ErrLocationNotFound{Spec: locStr}
+	}
 	return locations, subst, nil
 }
 



Test patch:
diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go
index 06e8e0b7..16b2d337 100644
--- a/cmd/dlv/dlv_test.go
+++ b/cmd/dlv/dlv_test.go
@@ -770,7 +770,7 @@ func TestRemoteDAPClientAfterContinue(t *testing.T) {
 
 	dlvbin := protest.GetDlvBinary(t)
 
-	fixture := protest.BuildFixture("loopprog", 0)
+	fixture := protest.BuildFixture(t, "loopprog", 0)
 	cmd := exec.Command(dlvbin, "exec", fixture.Path, "--headless", "--continue", "--accept-multiclient", "--log-output=debugger,dap", "--log", "--listen", listenAddr)
 	stdout, err := cmd.StdoutPipe()
 	assertNoError(err, t, "stdout pipe")
@@ -1035,7 +1035,7 @@ func TestTracePid(t *testing.T) {
 	expected := []byte("goroutine(1): main.A()\n>> goroutine(1): main.A => ()\n")
 
 	// make process run
-	fix := protest.BuildFixture("issue2023", 0)
+	fix := protest.BuildFixture(t, "issue2023", 0)
 	targetCmd := exec.Command(fix.Path)
 	assertNoError(targetCmd.Start(), t, "execute issue2023")
 
diff --git a/pkg/dwarf/line/line_parser_test.go b/pkg/dwarf/line/line_parser_test.go
index 6f5bfd7b..022f3ff0 100644
--- a/pkg/dwarf/line/line_parser_test.go
+++ b/pkg/dwarf/line/line_parser_test.go
@@ -122,7 +122,7 @@ func testDebugLinePrologueParser(p string, t *testing.T) {
 		}
 
 		for _, ln := range dbl.Lookup {
-			if ln.Path == "<autogenerated>" || strings.HasPrefix(ln.Path, "<missing>_") || ln.Path == "_gomod_.go" {
+			if ln.Path == "<autogenerated>" || ln.Path == "?" || strings.HasPrefix(ln.Path, "<missing>_") || ln.Path == "_gomod_.go" {
 				continue
 			}
 			if _, err := os.Stat(ln.Path); err != nil {
diff --git a/pkg/proc/core/core_test.go b/pkg/proc/core/core_test.go
index cebac8ac..dddc1d3e 100644
--- a/pkg/proc/core/core_test.go
+++ b/pkg/proc/core/core_test.go
@@ -205,7 +205,7 @@ func withCoreFile(t *testing.T, name, args string) *proc.TargetGroup {
 	if buildMode == "pie" {
 		buildFlags = test.BuildModePIE
 	}
-	fix := test.BuildFixture(name, buildFlags)
+	fix := test.BuildFixture(t, name, buildFlags)
 	bashCmd := fmt.Sprintf("cd %v && ulimit -c unlimited && GOTRACEBACK=crash %v %s", tempDir, fix.Path, args)
 	exec.Command("bash", "-c", bashCmd).Run()
 	cores, err := filepath.Glob(path.Join(tempDir, "core*"))
@@ -464,7 +464,7 @@ func TestMinidump(t *testing.T) {
 	if buildMode == "pie" {
 		buildFlags = test.BuildModePIE
 	}
-	fix := test.BuildFixture("sleep", buildFlags)
+	fix := test.BuildFixture(t, "sleep", buildFlags)
 	mdmpPath := procdump(t, fix.Path)
 
 	grp, err := OpenCore(mdmpPath, fix.Path, []string{})
diff --git a/pkg/proc/gdbserial/rr_test.go b/pkg/proc/gdbserial/rr_test.go
index e6206652..3dc3d1e2 100644
--- a/pkg/proc/gdbserial/rr_test.go
+++ b/pkg/proc/gdbserial/rr_test.go
@@ -24,7 +24,7 @@ func TestMain(m *testing.M) {
 }
 
 func withTestRecording(name string, t testing.TB, fn func(grp *proc.TargetGroup, fixture protest.Fixture)) {
-	fixture := protest.BuildFixture(name, 0)
+	fixture := protest.BuildFixture(t, name, 0)
 	protest.MustHaveRecordingAllowed(t)
 	if path, _ := exec.LookPath("rr"); path == "" {
 		t.Skip("test skipped, rr not found")
diff --git a/pkg/proc/proc_general_test.go b/pkg/proc/proc_general_test.go
index dfa040a9..86e5b2de 100644
--- a/pkg/proc/proc_general_test.go
+++ b/pkg/proc/proc_general_test.go
@@ -130,7 +130,7 @@ func assertNoError(err error, t testing.TB, s string) {
 
 func TestDwarfVersion(t *testing.T) {
 	// Tests that we correctly read the version of compilation units
-	fixture := protest.BuildFixture("math", 0)
+	fixture := protest.BuildFixture(t, "math", 0)
 	bi := NewBinaryInfo(runtime.GOOS, runtime.GOARCH)
 	// Use a fake entry point so LoadBinaryInfo does not error in case the binary is PIE.
 	const fakeEntryPoint = 1
@@ -147,7 +147,7 @@ func TestRegabiFlagSentinel(t *testing.T) {
 	if !protest.RegabiSupported() {
 		t.Skip("irrelevant before Go 1.17 or on non-amd64 architectures")
 	}
-	fixture := protest.BuildFixture("math", 0)
+	fixture := protest.BuildFixture(t, "math", 0)
 	bi := NewBinaryInfo(runtime.GOOS, runtime.GOARCH)
 	// Use a fake entry point so LoadBinaryInfo does not error in case the binary is PIE.
 	const fakeEntryPoint = 1
diff --git a/pkg/proc/proc_linux_test.go b/pkg/proc/proc_linux_test.go
index a86c7bf5..f654a334 100644
--- a/pkg/proc/proc_linux_test.go
+++ b/pkg/proc/proc_linux_test.go
@@ -23,7 +23,7 @@ func mustHaveObjcopy(t *testing.T) {
 
 func TestLoadingExternalDebugInfo(t *testing.T) {
 	mustHaveObjcopy(t)
-	fixture := protest.BuildFixture("locationsprog", 0)
+	fixture := protest.BuildFixture(t, "locationsprog", 0)
 	defer os.Remove(fixture.Path)
 	stripAndCopyDebugInfo(fixture, t)
 	p, err := native.Launch(append([]string{fixture.Path}, ""), "", 0, []string{filepath.Dir(fixture.Path)}, "", "", proc.OutputRedirect{}, proc.OutputRedirect{})
@@ -36,7 +36,7 @@ func TestLoadingExternalDebugInfo(t *testing.T) {
 func TestGnuDebuglink(t *testing.T) {
 	mustHaveObjcopy(t)
 	// build math.go and make a copy of the executable
-	fixture := protest.BuildFixture("math", 0)
+	fixture := protest.BuildFixture(t, "math", 0)
 	buf, err := os.ReadFile(fixture.Path)
 	assertNoError(err, t, "ReadFile")
 	debuglinkPath := fixture.Path + "-gnu_debuglink"
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 9d65896b..9ecfec7e 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -94,10 +94,11 @@ func withTestProcess(name string, t testing.TB, fn func(p *proc.Target, grp *pro
 }
 
 func withTestProcessArgs(name string, t testing.TB, wd string, args []string, buildFlags protest.BuildFlags, fn func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture)) {
+	t.Helper()
 	if buildMode == "pie" {
 		buildFlags |= protest.BuildModePIE
 	}
-	fixture := protest.BuildFixture(name, buildFlags)
+	fixture := protest.BuildFixture(t, name, buildFlags)
 
 	grp := startTestProcessArgs(fixture, t, wd, args)
 
@@ -2487,7 +2488,7 @@ func TestAttachDetach(t *testing.T) {
 	if buildMode == "pie" {
 		buildFlags |= protest.BuildModePIE
 	}
-	fixture := protest.BuildFixture("testnextnethttp", buildFlags)
+	fixture := protest.BuildFixture(t, "testnextnethttp", buildFlags)
 	cmd := exec.Command(fixture.Path)
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
@@ -4498,7 +4499,7 @@ func TestIssue2319(t *testing.T) {
 		t.Skip("test contains fixture that is specific to go 1.14+")
 	}
 
-	fixture := protest.BuildFixture("issue2319/", protest.BuildModeExternalLinker)
+	fixture := protest.BuildFixture(t, "issue2319/", protest.BuildModeExternalLinker)
 
 	// Load up the binary and make sure there are no crashes.
 	bi := proc.NewBinaryInfo("linux", "amd64")
@@ -5361,7 +5362,7 @@ func testWaitForSetup(t *testing.T, mu *sync.Mutex, started *bool) (*exec.Cmd, *
 	if buildMode == "pie" {
 		buildFlags |= protest.BuildModePIE
 	}
-	fixture := protest.BuildFixture("loopprog", buildFlags)
+	fixture := protest.BuildFixture(t, "loopprog", buildFlags)
 
 	cmd := exec.Command(fixture.Path)
 
diff --git a/pkg/proc/proc_unix_test.go b/pkg/proc/proc_unix_test.go
index db3d1094..c7660d36 100644
--- a/pkg/proc/proc_unix_test.go
+++ b/pkg/proc/proc_unix_test.go
@@ -80,7 +80,7 @@ func TestSignalDeath(t *testing.T) {
 	if buildMode == "pie" {
 		buildFlags |= protest.BuildModePIE
 	}
-	fixture := protest.BuildFixture("loopprog", buildFlags)
+	fixture := protest.BuildFixture(t, "loopprog", buildFlags)
 	cmd := exec.Command(fixture.Path)
 	stdout, err := cmd.StdoutPipe()
 	assertNoError(err, t, "StdoutPipe")
diff --git a/pkg/proc/test/support.go b/pkg/proc/test/support.go
index 6312cfc6..d1f25bd4 100644
--- a/pkg/proc/test/support.go
+++ b/pkg/proc/test/support.go
@@ -96,7 +96,8 @@ func TempFile(name string) string {
 }
 
 // BuildFixture will compile the fixture 'name' using the provided build flags.
-func BuildFixture(name string, flags BuildFlags) Fixture {
+func BuildFixture(t testing.TB, name string, flags BuildFlags) Fixture {
+	t.Helper()
 	if !runningWithFixtures {
 		panic("RunTestsWithFixtures not called")
 	}
@@ -189,10 +190,12 @@ func BuildFixture(name string, flags BuildFlags) Fixture {
 	if flags&EnableDWZCompression != 0 {
 		cmd := exec.Command("dwz", tmpfile)
 		if out, err := cmd.CombinedOutput(); err != nil {
+			if strings.Contains(string(out), "Unknown debugging section .debug_addr") {
+				t.Skip("can not run dwz")
+				return Fixture{}
+			}
 			if regexp.MustCompile(`dwz: Section offsets in (.*?) not monotonically increasing`).FindString(string(out)) == "" {
-				fmt.Printf("Error running dwz on %s: %s\n", tmpfile, err)
-				fmt.Printf("%s\n", string(out))
-				os.Exit(1)
+				t.Fatalf("Error running dwz on %s: %s\n%s\n", tmpfile, err, string(out))
 			}
 		}
 	}
@@ -365,7 +368,7 @@ func WithPlugins(t *testing.T, flags BuildFlags, plugins ...string) []Fixture {
 
 	r := make([]Fixture, len(plugins))
 	for i := range plugins {
-		r[i] = BuildFixture(plugins[i], flags|BuildModePlugin)
+		r[i] = BuildFixture(t, plugins[i], flags|BuildModePlugin)
 	}
 	return r
 }
diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go
index 3dde9987..a426c02c 100644
--- a/pkg/terminal/command_test.go
+++ b/pkg/terminal/command_test.go
@@ -132,7 +132,7 @@ func withTestTerminalBuildFlags(name string, t testing.TB, buildFlags test.Build
 	}
 	server := rpccommon.NewServer(&service.Config{
 		Listener:    listener,
-		ProcessArgs: []string{test.BuildFixture(name, buildFlags).Path},
+		ProcessArgs: []string{test.BuildFixture(t, name, buildFlags).Path},
 		Debugger: debugger.Config{
 			Backend: testBackend,
 		},
@@ -1023,7 +1023,7 @@ func TestExamineMemoryCmd(t *testing.T) {
 			t.Fatalf("could convert %s into int64, err %s", addressStr, err)
 		}
 
-		res := term.MustExec("examinemem  -count 52 -fmt hex " + addressStr)
+		res := term.MustExec("examinemem  -count 51 -fmt hex " + addressStr)
 		t.Logf("the result of examining memory \n%s", res)
 		// check first line
 		firstLine := fmt.Sprintf("%#x:   0x0a   0x0b   0x0c   0x0d   0x0e   0x0f   0x10   0x11", address)
@@ -1032,7 +1032,7 @@ func TestExamineMemoryCmd(t *testing.T) {
 		}
 
 		// check last line
-		lastLine := fmt.Sprintf("%#x:   0x3a   0x3b   0x3c   0x00", address+6*8)
+		lastLine := fmt.Sprintf("%#x:   0x3a   0x3b   0x3c", address+6*8)
 		if !strings.Contains(res, lastLine) {
 			t.Fatalf("expected last line: %s", lastLine)
 		}
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index 079fa888..f16bf043 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -64,7 +64,7 @@ func runTest(t *testing.T, name string, test func(c *daptest.Client, f protest.F
 
 // name is for _fixtures/<name>.go
 func runTestBuildFlags(t *testing.T, name string, test func(c *daptest.Client, f protest.Fixture), buildFlags protest.BuildFlags, defaultDebugInfoDirs bool) {
-	fixture := protest.BuildFixture(name, buildFlags)
+	fixture := protest.BuildFixture(t, name, buildFlags)
 
 	// Start the DAP server.
 	serverStopped := make(chan struct{})
@@ -216,7 +216,7 @@ func TestStopWithTarget(t *testing.T) {
 
 			client.InitializeRequest()
 			client.ExpectInitializeResponseAndCapabilities(t)
-			fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
+			fixture := protest.BuildFixture(t, "increment", protest.AllNonOptimized)
 			client.LaunchRequest("debug", fixture.Source, stopOnEntry)
 			client.ExpectProcessEvent(t)
 			client.ExpectInitializedEvent(t)
@@ -303,7 +303,7 @@ func TestSessionStop(t *testing.T) {
 			time.Sleep(10 * time.Millisecond) // give time to start reading
 			client.InitializeRequest()
 			client.ExpectInitializeResponseAndCapabilities(t)
-			fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
+			fixture := protest.BuildFixture(t, "increment", protest.AllNonOptimized)
 			client.LaunchRequest("debug", fixture.Source, stopOnEntry)
 			client.ExpectProcessEvent(t)
 			client.ExpectInitializedEvent(t)
@@ -321,7 +321,7 @@ func TestForceStopWhileStopping(t *testing.T) {
 
 	client.InitializeRequest()
 	client.ExpectInitializeResponseAndCapabilities(t)
-	fixture := protest.BuildFixture("increment", protest.AllNonOptimized)
+	fixture := protest.BuildFixture(t, "increment", protest.AllNonOptimized)
 	client.LaunchRequest("exec", fixture.Path, stopOnEntry)
 	client.ExpectProcessEvent(t)
 	client.ExpectInitializedEvent(t)
@@ -6771,7 +6771,7 @@ func launchDebuggerWithTargetRunning(t *testing.T, fixture string) (*protest.Fix
 
 func launchDebuggerWithTargetHalted(t *testing.T, fixture string) (*protest.Fixture, *debugger.Debugger) {
 	t.Helper()
-	fixbin := protest.BuildFixture(fixture, protest.AllNonOptimized)
+	fixbin := protest.BuildFixture(t, fixture, protest.AllNonOptimized)
 	cfg := service.Config{
 		ProcessArgs: []string{fixbin.Path},
 		Debugger:    debugger.Config{Backend: "default"},
@@ -6785,7 +6785,7 @@ func launchDebuggerWithTargetHalted(t *testing.T, fixture string) (*protest.Fixt
 
 func attachDebuggerWithTargetHalted(t *testing.T, fixture string) (*exec.Cmd, *debugger.Debugger) {
 	t.Helper()
-	fixbin := protest.BuildFixture(fixture, protest.AllNonOptimized)
+	fixbin := protest.BuildFixture(t, fixture, protest.AllNonOptimized)
 	cmd := execFixture(t, fixbin)
 	cfg := service.Config{Debugger: debugger.Config{Backend: "default", AttachPid: cmd.Process.Pid}}
 	dbg, err := debugger.New(&cfg.Debugger, nil) // debugger halts process on entry
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index 2b972bf7..224d2d5a 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -69,7 +69,7 @@ func startServer(name string, buildFlags protest.BuildFlags, t *testing.T, redir
 	if buildMode == "pie" {
 		buildFlags |= protest.BuildModePIE
 	}
-	fixture = protest.BuildFixture(name, buildFlags)
+	fixture = protest.BuildFixture(t, name, buildFlags)
 	for i := range redirects {
 		if redirects[i] != "" {
 			redirects[i] = filepath.Join(fixture.BuildDir, redirects[i])
@@ -2067,7 +2067,7 @@ func TestAcceptMulticlient(t *testing.T) {
 		disconnectChan := make(chan struct{})
 		server := rpccommon.NewServer(&service.Config{
 			Listener:       listener,
-			ProcessArgs:    []string{protest.BuildFixture("testvariables2", 0).Path},
+			ProcessArgs:    []string{protest.BuildFixture(t, "testvariables2", 0).Path},
 			AcceptMulti:    true,
 			DisconnectChan: disconnectChan,
 			Debugger: debugger.Config{
@@ -2105,7 +2105,7 @@ func TestForceStopWhileContinue(t *testing.T) {
 		defer listener.Close()
 		server := rpccommon.NewServer(&service.Config{
 			Listener:       listener,
-			ProcessArgs:    []string{protest.BuildFixture("http_server", protest.AllNonOptimized).Path},
+			ProcessArgs:    []string{protest.BuildFixture(t, "http_server", protest.AllNonOptimized).Path},
 			AcceptMulti:    true,
 			DisconnectChan: disconnectChan,
 			Debugger: debugger.Config{
@@ -2493,7 +2493,7 @@ func TestDetachLeaveRunning(t *testing.T) {
 	if buildMode == "pie" {
 		buildFlags |= protest.BuildModePIE
 	}
-	fixture := protest.BuildFixture("testnextnethttp", buildFlags)
+	fixture := protest.BuildFixture(t, "testnextnethttp", buildFlags)
 
 	cmd := exec.Command(fixture.Path)
 	cmd.Stdout = os.Stdout
@@ -2572,7 +2572,7 @@ func TestStopServerWithClosedListener(t *testing.T) {
 	}
 	listener, err := net.Listen("tcp", "localhost:0")
 	assertNoError(err, t, "listener")
-	fixture := protest.BuildFixture("math", 0)
+	fixture := protest.BuildFixture(t, "math", 0)
 	server := rpccommon.NewServer(&service.Config{
 		Listener:           listener,
 		AcceptMulti:        false,
@@ -3296,3 +3296,49 @@ func TestGuessSubstitutePath(t *testing.T) {
 		}
 	})
 }
+
+func TestFollowExecFindLocation(t *testing.T) {
+	// FindLocation should not return an error if at least one of the currently
+	// attached targets can find the specified location.
+	// See issue #3933
+	if runtime.GOOS == "freebsd" || runtime.GOOS == "darwin" {
+		t.Skip("follow exec not implemented")
+	}
+	var buildFlags protest.BuildFlags
+	if buildMode == "pie" {
+		buildFlags |= protest.BuildModePIE
+	}
+	childFixture := protest.BuildFixture("spawnchild", buildFlags)
+
+	withTestClient2Extended("spawn", t, 0, [3]string{}, []string{"spawn2", childFixture.Path}, func(c service.Client, fixture protest.Fixture) {
+		assertNoError(c.FollowExec(true, ""), t, "FollowExec")
+		_, err := c.CreateBreakpointWithExpr(&api.Breakpoint{File: childFixture.Source, Line: 9}, fmt.Sprintf("%s:%d", childFixture.Source, 9), nil, true)
+		assertNoError(err, t, "CreateBreakpoint(spawnchild.go:9)")
+
+		state := <-c.Continue()
+		assertNoError(state.Err, t, "Continue()")
+
+		tgts, err := c.ListTargets()
+		assertNoError(err, t, "ListTargets")
+
+		t.Logf("%v\n", tgts)
+		found := false
+		for _, tgt := range tgts {
+			if tgt.Pid == state.Pid {
+				if !strings.Contains(tgt.CmdLine, "spawnchild") {
+					t.Fatalf("did not switch to child process")
+				}
+				found = true
+			}
+		}
+		if !found {
+			t.Fatalf("current target not found")
+		}
+
+		_, _, err = c.FindLocation(api.EvalScope{GoroutineID: -1}, fmt.Sprintf("%s:%d", childFixture.Source, 6), true, nil)
+		assertNoError(err, t, "FindLocation(spawnchild.go:6)")
+
+		_, _, err = c.FindLocation(api.EvalScope{GoroutineID: -1}, fmt.Sprintf("%s:%d", fixture.Source, 19), true, nil)
+		assertNoError(err, t, "FindLocation(spawn.go:19)")
+	})
+}