proc,proc/native: adds ability to automatically debug child processes (#3165)

Adds the ability to automatically debug child processes executed by the
target to the linux native backend.
This commit does not contain user interface or API to access this
functionality.

Updates #2551
This commit is contained in:
Alessandro Arzilli 2023-02-22 18:26:28 +01:00 committed by GitHub
parent 3d6730d12e
commit 37e44bf603
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1391 additions and 829 deletions

48
_fixtures/spawn.go Normal file

@ -0,0 +1,48 @@
package main
import (
"fmt"
"os"
"os/exec"
"strconv"
"time"
)
func traceme1() {
fmt.Printf("parent starting\n")
}
func traceme2(n string) {
fmt.Printf("hello from %s\n", n)
}
func traceme3() {
fmt.Printf("done\n")
}
func main() {
exe, _ := os.Executable()
switch os.Args[1] {
case "spawn":
traceme1()
n, _ := strconv.Atoi(os.Args[2])
cmds := []*exec.Cmd{}
for i := 0; i < n; i++ {
cmd := exec.Command(exe, "child", fmt.Sprintf("C%d", i))
cmd.Stdout = os.Stdout
cmd.Start()
cmds = append(cmds, cmd)
}
for _, cmd := range cmds {
cmd.Wait()
}
time.Sleep(1 * time.Second)
traceme3()
case "child":
traceme2(os.Args[2])
}
}

@ -204,7 +204,7 @@ var ErrUnrecognizedFormat = errors.New("unrecognized core format")
// OpenCore will open the core file and return a Process struct. // OpenCore will open the core file and return a Process struct.
// If the DWARF information cannot be found in the binary, Delve will look // If the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in. // for external debug files in the directories passed in.
func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.Target, error) { func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.TargetGroup, error) {
var p *process var p *process
var currentThread proc.Thread var currentThread proc.Thread
var err error var err error
@ -222,14 +222,13 @@ func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.Target, e
return nil, ErrNoThreads return nil, ErrNoThreads
} }
return proc.NewTarget(p, p.pid, currentThread, proc.NewTargetConfig{ grp, addTarget := proc.NewGroup(p, proc.NewTargetGroupConfig{
Path: exePath,
DebugInfoDirs: debugInfoDirs, DebugInfoDirs: debugInfoDirs,
DisableAsyncPreempt: false, DisableAsyncPreempt: false,
StopReason: proc.StopAttached,
CanDump: false, CanDump: false,
ContinueOnce: continueOnce,
}) })
_, err = addTarget(p, p.pid, currentThread, exePath, proc.StopAttached)
return grp, err
} }
// BinInfo will return the binary info. // BinInfo will return the binary info.
@ -310,6 +309,11 @@ func (p *process) WriteMemory(addr uint64, data []byte) (int, error) {
return 0, ErrWriteCore return 0, ErrWriteCore
} }
// FollowExec enables (or disables) follow exec mode
func (p *process) FollowExec(bool) error {
return nil
}
// ProcessMemory returns the memory of this thread's process. // ProcessMemory returns the memory of this thread's process.
func (t *thread) ProcessMemory() proc.MemoryReadWriter { func (t *thread) ProcessMemory() proc.MemoryReadWriter {
return t.p return t.p
@ -419,7 +423,7 @@ func (p *process) ClearInternalBreakpoints() error {
return nil return nil
} }
func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { func (*process) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) {
return nil, proc.StopUnknown, ErrContinueCore return nil, proc.StopUnknown, ErrContinueCore
} }

@ -198,7 +198,7 @@ func TestSplicedReader(t *testing.T) {
} }
} }
func withCoreFile(t *testing.T, name, args string) *proc.Target { func withCoreFile(t *testing.T, name, args string) *proc.TargetGroup {
// This is all very fragile and won't work on hosts with non-default core patterns. // This is all very fragile and won't work on hosts with non-default core patterns.
// Might be better to check in the binary and core? // Might be better to check in the binary and core?
tempDir := t.TempDir() tempDir := t.TempDir()
@ -251,8 +251,8 @@ func TestCore(t *testing.T) {
if runtime.GOOS == "linux" && os.Getenv("CI") == "true" && buildMode == "pie" { if runtime.GOOS == "linux" && os.Getenv("CI") == "true" && buildMode == "pie" {
t.Skip("disabled on linux, Github Actions, with PIE buildmode") t.Skip("disabled on linux, Github Actions, with PIE buildmode")
} }
p := withCoreFile(t, "panic", "") grp := withCoreFile(t, "panic", "")
grp := proc.NewGroup(p) p := grp.Selected
recorded, _ := grp.Recorded() recorded, _ := grp.Recorded()
if !recorded { if !recorded {
@ -324,7 +324,8 @@ func TestCoreFpRegisters(t *testing.T) {
t.Skip("not supported in go1.10 and later") t.Skip("not supported in go1.10 and later")
} }
p := withCoreFile(t, "fputest/", "panic") grp := withCoreFile(t, "fputest/", "panic")
p := grp.Selected
gs, _, err := proc.GoroutinesInfo(p, 0, 0) gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 { if err != nil || len(gs) == 0 {
@ -407,7 +408,8 @@ func TestCoreWithEmptyString(t *testing.T) {
if runtime.GOOS == "linux" && os.Getenv("CI") == "true" && buildMode == "pie" { if runtime.GOOS == "linux" && os.Getenv("CI") == "true" && buildMode == "pie" {
t.Skip("disabled on linux, Github Actions, with PIE buildmode") t.Skip("disabled on linux, Github Actions, with PIE buildmode")
} }
p := withCoreFile(t, "coreemptystring", "") grp := withCoreFile(t, "coreemptystring", "")
p := grp.Selected
gs, _, err := proc.GoroutinesInfo(p, 0, 0) gs, _, err := proc.GoroutinesInfo(p, 0, 0)
assertNoError(err, t, "GoroutinesInfo") assertNoError(err, t, "GoroutinesInfo")
@ -452,10 +454,11 @@ func TestMinidump(t *testing.T) {
fix := test.BuildFixture("sleep", buildFlags) fix := test.BuildFixture("sleep", buildFlags)
mdmpPath := procdump(t, fix.Path) mdmpPath := procdump(t, fix.Path)
p, err := OpenCore(mdmpPath, fix.Path, []string{}) grp, err := OpenCore(mdmpPath, fix.Path, []string{})
if err != nil { if err != nil {
t.Fatalf("OpenCore: %v", err) t.Fatalf("OpenCore: %v", err)
} }
p := grp.Selected
gs, _, err := proc.GoroutinesInfo(p, 0, 0) gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 { if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err) t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)

@ -278,7 +278,7 @@ func newProcess(process *os.Process) *gdbProcess {
} }
// Listen waits for a connection from the stub. // Listen waits for a connection from the stub.
func (p *gdbProcess) Listen(listener net.Listener, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.Target, error) { func (p *gdbProcess) Listen(listener net.Listener, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) {
acceptChan := make(chan net.Conn) acceptChan := make(chan net.Conn)
go func() { go func() {
@ -300,7 +300,7 @@ func (p *gdbProcess) Listen(listener net.Listener, path string, pid int, debugIn
} }
// Dial attempts to connect to the stub. // Dial attempts to connect to the stub.
func (p *gdbProcess) Dial(addr string, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.Target, error) { func (p *gdbProcess) Dial(addr string, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) {
for { for {
conn, err := net.Dial("tcp", addr) conn, err := net.Dial("tcp", addr)
if err == nil { if err == nil {
@ -321,7 +321,7 @@ func (p *gdbProcess) Dial(addr string, path string, pid int, debugInfoDirs []str
// program and the PID of the target process, both are optional, however // program and the PID of the target process, both are optional, however
// some stubs do not provide ways to determine path and pid automatically // some stubs do not provide ways to determine path and pid automatically
// and Connect will be unable to function without knowing them. // and Connect will be unable to function without knowing them.
func (p *gdbProcess) Connect(conn net.Conn, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.Target, error) { func (p *gdbProcess) Connect(conn net.Conn, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) {
p.conn.conn = conn p.conn.conn = conn
p.conn.pid = pid p.conn.pid = pid
err := p.conn.handshake(p.regnames) err := p.conn.handshake(p.regnames)
@ -450,7 +450,7 @@ func getLdEnvVars() []string {
// LLDBLaunch starts an instance of lldb-server and connects to it, asking // LLDBLaunch starts an instance of lldb-server and connects to it, asking
// it to launch the specified target program with the specified arguments // it to launch the specified target program with the specified arguments
// (cmd) on the specified directory wd. // (cmd) on the specified directory wd.
func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.Target, error) { func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.TargetGroup, error) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return nil, ErrUnsupportedOS return nil, ErrUnsupportedOS
} }
@ -567,11 +567,11 @@ func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs [
p := newProcess(process.Process) p := newProcess(process.Process)
p.conn.isDebugserver = isDebugserver p.conn.isDebugserver = isDebugserver
var tgt *proc.Target var grp *proc.TargetGroup
if listener != nil { if listener != nil {
tgt, err = p.Listen(listener, cmd[0], 0, debugInfoDirs, proc.StopLaunched) grp, err = p.Listen(listener, cmd[0], 0, debugInfoDirs, proc.StopLaunched)
} else { } else {
tgt, err = p.Dial(port, cmd[0], 0, debugInfoDirs, proc.StopLaunched) grp, err = p.Dial(port, cmd[0], 0, debugInfoDirs, proc.StopLaunched)
} }
if p.conn.pid != 0 && foreground && isatty.IsTerminal(os.Stdin.Fd()) { if p.conn.pid != 0 && foreground && isatty.IsTerminal(os.Stdin.Fd()) {
// Make the target process the controlling process of the tty if it is a foreground process. // Make the target process the controlling process of the tty if it is a foreground process.
@ -580,7 +580,7 @@ func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs [
logflags.DebuggerLogger().Errorf("could not set controlling process: %v", err) logflags.DebuggerLogger().Errorf("could not set controlling process: %v", err)
} }
} }
return tgt, err return grp, err
} }
// LLDBAttach starts an instance of lldb-server and connects to it, asking // LLDBAttach starts an instance of lldb-server and connects to it, asking
@ -588,7 +588,7 @@ func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs [
// Path is path to the target's executable, path only needs to be specified // Path is path to the target's executable, path only needs to be specified
// for some stubs that do not provide an automated way of determining it // for some stubs that do not provide an automated way of determining it
// (for example debugserver). // (for example debugserver).
func LLDBAttach(pid int, path string, debugInfoDirs []string) (*proc.Target, error) { func LLDBAttach(pid int, path string, debugInfoDirs []string) (*proc.TargetGroup, error) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
return nil, ErrUnsupportedOS return nil, ErrUnsupportedOS
} }
@ -633,13 +633,13 @@ func LLDBAttach(pid int, path string, debugInfoDirs []string) (*proc.Target, err
p := newProcess(process.Process) p := newProcess(process.Process)
p.conn.isDebugserver = isDebugserver p.conn.isDebugserver = isDebugserver
var tgt *proc.Target var grp *proc.TargetGroup
if listener != nil { if listener != nil {
tgt, err = p.Listen(listener, path, pid, debugInfoDirs, proc.StopAttached) grp, err = p.Listen(listener, path, pid, debugInfoDirs, proc.StopAttached)
} else { } else {
tgt, err = p.Dial(port, path, pid, debugInfoDirs, proc.StopAttached) grp, err = p.Dial(port, path, pid, debugInfoDirs, proc.StopAttached)
} }
return tgt, err return grp, err
} }
// EntryPoint will return the process entry point address, useful for // EntryPoint will return the process entry point address, useful for
@ -673,7 +673,7 @@ func (p *gdbProcess) EntryPoint() (uint64, error) {
// initialize uses qProcessInfo to load the inferior's PID and // initialize uses qProcessInfo to load the inferior's PID and
// executable path. This command is not supported by all stubs and not all // executable path. This command is not supported by all stubs and not all
// stubs will report both the PID and executable path. // stubs will report both the PID and executable path.
func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason proc.StopReason) (*proc.Target, error) { func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) {
var err error var err error
if path == "" { if path == "" {
// If we are attaching to a running process and the user didn't specify // If we are attaching to a running process and the user didn't specify
@ -724,19 +724,18 @@ func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason
return nil, err return nil, err
} }
} }
tgt, err := proc.NewTarget(p, p.conn.pid, p.currentThread, proc.NewTargetConfig{ grp, addTarget := proc.NewGroup(p, proc.NewTargetGroupConfig{
Path: path,
DebugInfoDirs: debugInfoDirs, DebugInfoDirs: debugInfoDirs,
DisableAsyncPreempt: runtime.GOOS == "darwin", DisableAsyncPreempt: runtime.GOOS == "darwin",
StopReason: stopReason, StopReason: stopReason,
CanDump: runtime.GOOS == "darwin", CanDump: runtime.GOOS == "darwin",
ContinueOnce: continueOnce,
}) })
_, err = addTarget(p, p.conn.pid, p.currentThread, path, stopReason)
if err != nil { if err != nil {
p.Detach(true) p.Detach(true)
return nil, err return nil, err
} }
return tgt, nil return grp, nil
} }
func queryProcessInfo(p *gdbProcess, pid int) (int, string, error) { func queryProcessInfo(p *gdbProcess, pid int) (int, string, error) {
@ -821,11 +820,7 @@ const (
debugServerTargetExcBreakpoint = 0x96 debugServerTargetExcBreakpoint = 0x96
) )
func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { func (p *gdbProcess) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) {
if len(procs) != 1 {
panic("not implemented")
}
p := procs[0].(*gdbProcess)
if p.exited { if p.exited {
return nil, proc.StopExited, proc.ErrProcessExited{Pid: p.conn.pid} return nil, proc.StopExited, proc.ErrProcessExited{Pid: p.conn.pid}
} }
@ -1307,6 +1302,11 @@ func (p *gdbProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
return p.conn.clearBreakpoint(bp.Addr, watchTypeToBreakpointType(bp.WatchType), kind) return p.conn.clearBreakpoint(bp.Addr, watchTypeToBreakpointType(bp.WatchType), kind)
} }
// FollowExec enables (or disables) follow exec mode
func (p *gdbProcess) FollowExec(bool) error {
return errors.New("follow exec not supported")
}
type threadUpdater struct { type threadUpdater struct {
p *gdbProcess p *gdbProcess
seen map[int]bool seen map[int]bool

@ -124,7 +124,7 @@ func Record(cmd []string, wd string, quiet bool, redirects [3]string) (tracedir
// Replay starts an instance of rr in replay mode, with the specified trace // Replay starts an instance of rr in replay mode, with the specified trace
// directory, and connects to it. // directory, and connects to it.
func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string) (*proc.Target, error) { func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string) (*proc.TargetGroup, error) {
if err := checkRRAvailable(); err != nil { if err := checkRRAvailable(); err != nil {
return nil, err return nil, err
} }
@ -279,7 +279,7 @@ func rrParseGdbCommand(line string) rrInit {
} }
// RecordAndReplay acts like calling Record and then Replay. // RecordAndReplay acts like calling Record and then Replay.
func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string, redirects [3]string) (*proc.Target, string, error) { func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string, redirects [3]string) (*proc.TargetGroup, string, error) {
tracedir, err := Record(cmd, wd, quiet, redirects) tracedir, err := Record(cmd, wd, quiet, redirects)
if tracedir == "" { if tracedir == "" {
return nil, "", err return nil, "", err

@ -30,14 +30,12 @@ func withTestRecording(name string, t testing.TB, fn func(grp *proc.TargetGroup,
t.Skip("test skipped, rr not found") t.Skip("test skipped, rr not found")
} }
t.Log("recording") t.Log("recording")
p, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{}, [3]string{}) grp, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{}, [3]string{})
if err != nil { if err != nil {
t.Fatal("Launch():", err) t.Fatal("Launch():", err)
} }
t.Logf("replaying %q", tracedir) t.Logf("replaying %q", tracedir)
grp := proc.NewGroup(p)
defer grp.Detach(true) defer grp.Detach(true)
fn(grp, fixture) fn(grp, fixture)

@ -7,6 +7,11 @@ import (
"github.com/go-delve/delve/pkg/proc/internal/ebpf" "github.com/go-delve/delve/pkg/proc/internal/ebpf"
) )
// ProcessGroup is a group of processes that are resumed at the same time.
type ProcessGroup interface {
ContinueOnce(*ContinueOnceContext) (Thread, StopReason, error)
}
// Process represents the target of the debugger. This // Process represents the target of the debugger. This
// target could be a system process, core file, etc. // target could be a system process, core file, etc.
// //
@ -57,6 +62,9 @@ type ProcessInternal interface {
// StartCallInjection notifies the backend that we are about to inject a function call. // StartCallInjection notifies the backend that we are about to inject a function call.
StartCallInjection() (func(), error) StartCallInjection() (func(), error)
// FollowExec enables (or disables) follow exec mode
FollowExec(bool) error
} }
// RecordingManipulation is an interface for manipulating process recordings. // RecordingManipulation is an interface for manipulating process recordings.

@ -0,0 +1,11 @@
//go:build !linux
// +build !linux
package native
import "errors"
// FollowExec enables (or disables) follow exec mode
func (*nativeProcess) FollowExec(bool) error {
return errors.New("follow exec not implemented")
}

@ -16,12 +16,12 @@ import (
var ErrNativeBackendDisabled = errors.New("native backend disabled during compilation") var ErrNativeBackendDisabled = errors.New("native backend disabled during compilation")
// Launch returns ErrNativeBackendDisabled. // Launch returns ErrNativeBackendDisabled.
func Launch(_ []string, _ string, _ proc.LaunchFlags, _ []string, _ string, _ [3]string) (*proc.Target, error) { func Launch(_ []string, _ string, _ proc.LaunchFlags, _ []string, _ string, _ [3]string) (*proc.TargetGroup, error) {
return nil, ErrNativeBackendDisabled return nil, ErrNativeBackendDisabled
} }
// Attach returns ErrNativeBackendDisabled. // Attach returns ErrNativeBackendDisabled.
func Attach(_ int, _ []string) (*proc.Target, error) { func Attach(_ int, _ []string) (*proc.TargetGroup, error) {
return nil, ErrNativeBackendDisabled return nil, ErrNativeBackendDisabled
} }
@ -57,11 +57,11 @@ func (dbp *nativeProcess) resume() error {
panic(ErrNativeBackendDisabled) panic(ErrNativeBackendDisabled)
} }
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) { func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
panic(ErrNativeBackendDisabled) panic(ErrNativeBackendDisabled)
} }
func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) { func (*processGroup) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
panic(ErrNativeBackendDisabled) panic(ErrNativeBackendDisabled)
} }

@ -24,11 +24,11 @@ type nativeProcess struct {
// Thread used to read and write memory // Thread used to read and write memory
memthread *nativeThread memthread *nativeThread
os *osProcessDetails os *osProcessDetails
firstStart bool firstStart bool
ptraceChan chan func() ptraceThread *ptraceThread
ptraceDoneChan chan interface{} childProcess bool // this process was launched, not attached to
childProcess bool // this process was launched, not attached to followExec bool // automatically attach to new processes
// Controlling terminal file descriptor for // Controlling terminal file descriptor for
// this process. // this process.
@ -45,19 +45,30 @@ type nativeProcess struct {
// `handlePtraceFuncs`. // `handlePtraceFuncs`.
func newProcess(pid int) *nativeProcess { func newProcess(pid int) *nativeProcess {
dbp := &nativeProcess{ dbp := &nativeProcess{
pid: pid, pid: pid,
threads: make(map[int]*nativeThread), threads: make(map[int]*nativeThread),
breakpoints: proc.NewBreakpointMap(), breakpoints: proc.NewBreakpointMap(),
firstStart: true, firstStart: true,
os: new(osProcessDetails), os: new(osProcessDetails),
ptraceChan: make(chan func()), ptraceThread: newPtraceThread(),
ptraceDoneChan: make(chan interface{}), bi: proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
bi: proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
} }
go dbp.handlePtraceFuncs()
return dbp return dbp
} }
// newChildProcess is like newProcess but uses the same ptrace thread as dbp.
func newChildProcess(dbp *nativeProcess, pid int) *nativeProcess {
return &nativeProcess{
pid: pid,
threads: make(map[int]*nativeThread),
breakpoints: proc.NewBreakpointMap(),
firstStart: true,
os: new(osProcessDetails),
ptraceThread: dbp.ptraceThread.acquire(),
bi: proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
}
}
// BinInfo will return the binary info struct associated with this process. // BinInfo will return the binary info struct associated with this process.
func (dbp *nativeProcess) BinInfo() *proc.BinaryInfo { func (dbp *nativeProcess) BinInfo() *proc.BinaryInfo {
return dbp.bi return dbp.bi
@ -172,23 +183,58 @@ func (dbp *nativeProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
return dbp.memthread.clearSoftwareBreakpoint(bp) return dbp.memthread.clearSoftwareBreakpoint(bp)
} }
func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) { type processGroup struct {
if len(procs) != 1 { procs []*nativeProcess
addTarget proc.AddTargetFunc
}
func (procgrp *processGroup) numValid() int {
n := 0
for _, p := range procgrp.procs {
if ok, _ := p.Valid(); ok {
n++
}
}
return n
}
func (procgrp *processGroup) procForThread(tid int) *nativeProcess {
for _, p := range procgrp.procs {
if p.threads[tid] != nil {
return p
}
}
return nil
}
func (procgrp *processGroup) add(p *nativeProcess, pid int, currentThread proc.Thread, path string, stopReason proc.StopReason) (*proc.Target, error) {
tgt, err := procgrp.addTarget(p, pid, currentThread, path, stopReason)
if err != nil {
return nil, err
}
procgrp.procs = append(procgrp.procs, p)
return tgt, nil
}
func (procgrp *processGroup) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) {
if len(procgrp.procs) != 1 && runtime.GOOS != "linux" {
panic("not implemented") panic("not implemented")
} }
dbp := procs[0].(*nativeProcess) if procgrp.numValid() == 0 {
if dbp.exited { return nil, proc.StopExited, proc.ErrProcessExited{Pid: procgrp.procs[0].pid}
return nil, proc.StopExited, proc.ErrProcessExited{Pid: dbp.pid}
} }
for { for {
for _, dbp := range procgrp.procs {
if err := dbp.resume(); err != nil { if dbp.exited {
return nil, proc.StopUnknown, err continue
} }
if err := dbp.resume(); err != nil {
for _, th := range dbp.threads { return nil, proc.StopUnknown, err
th.CurrentBreakpoint.Clear() }
for _, th := range dbp.threads {
th.CurrentBreakpoint.Clear()
}
} }
if cctx.ResumeChan != nil { if cctx.ResumeChan != nil {
@ -196,16 +242,29 @@ func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext)
cctx.ResumeChan = nil cctx.ResumeChan = nil
} }
trapthread, err := dbp.trapWait(-1) trapthread, err := trapWait(procgrp, -1)
if err != nil { if err != nil {
return nil, proc.StopUnknown, err return nil, proc.StopUnknown, err
} }
trapthread, err = dbp.stop(cctx, trapthread) trapthread, err = procgrp.stop(cctx, trapthread)
if err != nil { if err != nil {
return nil, proc.StopUnknown, err return nil, proc.StopUnknown, err
} }
if trapthread != nil { if trapthread != nil {
dbp := procgrp.procForThread(trapthread.ID)
dbp.memthread = trapthread dbp.memthread = trapthread
// refresh memthread for every other process
for _, p2 := range procgrp.procs {
if p2.exited || p2 == dbp {
continue
}
for _, th := range p2.threads {
p2.memthread = th
if th.SoftExc() {
break
}
}
}
return trapthread, proc.StopUnknown, nil return trapthread, proc.StopUnknown, nil
} }
} }
@ -226,21 +285,26 @@ func (dbp *nativeProcess) FindBreakpoint(pc uint64, adjustPC bool) (*proc.Breakp
return nil, false return nil, false
} }
// initialize will ensure that all relevant information is loaded func (dbp *nativeProcess) initializeBasic() error {
// so the process is ready to be debugged.
func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc.Target, error) {
if err := initialize(dbp); err != nil { if err := initialize(dbp); err != nil {
return nil, err return err
} }
if err := dbp.updateThreadList(); err != nil { if err := dbp.updateThreadList(); err != nil {
return nil, err return err
} }
return nil
}
// initialize will ensure that all relevant information is loaded
// so the process is ready to be debugged.
func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc.TargetGroup, error) {
dbp.initializeBasic()
stopReason := proc.StopLaunched stopReason := proc.StopLaunched
if !dbp.childProcess { if !dbp.childProcess {
stopReason = proc.StopAttached stopReason = proc.StopAttached
} }
tgt, err := proc.NewTarget(dbp, dbp.pid, dbp.memthread, proc.NewTargetConfig{ procgrp := &processGroup{}
Path: path, grp, addTarget := proc.NewGroup(procgrp, proc.NewTargetGroupConfig{
DebugInfoDirs: debugInfoDirs, DebugInfoDirs: debugInfoDirs,
// We disable asyncpreempt for the following reasons: // We disable asyncpreempt for the following reasons:
@ -253,20 +317,21 @@ func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc
// See: https://go-review.googlesource.com/c/go/+/208126 // See: https://go-review.googlesource.com/c/go/+/208126
DisableAsyncPreempt: runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "arm64"), DisableAsyncPreempt: runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "arm64"),
StopReason: stopReason, StopReason: stopReason,
CanDump: runtime.GOOS == "linux" || (runtime.GOOS == "windows" && runtime.GOARCH == "amd64"), CanDump: runtime.GOOS == "linux" || (runtime.GOOS == "windows" && runtime.GOARCH == "amd64"),
ContinueOnce: continueOnce,
}) })
procgrp.addTarget = addTarget
tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, path, stopReason)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if dbp.bi.Arch.Name == "arm64" { if dbp.bi.Arch.Name == "arm64" {
dbp.iscgo = tgt.IsCgo() dbp.iscgo = tgt.IsCgo()
} }
return tgt, nil return grp, nil
} }
func (dbp *nativeProcess) handlePtraceFuncs() { func (pt *ptraceThread) handlePtraceFuncs() {
// We must ensure here that we are running on the same thread during // We must ensure here that we are running on the same thread during
// while invoking the ptrace(2) syscall. This is due to the fact that ptrace(2) expects // while invoking the ptrace(2) syscall. This is due to the fact that ptrace(2) expects
// all commands after PTRACE_ATTACH to come from the same thread. // all commands after PTRACE_ATTACH to come from the same thread.
@ -279,21 +344,20 @@ func (dbp *nativeProcess) handlePtraceFuncs() {
defer runtime.UnlockOSThread() defer runtime.UnlockOSThread()
} }
for fn := range dbp.ptraceChan { for fn := range pt.ptraceChan {
fn() fn()
dbp.ptraceDoneChan <- nil pt.ptraceDoneChan <- nil
} }
} }
func (dbp *nativeProcess) execPtraceFunc(fn func()) { func (dbp *nativeProcess) execPtraceFunc(fn func()) {
dbp.ptraceChan <- fn dbp.ptraceThread.ptraceChan <- fn
<-dbp.ptraceDoneChan <-dbp.ptraceThread.ptraceDoneChan
} }
func (dbp *nativeProcess) postExit() { func (dbp *nativeProcess) postExit() {
dbp.exited = true dbp.exited = true
close(dbp.ptraceChan) dbp.ptraceThread.release()
close(dbp.ptraceDoneChan)
dbp.bi.Close() dbp.bi.Close()
if dbp.ctty != nil { if dbp.ctty != nil {
dbp.ctty.Close() dbp.ctty.Close()
@ -349,3 +413,32 @@ func openRedirects(redirects [3]string, foreground bool) (stdin, stdout, stderr
return stdin, stdout, stderr, closefn, nil return stdin, stdout, stderr, closefn, nil
} }
type ptraceThread struct {
ptraceRefCnt int
ptraceChan chan func()
ptraceDoneChan chan interface{}
}
func newPtraceThread() *ptraceThread {
pt := &ptraceThread{
ptraceChan: make(chan func()),
ptraceDoneChan: make(chan interface{}),
ptraceRefCnt: 1,
}
go pt.handlePtraceFuncs()
return pt
}
func (pt *ptraceThread) acquire() *ptraceThread {
pt.ptraceRefCnt++
return pt
}
func (pt *ptraceThread) release() {
pt.ptraceRefCnt--
if pt.ptraceRefCnt == 0 {
close(pt.ptraceChan)
close(pt.ptraceDoneChan)
}
}

@ -42,7 +42,7 @@ func (os *osProcessDetails) Close() {}
// custom fork/exec process in order to take advantage of // custom fork/exec process in order to take advantage of
// PT_SIGEXC on Darwin which will turn Unix signals into // PT_SIGEXC on Darwin which will turn Unix signals into
// Mach exceptions. // Mach exceptions.
func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ string, _ [3]string) (*proc.Target, error) { func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ string, _ [3]string) (*proc.TargetGroup, error) {
argv0Go, err := filepath.Abs(cmd[0]) argv0Go, err := filepath.Abs(cmd[0])
if err != nil { if err != nil {
return nil, err return nil, err
@ -121,7 +121,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ strin
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, err := dbp.stop(nil, nil); err != nil { if _, err := dbp.stop(nil); err != nil {
return nil, err return nil, err
} }
@ -137,7 +137,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ strin
} }
// Attach to an existing process with the given PID. // Attach to an existing process with the given PID.
func Attach(pid int, _ []string) (*proc.Target, error) { func Attach(pid int, _ []string) (*proc.TargetGroup, error) {
if err := macutil.CheckRosetta(); err != nil { if err := macutil.CheckRosetta(); err != nil {
return nil, err return nil, err
} }
@ -291,6 +291,10 @@ func findExecutable(path string, pid int) string {
return path return path
} }
func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
return procgrp.procs[0].trapWait(pid)
}
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) { func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) {
for { for {
task := dbp.os.task task := dbp.os.task
@ -429,7 +433,11 @@ func (dbp *nativeProcess) resume() error {
} }
// stop stops all running threads and sets breakpoints // stop stops all running threads and sets breakpoints
func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) { func (procgrp *processGroup) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
return procgrp.procs[0].stop(trapthread)
}
func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error) {
if dbp.exited { if dbp.exited {
return nil, proc.ErrProcessExited{Pid: dbp.pid} return nil, proc.ErrProcessExited{Pid: dbp.pid}
} }

@ -55,7 +55,7 @@ func (os *osProcessDetails) Close() {}
// to be supplied to that process. `wd` is working directory of the program. // to be supplied to that process. `wd` is working directory of the program.
// If the DWARF information cannot be found in the binary, Delve will look // If the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in. // for external debug files in the directories passed in.
func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.Target, error) { func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.TargetGroup, error) {
var ( var (
process *exec.Cmd process *exec.Cmd
err error err error
@ -121,7 +121,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []str
// Attach to an existing process with the given PID. Once attached, if // Attach to an existing process with the given PID. Once attached, if
// the DWARF information cannot be found in the binary, Delve will look // the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in. // for external debug files in the directories passed in.
func Attach(pid int, debugInfoDirs []string) (*proc.Target, error) { func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) {
dbp := newProcess(pid) dbp := newProcess(pid)
var err error var err error
@ -227,8 +227,8 @@ func findExecutable(path string, pid int) string {
return path return path
} }
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) { func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
return dbp.trapWaitInternal(pid, trapWaitNormal) return procgrp.procs[0].trapWaitInternal(pid, trapWaitNormal)
} }
type trapWaitMode uint8 type trapWaitMode uint8
@ -403,7 +403,11 @@ func (dbp *nativeProcess) resume() error {
// Used by ContinueOnce // Used by ContinueOnce
// stop stops all running threads and sets breakpoints // stop stops all running threads and sets breakpoints
func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) { func (procgrp *processGroup) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
return procgrp.procs[0].stop(trapthread)
}
func (dbp *nativeProcess) stop(trapthread *nativeThread) (*nativeThread, error) {
if dbp.exited { if dbp.exited {
return nil, proc.ErrProcessExited{Pid: dbp.pid} return nil, proc.ErrProcessExited{Pid: dbp.pid}
} }

@ -62,7 +62,7 @@ func (os *osProcessDetails) Close() {
// to be supplied to that process. `wd` is working directory of the program. // to be supplied to that process. `wd` is working directory of the program.
// If the DWARF information cannot be found in the binary, Delve will look // If the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in. // for external debug files in the directories passed in.
func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.Target, error) { func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.TargetGroup, error) {
var ( var (
process *exec.Cmd process *exec.Cmd
err error err error
@ -141,7 +141,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []str
// Attach to an existing process with the given PID. Once attached, if // Attach to an existing process with the given PID. Once attached, if
// the DWARF information cannot be found in the binary, Delve will look // the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in. // for external debug files in the directories passed in.
func Attach(pid int, debugInfoDirs []string) (*proc.Target, error) { func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) {
dbp := newProcess(pid) dbp := newProcess(pid)
var err error var err error
@ -237,6 +237,11 @@ func (dbp *nativeProcess) requestManualStop() (err error) {
return sys.Kill(dbp.pid, sys.SIGTRAP) return sys.Kill(dbp.pid, sys.SIGTRAP)
} }
const (
ptraceOptionsNormal = syscall.PTRACE_O_TRACECLONE
ptraceOptionsFollowExec = syscall.PTRACE_O_TRACECLONE | syscall.PTRACE_O_TRACEVFORK | syscall.PTRACE_O_TRACEEXEC
)
// Attach to a newly created thread, and store that thread in our list of // Attach to a newly created thread, and store that thread in our list of
// known threads. // known threads.
func (dbp *nativeProcess) addThread(tid int, attach bool) (*nativeThread, error) { func (dbp *nativeProcess) addThread(tid int, attach bool) (*nativeThread, error) {
@ -244,6 +249,11 @@ func (dbp *nativeProcess) addThread(tid int, attach bool) (*nativeThread, error)
return thread, nil return thread, nil
} }
ptraceOptions := ptraceOptionsNormal
if dbp.followExec {
ptraceOptions = ptraceOptionsFollowExec
}
var err error var err error
if attach { if attach {
dbp.execPtraceFunc(func() { err = sys.PtraceAttach(tid) }) dbp.execPtraceFunc(func() { err = sys.PtraceAttach(tid) })
@ -263,12 +273,12 @@ func (dbp *nativeProcess) addThread(tid int, attach bool) (*nativeThread, error)
} }
} }
dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, syscall.PTRACE_O_TRACECLONE) }) dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })
if err == syscall.ESRCH { if err == syscall.ESRCH {
if _, _, err = dbp.waitFast(tid); err != nil { if _, _, err = dbp.waitFast(tid); err != nil {
return nil, fmt.Errorf("error while waiting after adding thread: %d %s", tid, err) return nil, fmt.Errorf("error while waiting after adding thread: %d %s", tid, err)
} }
dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, syscall.PTRACE_O_TRACECLONE) }) dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, ptraceOptions) })
if err == syscall.ESRCH { if err == syscall.ESRCH {
return nil, err return nil, err
} }
@ -318,8 +328,8 @@ func findExecutable(path string, pid int) string {
return path return path
} }
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) { func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
return dbp.trapWaitInternal(pid, 0) return trapWaitInternal(procgrp, pid, 0)
} }
type trapWaitOptions uint8 type trapWaitOptions uint8
@ -330,14 +340,21 @@ const (
trapWaitDontCallExitGuard trapWaitDontCallExitGuard
) )
func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*nativeThread, error) { func trapWaitInternal(procgrp *processGroup, pid int, options trapWaitOptions) (*nativeThread, error) {
var waitdbp *nativeProcess = nil
if len(procgrp.procs) == 1 {
// Note that waitdbp is only used to call (*nativeProcess).wait which will
// behave correctly if waitdbp == nil.
waitdbp = procgrp.procs[0]
}
halt := options&trapWaitHalt != 0 halt := options&trapWaitHalt != 0
for { for {
wopt := 0 wopt := 0
if options&trapWaitNohang != 0 { if options&trapWaitNohang != 0 {
wopt = sys.WNOHANG wopt = sys.WNOHANG
} }
wpid, status, err := dbp.wait(pid, wopt) wpid, status, err := waitdbp.wait(pid, wopt)
if err != nil { if err != nil {
return nil, fmt.Errorf("wait err %s %d", err, pid) return nil, fmt.Errorf("wait err %s %d", err, pid)
} }
@ -347,14 +364,27 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
} }
continue continue
} }
th, ok := dbp.threads[wpid] dbp := procgrp.procForThread(wpid)
if ok { var th *nativeThread
th.Status = (*waitStatus)(status) if dbp != nil {
var ok bool
th, ok = dbp.threads[wpid]
if ok {
th.Status = (*waitStatus)(status)
}
} else {
dbp = procgrp.procs[0]
} }
if status.Exited() { if status.Exited() {
if wpid == dbp.pid { if wpid == dbp.pid {
dbp.postExit() dbp.postExit()
return nil, proc.ErrProcessExited{Pid: wpid, Status: status.ExitStatus()} if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: wpid, Status: status.ExitStatus()}
}
if halt {
return nil, nil
}
continue
} }
delete(dbp.threads, wpid) delete(dbp.threads, wpid)
continue continue
@ -363,15 +393,24 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
// Signaled means the thread was terminated due to a signal. // Signaled means the thread was terminated due to a signal.
if wpid == dbp.pid { if wpid == dbp.pid {
dbp.postExit() dbp.postExit()
return nil, proc.ErrProcessExited{Pid: wpid, Status: -int(status.Signal())} if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: wpid, Status: -int(status.Signal())}
}
if halt {
return nil, nil
}
continue
} }
// does this ever happen? // does this ever happen?
delete(dbp.threads, wpid) delete(dbp.threads, wpid)
continue continue
} }
if status.StopSignal() == sys.SIGTRAP && status.TrapCause() == sys.PTRACE_EVENT_CLONE { if status.StopSignal() == sys.SIGTRAP && (status.TrapCause() == sys.PTRACE_EVENT_CLONE || status.TrapCause() == sys.PTRACE_EVENT_VFORK) {
// A traced thread has cloned a new thread, grab the pid and // A traced thread has cloned a new thread, grab the pid and
// add it to our list of traced threads. // add it to our list of traced threads.
// If TrapCause() is sys.PTRACE_EVENT_VFORK this is actually a new
// process, but treat it as a normal thread until exec happens, so that
// we can initialize the new process normally.
var cloned uint var cloned uint
dbp.execPtraceFunc(func() { cloned, err = sys.PtraceGetEventMsg(wpid) }) dbp.execPtraceFunc(func() { cloned, err = sys.PtraceGetEventMsg(wpid) })
if err != nil { if err != nil {
@ -410,6 +449,34 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
} }
continue continue
} }
if status.StopSignal() == sys.SIGTRAP && (status.TrapCause() == sys.PTRACE_EVENT_EXEC) {
// A thread called exec and we now have a new process. Retrieve the
// thread ID of the exec'ing thread with PtraceGetEventMsg to remove it
// and create a new nativeProcess object to track the new process.
var tid uint
dbp.execPtraceFunc(func() { tid, err = sys.PtraceGetEventMsg(wpid) })
if err == nil {
delete(dbp.threads, int(tid))
}
dbp = newChildProcess(procgrp.procs[0], wpid)
dbp.followExec = true
dbp.initializeBasic()
_, err := procgrp.add(dbp, dbp.pid, dbp.memthread, findExecutable("", dbp.pid), proc.StopLaunched)
if err != nil {
_ = dbp.Detach(false)
return nil, err
}
if halt {
return nil, nil
}
// TODO(aarzilli): if we want to give users the ability to stop the target
// group on exec here is where we should return
err = dbp.threads[dbp.pid].Continue()
if err != nil {
return nil, err
}
continue
}
if th == nil { if th == nil {
// Sometimes we get an unknown thread, ignore it? // Sometimes we get an unknown thread, ignore it?
continue continue
@ -439,7 +506,10 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
// do the same thing we do if a thread quit // do the same thing we do if a thread quit
if wpid == dbp.pid { if wpid == dbp.pid {
dbp.postExit() dbp.postExit()
return nil, proc.ErrProcessExited{Pid: wpid, Status: status.ExitStatus()} if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: wpid, Status: status.ExitStatus()}
}
continue
} }
delete(dbp.threads, wpid) delete(dbp.threads, wpid)
} }
@ -476,7 +546,7 @@ func (dbp *nativeProcess) waitFast(pid int) (int, *sys.WaitStatus, error) {
func (dbp *nativeProcess) wait(pid, options int) (int, *sys.WaitStatus, error) { func (dbp *nativeProcess) wait(pid, options int) (int, *sys.WaitStatus, error) {
var s sys.WaitStatus var s sys.WaitStatus
if (pid != dbp.pid) || (options != 0) { if (dbp == nil) || (pid != dbp.pid) || (options != 0) {
wpid, err := sys.Wait4(pid, &s, sys.WALL|options, nil) wpid, err := sys.Wait4(pid, &s, sys.WALL|options, nil)
return wpid, &s, err return wpid, &s, err
} }
@ -506,12 +576,12 @@ func (dbp *nativeProcess) wait(pid, options int) (int, *sys.WaitStatus, error) {
} }
} }
func (dbp *nativeProcess) exitGuard(err error) error { func exitGuard(dbp *nativeProcess, procgrp *processGroup, err error) error {
if err != sys.ESRCH { if err != sys.ESRCH {
return err return err
} }
if status(dbp.pid, dbp.os.comm) == statusZombie { if status(dbp.pid, dbp.os.comm) == statusZombie {
_, err := dbp.trapWaitInternal(-1, trapWaitDontCallExitGuard) _, err := trapWaitInternal(procgrp, -1, trapWaitDontCallExitGuard)
return err return err
} }
@ -538,21 +608,27 @@ func (dbp *nativeProcess) resume() error {
} }
// stop stops all running threads and sets breakpoints // stop stops all running threads and sets breakpoints
func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) { func (procgrp *processGroup) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
if dbp.exited { if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: dbp.pid} return nil, proc.ErrProcessExited{Pid: procgrp.procs[0].pid}
} }
for _, th := range dbp.threads { for _, dbp := range procgrp.procs {
th.os.setbp = false if dbp.exited {
continue
}
for _, th := range dbp.threads {
th.os.setbp = false
}
} }
trapthread.os.setbp = true trapthread.os.setbp = true
// check if any other thread simultaneously received a SIGTRAP // check if any other thread simultaneously received a SIGTRAP
for { for {
th, err := dbp.trapWaitInternal(-1, trapWaitNohang) th, err := trapWaitInternal(procgrp, -1, trapWaitNohang)
if err != nil { if err != nil {
return nil, dbp.exitGuard(err) p := procgrp.procForThread(th.ID)
return nil, exitGuard(p, procgrp, err)
} }
if th == nil { if th == nil {
break break
@ -560,10 +636,20 @@ func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativ
} }
// stop all threads that are still running // stop all threads that are still running
for _, th := range dbp.threads { for _, dbp := range procgrp.procs {
if th.os.running { if dbp.exited {
if err := th.stop(); err != nil { continue
return nil, dbp.exitGuard(err) }
for _, th := range dbp.threads {
if th.os.running {
if err := th.stop(); err != nil {
if err == sys.ESRCH {
// thread exited
delete(dbp.threads, th.ID)
} else {
return nil, exitGuard(dbp, procgrp, err)
}
}
} }
} }
} }
@ -571,26 +657,60 @@ func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativ
// wait for all threads to stop // wait for all threads to stop
for { for {
allstopped := true allstopped := true
for _, th := range dbp.threads { for _, dbp := range procgrp.procs {
if th.os.running { if dbp.exited {
allstopped = false continue
break }
for _, th := range dbp.threads {
if th.os.running {
allstopped = false
break
}
} }
} }
if allstopped { if allstopped {
break break
} }
_, err := dbp.trapWaitInternal(-1, trapWaitHalt) _, err := trapWaitInternal(procgrp, -1, trapWaitHalt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
if err := linutil.ElfUpdateSharedObjects(dbp); err != nil { switchTrapthread := false
return nil, err
for _, dbp := range procgrp.procs {
if dbp.exited {
continue
}
err := stop1(cctx, dbp, trapthread, &switchTrapthread)
if err != nil {
return nil, err
}
} }
switchTrapthread := false if switchTrapthread {
trapthreadID := trapthread.ID
trapthread = nil
for _, dbp := range procgrp.procs {
if dbp.exited {
continue
}
for _, th := range dbp.threads {
if th.os.setbp && th.ThreadID() != trapthreadID {
return th, nil
}
}
}
}
return trapthread, nil
}
func stop1(cctx *proc.ContinueOnceContext, dbp *nativeProcess, trapthread *nativeThread, switchTrapthread *bool) error {
if err := linutil.ElfUpdateSharedObjects(dbp); err != nil {
return err
}
// set breakpoints on SIGTRAP threads // set breakpoints on SIGTRAP threads
var err1 error var err1 error
@ -649,27 +769,13 @@ func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativ
// Will switch to a different thread for trapthread because we don't // Will switch to a different thread for trapthread because we don't
// want pkg/proc to believe that this thread was stopped by a // want pkg/proc to believe that this thread was stopped by a
// hardcoded breakpoint. // hardcoded breakpoint.
switchTrapthread = true *switchTrapthread = true
} }
} }
} }
} }
} }
if err1 != nil { return err1
return nil, err1
}
if switchTrapthread {
trapthreadID := trapthread.ID
trapthread = nil
for _, th := range dbp.threads {
if th.os.setbp && th.ThreadID() != trapthreadID {
return th, nil
}
}
}
return trapthread, nil
} }
func (dbp *nativeProcess) detach(kill bool) error { func (dbp *nativeProcess) detach(kill bool) error {
@ -788,6 +894,25 @@ func (dbp *nativeProcess) SetUProbe(fnName string, goidOffset int64, args []ebpf
return dbp.os.ebpf.AttachUprobe(dbp.pid, debugname, off) return dbp.os.ebpf.AttachUprobe(dbp.pid, debugname, off)
} }
// FollowExec enables (or disables) follow exec mode
func (dbp *nativeProcess) FollowExec(v bool) error {
dbp.followExec = v
ptraceOptions := ptraceOptionsNormal
if dbp.followExec {
ptraceOptions = ptraceOptionsFollowExec
}
var err error
dbp.execPtraceFunc(func() {
for tid := range dbp.threads {
err = syscall.PtraceSetOptions(tid, ptraceOptions)
if err != nil {
return
}
}
})
return err
}
func killProcess(pid int) error { func killProcess(pid int) error {
return sys.Kill(pid, sys.SIGINT) return sys.Kill(pid, sys.SIGINT)
} }

@ -23,7 +23,7 @@ type osProcessDetails struct {
func (os *osProcessDetails) Close() {} func (os *osProcessDetails) Close() {}
// Launch creates and begins debugging a new process. // Launch creates and begins debugging a new process.
func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ string, redirects [3]string) (*proc.Target, error) { func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ string, redirects [3]string) (*proc.TargetGroup, error) {
argv0Go := cmd[0] argv0Go := cmd[0]
env := proc.DisableAsyncPreemptEnv() env := proc.DisableAsyncPreemptEnv()
@ -140,7 +140,7 @@ func findExePath(pid int) (string, error) {
var debugPrivilegeRequested = false var debugPrivilegeRequested = false
// Attach to an existing process with the given PID. // Attach to an existing process with the given PID.
func Attach(pid int, _ []string) (*proc.Target, error) { func Attach(pid int, _ []string) (*proc.TargetGroup, error) {
var aperr error var aperr error
if !debugPrivilegeRequested { if !debugPrivilegeRequested {
debugPrivilegeRequested = true debugPrivilegeRequested = true
@ -427,7 +427,8 @@ func (dbp *nativeProcess) waitForDebugEvent(flags waitForDebugEventFlags) (threa
} }
} }
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) { func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
dbp := procgrp.procs[0]
var err error var err error
var tid, exitCode int var tid, exitCode int
dbp.execPtraceFunc(func() { dbp.execPtraceFunc(func() {
@ -474,7 +475,8 @@ func (dbp *nativeProcess) resume() error {
} }
// stop stops all running threads threads and sets breakpoints // stop stops all running threads threads and sets breakpoints
func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) { func (procgrp *processGroup) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
dbp := procgrp.procs[0]
if dbp.exited { if dbp.exited {
return nil, proc.ErrProcessExited{Pid: dbp.pid} return nil, proc.ErrProcessExited{Pid: dbp.pid}
} }

@ -22,6 +22,9 @@ type osSpecificDetails struct {
func (t *nativeThread) stop() (err error) { func (t *nativeThread) stop() (err error) {
err = sys.Tgkill(t.dbp.pid, t.ID, sys.SIGSTOP) err = sys.Tgkill(t.dbp.pid, t.ID, sys.SIGSTOP)
if err != nil { if err != nil {
if err == sys.ESRCH {
return
}
err = fmt.Errorf("stop err %s on thread %d", err, t.ID) err = fmt.Errorf("stop err %s on thread %d", err, t.ID)
return return
} }

@ -1,42 +0,0 @@
package proc
// Wrapper functions so that most tests proc_test.go don't need to worry
// about TargetGroup when they just need to resume a single process.
func newGroupTransient(tgt *Target) *TargetGroup {
grp := NewGroup(tgt)
tgt.partOfGroup = false
return grp
}
func (tgt *Target) Detach(kill bool) error {
return tgt.detach(kill)
}
func (tgt *Target) Continue() error {
return newGroupTransient(tgt).Continue()
}
func (tgt *Target) Next() error {
return newGroupTransient(tgt).Next()
}
func (tgt *Target) Step() error {
return newGroupTransient(tgt).Step()
}
func (tgt *Target) StepOut() error {
return newGroupTransient(tgt).StepOut()
}
func (tgt *Target) ChangeDirection(dir Direction) error {
return tgt.recman.ChangeDirection(dir)
}
func (tgt *Target) StepInstruction() error {
return newGroupTransient(tgt).StepInstruction()
}
func (tgt *Target) Recorded() (bool, string) {
return tgt.recman.Recorded()
}

File diff suppressed because it is too large Load Diff

@ -36,8 +36,7 @@ func TestIssue419(t *testing.T) {
errChan := make(chan error, 2) errChan := make(chan error, 2)
// SIGINT directed at the inferior should be passed along not swallowed by delve // SIGINT directed at the inferior should be passed along not swallowed by delve
withTestProcess("issue419", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("issue419", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
grp := proc.NewGroup(p)
setFunctionBreakpoint(p, t, "main.main") setFunctionBreakpoint(p, t, "main.main")
assertNoError(grp.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
resumeChan := make(chan struct{}, 1) resumeChan := make(chan struct{}, 1)

@ -23,8 +23,8 @@ func TestScopeWithEscapedVariable(t *testing.T) {
return return
} }
withTestProcess("scopeescapevareval", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("scopeescapevareval", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue") assertNoError(grp.Continue(), t, "Continue")
// On the breakpoint there are two 'a' variables in scope, the one that // On the breakpoint there are two 'a' variables in scope, the one that
// isn't shadowed is a variable that escapes to the heap and figures in // isn't shadowed is a variable that escapes to the heap and figures in
@ -72,7 +72,7 @@ func TestScope(t *testing.T) {
scopeChecks := getScopeChecks(scopetestPath, t) scopeChecks := getScopeChecks(scopetestPath, t)
withTestProcess("scopetest", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("scopetest", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
for i := range scopeChecks { for i := range scopeChecks {
setFileBreakpoint(p, t, fixture.Source, scopeChecks[i].line) setFileBreakpoint(p, t, fixture.Source, scopeChecks[i].line)
} }
@ -80,7 +80,7 @@ func TestScope(t *testing.T) {
t.Logf("%d breakpoints set", len(scopeChecks)) t.Logf("%d breakpoints set", len(scopeChecks))
for { for {
if err := p.Continue(); err != nil { if err := grp.Continue(); err != nil {
if _, exited := err.(proc.ErrProcessExited); exited { if _, exited := err.(proc.ErrProcessExited); exited {
break break
} }

@ -38,9 +38,8 @@ const (
type Target struct { type Target struct {
Process Process
proc ProcessInternal proc ProcessInternal
recman RecordingManipulationInternal recman RecordingManipulationInternal
continueOnce ContinueOnceFunc
pid int pid int
@ -49,9 +48,6 @@ type Target struct {
// case only one will be reported. // case only one will be reported.
StopReason StopReason StopReason StopReason
// CanDump is true if core dumping is supported.
CanDump bool
// currentThread is the thread that will be used by next/step/stepout and to evaluate variables if no goroutine is selected. // currentThread is the thread that will be used by next/step/stepout and to evaluate variables if no goroutine is selected.
currentThread Thread currentThread Thread
@ -148,18 +144,6 @@ const (
StopWatchpoint // The target process hit one or more watchpoints StopWatchpoint // The target process hit one or more watchpoints
) )
type ContinueOnceFunc func([]ProcessInternal, *ContinueOnceContext) (trapthread Thread, stopReason StopReason, err error)
// NewTargetConfig contains the configuration for a new Target object,
type NewTargetConfig struct {
Path string // path of the main executable
DebugInfoDirs []string // Directories to search for split debug info
DisableAsyncPreempt bool // Go 1.14 asynchronous preemption should be disabled
StopReason StopReason // Initial stop reason
CanDump bool // Can create core dumps (must implement ProcessInternal.MemoryMap)
ContinueOnce ContinueOnceFunc
}
// DisableAsyncPreemptEnv returns a process environment (like os.Environ) // DisableAsyncPreemptEnv returns a process environment (like os.Environ)
// where asyncpreemptoff is set to 1. // where asyncpreemptoff is set to 1.
func DisableAsyncPreemptEnv() []string { func DisableAsyncPreemptEnv() []string {
@ -174,15 +158,15 @@ func DisableAsyncPreemptEnv() []string {
return env return env
} }
// NewTarget returns an initialized Target object. // newTarget returns an initialized Target object.
// The p argument can optionally implement the RecordingManipulation interface. // The p argument can optionally implement the RecordingManipulation interface.
func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetConfig) (*Target, error) { func (grp *TargetGroup) newTarget(p ProcessInternal, pid int, currentThread Thread, path string) (*Target, error) {
entryPoint, err := p.EntryPoint() entryPoint, err := p.EntryPoint()
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = p.BinInfo().LoadBinaryInfo(cfg.Path, entryPoint, cfg.DebugInfoDirs) err = p.BinInfo().LoadBinaryInfo(path, entryPoint, grp.cfg.DebugInfoDirs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -196,11 +180,8 @@ func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetCo
Process: p, Process: p,
proc: p, proc: p,
fncallForG: make(map[int64]*callInjection), fncallForG: make(map[int64]*callInjection),
StopReason: cfg.StopReason,
currentThread: currentThread, currentThread: currentThread,
CanDump: cfg.CanDump,
pid: pid, pid: pid,
continueOnce: cfg.ContinueOnce,
} }
if recman, ok := p.(RecordingManipulationInternal); ok { if recman, ok := p.(RecordingManipulationInternal); ok {
@ -219,7 +200,7 @@ func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetCo
t.gcache.init(p.BinInfo()) t.gcache.init(p.BinInfo())
t.fakeMemoryRegistryMap = make(map[string]*compositeMemory) t.fakeMemoryRegistryMap = make(map[string]*compositeMemory)
if cfg.DisableAsyncPreempt { if grp.cfg.DisableAsyncPreempt {
setAsyncPreemptOff(t, 1) setAsyncPreemptOff(t, 1)
} }

@ -48,48 +48,55 @@ func (grp *TargetGroup) Next() (err error) {
// processes. It will continue until it hits a breakpoint // processes. It will continue until it hits a breakpoint
// or is otherwise stopped. // or is otherwise stopped.
func (grp *TargetGroup) Continue() error { func (grp *TargetGroup) Continue() error {
if len(grp.targets) != 1 { if grp.numValid() == 0 {
panic("multiple targets not implemented") _, err := grp.targets[0].Valid()
}
dbp := grp.Selected
if _, err := dbp.Valid(); err != nil {
return err return err
} }
for _, thread := range dbp.ThreadList() { for _, dbp := range grp.targets {
thread.Common().CallReturn = false if isvalid, _ := dbp.Valid(); !isvalid {
thread.Common().returnValues = nil continue
}
for _, thread := range dbp.ThreadList() {
thread.Common().CallReturn = false
thread.Common().returnValues = nil
}
dbp.Breakpoints().WatchOutOfScope = nil
dbp.clearHardcodedBreakpoints()
} }
dbp.Breakpoints().WatchOutOfScope = nil
dbp.clearHardcodedBreakpoints()
grp.cctx.CheckAndClearManualStopRequest() grp.cctx.CheckAndClearManualStopRequest()
defer func() { defer func() {
// Make sure we clear internal breakpoints if we simultaneously receive a // Make sure we clear internal breakpoints if we simultaneously receive a
// manual stop request and hit a breakpoint. // manual stop request and hit a breakpoint.
if grp.cctx.CheckAndClearManualStopRequest() { if grp.cctx.CheckAndClearManualStopRequest() {
dbp.StopReason = StopManual grp.finishManualStop()
dbp.clearHardcodedBreakpoints()
if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 {
dbp.ClearSteppingBreakpoints()
}
} }
}() }()
for { for {
if grp.cctx.CheckAndClearManualStopRequest() { if grp.cctx.CheckAndClearManualStopRequest() {
dbp.StopReason = StopManual grp.finishManualStop()
dbp.clearHardcodedBreakpoints()
if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 {
dbp.ClearSteppingBreakpoints()
}
return nil return nil
} }
dbp.ClearCaches() for _, dbp := range grp.targets {
trapthread, stopReason, contOnceErr := grp.continueOnce([]ProcessInternal{grp.targets[0].proc}, grp.cctx) dbp.ClearCaches()
dbp.StopReason = stopReason }
trapthread, stopReason, contOnceErr := grp.procgrp.ContinueOnce(grp.cctx)
var traptgt *Target
if trapthread != nil {
traptgt = grp.TargetForThread(trapthread.ThreadID())
if traptgt == nil {
return fmt.Errorf("could not find target for thread %d", trapthread.ThreadID())
}
} else {
traptgt = grp.targets[0]
}
traptgt.StopReason = stopReason
threads := dbp.ThreadList() it := ValidTargets{Group: grp}
for _, thread := range threads { for it.Next() {
if thread.Breakpoint().Breakpoint != nil { for _, thread := range it.ThreadList() {
thread.Breakpoint().Breakpoint.checkCondition(dbp, thread, thread.Breakpoint()) if thread.Breakpoint().Breakpoint != nil {
thread.Breakpoint().Breakpoint.checkCondition(it.Target, thread, thread.Breakpoint())
}
} }
} }
@ -98,31 +105,56 @@ func (grp *TargetGroup) Continue() error {
// Issue #2078. // Issue #2078.
// Errors are ignored because depending on why ContinueOnce failed this // Errors are ignored because depending on why ContinueOnce failed this
// might very well not work. // might very well not work.
if valid, _ := dbp.Valid(); valid { _ = grp.setCurrentThreads(traptgt, trapthread)
if trapthread != nil {
_ = dbp.SwitchThread(trapthread.ThreadID())
} else if curth := dbp.CurrentThread(); curth != nil {
dbp.selectedGoroutine, _ = GetG(curth)
}
}
if pe, ok := contOnceErr.(ErrProcessExited); ok { if pe, ok := contOnceErr.(ErrProcessExited); ok {
dbp.exitStatus = pe.Status traptgt.exitStatus = pe.Status
} }
return contOnceErr return contOnceErr
} }
if dbp.StopReason == StopLaunched { if stopReason == StopLaunched {
dbp.ClearSteppingBreakpoints() it.Reset()
for it.Next() {
it.Target.ClearSteppingBreakpoints()
}
} }
callInjectionDone, callErr := callInjectionProtocol(dbp, threads) var callInjectionDone bool
hcbpErr := dbp.handleHardcodedBreakpoints(trapthread, threads) var callErr error
var hcbpErr error
it.Reset()
for it.Next() {
dbp := it.Target
threads := dbp.ThreadList()
callInjectionDoneThis, callErrThis := callInjectionProtocol(dbp, threads)
callInjectionDone = callInjectionDone || callInjectionDoneThis
if callInjectionDoneThis {
dbp.StopReason = StopCallReturned
}
if callErrThis != nil && callErr == nil {
callErr = callErrThis
}
hcbpErrThis := dbp.handleHardcodedBreakpoints(trapthread, threads)
if hcbpErrThis != nil && hcbpErr == nil {
hcbpErr = hcbpErrThis
}
}
// callErr and hcbpErr check delayed until after pickCurrentThread, which // callErr and hcbpErr check delayed until after pickCurrentThread, which
// must always happen, otherwise the debugger could be left in an // must always happen, otherwise the debugger could be left in an
// inconsistent state. // inconsistent state.
if err := pickCurrentThread(dbp, trapthread, threads); err != nil { it = ValidTargets{Group: grp}
return err for it.Next() {
var th Thread = nil
if it.Target == traptgt {
th = trapthread
}
err := pickCurrentThread(it.Target, th)
if err != nil {
return err
}
} }
grp.pickCurrentTarget(traptgt)
dbp := grp.Selected
if callErr != nil { if callErr != nil {
return callErr return callErr
@ -138,7 +170,7 @@ func (grp *TargetGroup) Continue() error {
case curbp.Active && curbp.Stepping: case curbp.Active && curbp.Stepping:
if curbp.SteppingInto { if curbp.SteppingInto {
// See description of proc.(*Process).next for the meaning of StepBreakpoints // See description of proc.(*Process).next for the meaning of StepBreakpoints
if err := conditionErrors(threads); err != nil { if err := conditionErrors(grp); err != nil {
return err return err
} }
if grp.GetDirection() == Forward { if grp.GetDirection() == Forward {
@ -168,7 +200,7 @@ func (grp *TargetGroup) Continue() error {
return err return err
} }
dbp.StopReason = StopNextFinished dbp.StopReason = StopNextFinished
return conditionErrors(threads) return conditionErrors(grp)
} }
case curbp.Active: case curbp.Active:
onNextGoroutine, err := onNextGoroutine(dbp, curthread, dbp.Breakpoints()) onNextGoroutine, err := onNextGoroutine(dbp, curthread, dbp.Breakpoints())
@ -191,19 +223,58 @@ func (grp *TargetGroup) Continue() error {
if curbp.Breakpoint.WatchType != 0 { if curbp.Breakpoint.WatchType != 0 {
dbp.StopReason = StopWatchpoint dbp.StopReason = StopWatchpoint
} }
return conditionErrors(threads) return conditionErrors(grp)
default: default:
// not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat // not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat
} }
if callInjectionDone { if callInjectionDone {
// a call injection was finished, don't let a breakpoint with a failed // a call injection was finished, don't let a breakpoint with a failed
// condition or a step breakpoint shadow this. // condition or a step breakpoint shadow this.
dbp.StopReason = StopCallReturned return conditionErrors(grp)
return conditionErrors(threads)
} }
} }
} }
func (grp *TargetGroup) finishManualStop() {
for _, dbp := range grp.targets {
if isvalid, _ := dbp.Valid(); !isvalid {
continue
}
dbp.StopReason = StopManual
dbp.clearHardcodedBreakpoints()
if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 {
dbp.ClearSteppingBreakpoints()
}
}
}
// setCurrentThreads switches traptgt to trapthread, then for each target in
// the group if its current thread exists it refreshes the current
// goroutine, otherwise it switches it to a randomly selected thread.
func (grp *TargetGroup) setCurrentThreads(traptgt *Target, trapthread Thread) error {
var err error
if traptgt != nil && trapthread != nil {
err = traptgt.SwitchThread(trapthread.ThreadID())
}
for _, tgt := range grp.targets {
if isvalid, _ := tgt.Valid(); !isvalid {
continue
}
if _, ok := tgt.FindThread(tgt.currentThread.ThreadID()); ok {
tgt.selectedGoroutine, _ = GetG(tgt.currentThread)
} else {
threads := tgt.ThreadList()
if len(threads) > 0 {
err1 := tgt.SwitchThread(threads[0].ThreadID())
if err1 != nil && err == nil {
err = err1
}
}
}
}
return err
}
func isTraceOrTraceReturn(bp *Breakpoint) bool { func isTraceOrTraceReturn(bp *Breakpoint) bool {
if bp.Logical == nil { if bp.Logical == nil {
return false return false
@ -211,14 +282,19 @@ func isTraceOrTraceReturn(bp *Breakpoint) bool {
return bp.Logical.Tracepoint || bp.Logical.TraceReturn return bp.Logical.Tracepoint || bp.Logical.TraceReturn
} }
func conditionErrors(threads []Thread) error { func conditionErrors(grp *TargetGroup) error {
var condErr error var condErr error
for _, th := range threads { for _, dbp := range grp.targets {
if bp := th.Breakpoint(); bp.Breakpoint != nil && bp.CondError != nil { if isvalid, _ := dbp.Valid(); !isvalid {
if condErr == nil { continue
condErr = bp.CondError }
} else { for _, th := range dbp.ThreadList() {
return fmt.Errorf("multiple errors evaluating conditions") if bp := th.Breakpoint(); bp.Breakpoint != nil && bp.CondError != nil {
if condErr == nil {
condErr = bp.CondError
} else {
return fmt.Errorf("multiple errors evaluating conditions")
}
} }
} }
} }
@ -226,24 +302,88 @@ func conditionErrors(threads []Thread) error {
} }
// pick a new dbp.currentThread, with the following priority: // pick a new dbp.currentThread, with the following priority:
//
// - a thread with an active stepping breakpoint // - a thread with an active stepping breakpoint
// - a thread with an active breakpoint, prioritizing trapthread // - a thread with an active breakpoint, prioritizing trapthread
// - trapthread // - trapthread if it is not nil
func pickCurrentThread(dbp *Target, trapthread Thread, threads []Thread) error { // - the previous current thread if it still exists
// - a randomly selected thread
func pickCurrentThread(dbp *Target, trapthread Thread) error {
threads := dbp.ThreadList()
for _, th := range threads { for _, th := range threads {
if bp := th.Breakpoint(); bp.Active && bp.Stepping { if bp := th.Breakpoint(); bp.Active && bp.Stepping {
return dbp.SwitchThread(th.ThreadID()) return dbp.SwitchThread(th.ThreadID())
} }
} }
if bp := trapthread.Breakpoint(); bp.Active { if trapthread != nil {
return dbp.SwitchThread(trapthread.ThreadID()) if bp := trapthread.Breakpoint(); bp.Active {
return dbp.SwitchThread(trapthread.ThreadID())
}
} }
for _, th := range threads { for _, th := range threads {
if bp := th.Breakpoint(); bp.Active { if bp := th.Breakpoint(); bp.Active {
return dbp.SwitchThread(th.ThreadID()) return dbp.SwitchThread(th.ThreadID())
} }
} }
return dbp.SwitchThread(trapthread.ThreadID()) if trapthread != nil {
return dbp.SwitchThread(trapthread.ThreadID())
}
if _, ok := dbp.FindThread(dbp.currentThread.ThreadID()); ok {
dbp.selectedGoroutine, _ = GetG(dbp.currentThread)
return nil
}
if len(threads) > 0 {
return dbp.SwitchThread(threads[0].ThreadID())
}
return nil
}
// pickCurrentTarget picks a new current target, with the following property:
//
// - a target with an active stepping breakpoint
// - a target with StopReason == StopCallReturned
// - a target with an active breakpoint, prioritizing traptgt
// - traptgt
func (grp *TargetGroup) pickCurrentTarget(traptgt *Target) {
if len(grp.targets) == 1 {
grp.Selected = grp.targets[0]
return
}
for _, dbp := range grp.targets {
if isvalid, _ := dbp.Valid(); !isvalid {
continue
}
bp := dbp.currentThread.Breakpoint()
if bp.Active && bp.Stepping {
grp.Selected = dbp
return
}
}
for _, dbp := range grp.targets {
if isvalid, _ := dbp.Valid(); !isvalid {
continue
}
if dbp.StopReason == StopCallReturned {
grp.Selected = dbp
return
}
}
if traptgt.currentThread.Breakpoint().Active {
grp.Selected = traptgt
return
}
for _, dbp := range grp.targets {
if isvalid, _ := dbp.Valid(); !isvalid {
continue
}
bp := dbp.currentThread.Breakpoint()
if bp.Active {
grp.Selected = dbp
return
}
}
grp.Selected = traptgt
} }
func disassembleCurrentInstruction(p Process, thread Thread, off int64) ([]AsmInstruction, error) { func disassembleCurrentInstruction(p Process, thread Thread, off int64) ([]AsmInstruction, error) {

@ -2,8 +2,11 @@ package proc
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"strings" "strings"
"github.com/go-delve/delve/pkg/logflags"
) )
// TargetGroup represents a group of target processes being debugged that // TargetGroup represents a group of target processes being debugged that
@ -12,66 +15,114 @@ import (
// enabled and the backend supports it, otherwise the group will always // enabled and the backend supports it, otherwise the group will always
// contain a single target process. // contain a single target process.
type TargetGroup struct { type TargetGroup struct {
targets []*Target procgrp ProcessGroup
Selected *Target
targets []*Target
Selected *Target
followExecEnabled bool
RecordingManipulation RecordingManipulation
recman RecordingManipulationInternal recman RecordingManipulationInternal
// StopReason describes the reason why the selected target process is stopped.
// A process could be stopped for multiple simultaneous reasons, in which
// case only one will be reported.
StopReason StopReason
// KeepSteppingBreakpoints determines whether certain stop reasons (e.g. manual halts) // KeepSteppingBreakpoints determines whether certain stop reasons (e.g. manual halts)
// will keep the stepping breakpoints instead of clearing them. // will keep the stepping breakpoints instead of clearing them.
KeepSteppingBreakpoints KeepSteppingBreakpoints KeepSteppingBreakpoints KeepSteppingBreakpoints
LogicalBreakpoints map[int]*LogicalBreakpoint LogicalBreakpoints map[int]*LogicalBreakpoint
continueOnce ContinueOnceFunc cctx *ContinueOnceContext
cctx *ContinueOnceContext cfg NewTargetGroupConfig
CanDump bool
} }
// NewTargetGroupConfig contains the configuration for a new TargetGroup object,
type NewTargetGroupConfig struct {
DebugInfoDirs []string // Directories to search for split debug info
DisableAsyncPreempt bool // Go 1.14 asynchronous preemption should be disabled
StopReason StopReason // Initial stop reason
CanDump bool // Can create core dumps (must implement ProcessInternal.MemoryMap)
}
type AddTargetFunc func(ProcessInternal, int, Thread, string, StopReason) (*Target, error)
// NewGroup creates a TargetGroup containing the specified Target. // NewGroup creates a TargetGroup containing the specified Target.
func NewGroup(t *Target) *TargetGroup { func NewGroup(procgrp ProcessGroup, cfg NewTargetGroupConfig) (*TargetGroup, AddTargetFunc) {
if t.partOfGroup { grp := &TargetGroup{
panic("internal error: target is already part of a group") procgrp: procgrp,
} cctx: &ContinueOnceContext{},
t.partOfGroup = true LogicalBreakpoints: make(map[int]*LogicalBreakpoint),
if t.Breakpoints().Logical == nil { StopReason: cfg.StopReason,
t.Breakpoints().Logical = make(map[int]*LogicalBreakpoint) cfg: cfg,
} CanDump: cfg.CanDump,
return &TargetGroup{
RecordingManipulation: t.recman,
targets: []*Target{t},
Selected: t,
cctx: &ContinueOnceContext{},
recman: t.recman,
LogicalBreakpoints: t.Breakpoints().Logical,
continueOnce: t.continueOnce,
} }
return grp, grp.addTarget
} }
// NewGroupRestart creates a new group of targets containing t and // Restart copies breakpoints and follow exec status from oldgrp into grp.
// sets breakpoints and other attributes from oldgrp.
// Breakpoints that can not be set will be discarded, if discard is not nil // Breakpoints that can not be set will be discarded, if discard is not nil
// it will be called for each discarded breakpoint. // it will be called for each discarded breakpoint.
func NewGroupRestart(t *Target, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) *TargetGroup { func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) {
grp := NewGroup(t) for _, bp := range oldgrp.LogicalBreakpoints {
grp.LogicalBreakpoints = oldgrp.LogicalBreakpoints if _, ok := grp.LogicalBreakpoints[bp.LogicalID]; ok {
t.Breakpoints().Logical = grp.LogicalBreakpoints
for _, bp := range grp.LogicalBreakpoints {
if bp.LogicalID < 0 || !bp.Enabled {
continue continue
} }
grp.LogicalBreakpoints[bp.LogicalID] = bp
bp.TotalHitCount = 0 bp.TotalHitCount = 0
bp.HitCount = make(map[int64]uint64) bp.HitCount = make(map[int64]uint64)
bp.Set.PidAddrs = nil // breakpoints set through a list of addresses can not be restored after a restart bp.Set.PidAddrs = nil // breakpoints set through a list of addresses can not be restored after a restart
err := grp.EnableBreakpoint(bp) if bp.Enabled {
if err != nil { err := grp.EnableBreakpoint(bp)
if discard != nil { if err != nil {
discard(bp, err) if discard != nil {
discard(bp, err)
}
delete(grp.LogicalBreakpoints, bp.LogicalID)
} }
delete(grp.LogicalBreakpoints, bp.LogicalID)
} }
} }
return grp if oldgrp.followExecEnabled {
grp.FollowExec(true, "")
}
}
func (grp *TargetGroup) addTarget(p ProcessInternal, pid int, currentThread Thread, path string, stopReason StopReason) (*Target, error) {
t, err := grp.newTarget(p, pid, currentThread, path)
if err != nil {
return nil, err
}
t.StopReason = stopReason
//TODO(aarzilli): check if the target's command line matches the regex
if t.partOfGroup {
panic("internal error: target is already part of group")
}
t.partOfGroup = true
if grp.RecordingManipulation == nil {
grp.RecordingManipulation = t.recman
grp.recman = t.recman
}
if grp.Selected == nil {
grp.Selected = t
}
t.Breakpoints().Logical = grp.LogicalBreakpoints
logger := logflags.DebuggerLogger()
for _, lbp := range grp.LogicalBreakpoints {
if lbp.LogicalID < 0 {
continue
}
err := enableBreakpointOnTarget(t, lbp)
if err != nil {
logger.Debugf("could not enable breakpoint %d on new target %d: %v", lbp.LogicalID, t.Pid(), err)
} else {
logger.Debugf("breakpoint %d enabled on new target %d: %v", lbp.LogicalID, t.Pid(), err)
}
}
grp.targets = append(grp.targets, t)
return t, nil
} }
// Targets returns a slice of all targets in the group, including the // Targets returns a slice of all targets in the group, including the
@ -95,10 +146,22 @@ func (grp *TargetGroup) Valid() (bool, error) {
return false, err0 return false, err0
} }
func (grp *TargetGroup) numValid() int {
r := 0
for _, t := range grp.targets {
ok, _ := t.Valid()
if ok {
r++
}
}
return r
}
// Detach detaches all targets in the group. // Detach detaches all targets in the group.
func (grp *TargetGroup) Detach(kill bool) error { func (grp *TargetGroup) Detach(kill bool) error {
var errs []string var errs []string
for _, t := range grp.targets { for i := len(grp.targets) - 1; i >= 0; i-- {
t := grp.targets[i]
isvalid, _ := t.Valid() isvalid, _ := t.Valid()
if !isvalid { if !isvalid {
continue continue
@ -144,12 +207,10 @@ func (grp *TargetGroup) ThreadList() []Thread {
} }
// TargetForThread returns the target containing the given thread. // TargetForThread returns the target containing the given thread.
func (grp *TargetGroup) TargetForThread(thread Thread) *Target { func (grp *TargetGroup) TargetForThread(tid int) *Target {
for _, t := range grp.targets { for _, t := range grp.targets {
for _, th := range t.ThreadList() { if _, ok := t.FindThread(tid); ok {
if th == thread { return t
return t
}
} }
} }
return nil return nil
@ -274,6 +335,26 @@ func (grp *TargetGroup) DisableBreakpoint(lbp *LogicalBreakpoint) error {
return nil return nil
} }
// FollowExec enables or disables follow exec mode. When follow exec mode is
// enabled new processes spawned by the target process are automatically
// added to the target group.
// If regex is not the empty string only processes whose command line
// matches regex will be added to the target group.
func (grp *TargetGroup) FollowExec(v bool, regex string) error {
if regex != "" {
return errors.New("regex not implemented")
}
it := ValidTargets{Group: grp}
for it.Next() {
err := it.proc.FollowExec(v)
if err != nil {
return err
}
}
grp.followExecEnabled = v
return nil
}
// ValidTargets iterates through all valid targets in Group. // ValidTargets iterates through all valid targets in Group.
type ValidTargets struct { type ValidTargets struct {
*Target *Target
@ -295,3 +376,9 @@ func (it *ValidTargets) Next() bool {
it.Target = nil it.Target = nil
return false return false
} }
// Reset returns the iterator to the start of the group.
func (it *ValidTargets) Reset() {
it.Target = nil
it.start = 0
}

@ -10,9 +10,9 @@ import (
func TestGoroutineCreationLocation(t *testing.T) { func TestGoroutineCreationLocation(t *testing.T) {
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("goroutinestackprog", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("goroutinestackprog", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
bp := setFunctionBreakpoint(p, t, "main.agoroutine") bp := setFunctionBreakpoint(p, t, "main.agoroutine")
assertNoError(p.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
gs, _, err := proc.GoroutinesInfo(p, 0, 0) gs, _, err := proc.GoroutinesInfo(p, 0, 0)
assertNoError(err, t, "GoroutinesInfo") assertNoError(err, t, "GoroutinesInfo")
@ -39,6 +39,6 @@ func TestGoroutineCreationLocation(t *testing.T) {
} }
p.ClearBreakpoint(bp.Addr) p.ClearBreakpoint(bp.Addr)
p.Continue() grp.Continue()
}) })
} }

@ -140,8 +140,8 @@ func TestVariableEvaluation2(t *testing.T) {
} }
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := p.Continue() err := grp.Continue()
assertNoError(err, t, "Continue() returned an error") assertNoError(err, t, "Continue() returned an error")
for _, tc := range testcases { for _, tc := range testcases {
@ -198,8 +198,8 @@ func TestSetVariable(t *testing.T) {
{"s3", "[]int", "[]int len: 3, cap: 3, [3,4,5]", "arr1[:]", "[]int len: 4, cap: 4, [0,1,2,3]"}, {"s3", "[]int", "[]int len: 3, cap: 3, [3,4,5]", "arr1[:]", "[]int len: 4, cap: 4, [0,1,2,3]"},
} }
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
for _, tc := range testcases { for _, tc := range testcases {
if tc.name == "iface1" && tc.expr == "parr" { if tc.name == "iface1" && tc.expr == "parr" {
@ -267,8 +267,8 @@ func TestVariableEvaluationShort(t *testing.T) {
} }
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := p.Continue() err := grp.Continue()
assertNoError(err, t, "Continue() returned an error") assertNoError(err, t, "Continue() returned an error")
for _, tc := range testcases { for _, tc := range testcases {
@ -323,8 +323,8 @@ func TestMultilineVariableEvaluation(t *testing.T) {
} }
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := p.Continue() err := grp.Continue()
assertNoError(err, t, "Continue() returned an error") assertNoError(err, t, "Continue() returned an error")
for _, tc := range testcases { for _, tc := range testcases {
@ -399,8 +399,8 @@ func TestLocalVariables(t *testing.T) {
} }
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := p.Continue() err := grp.Continue()
assertNoError(err, t, "Continue() returned an error") assertNoError(err, t, "Continue() returned an error")
for _, tc := range testcases { for _, tc := range testcases {
@ -436,7 +436,7 @@ func TestLocalVariables(t *testing.T) {
func TestEmbeddedStruct(t *testing.T) { func TestEmbeddedStruct(t *testing.T) {
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
testcases := []varTest{ testcases := []varTest{
{"b.val", true, "-314", "-314", "int", nil}, {"b.val", true, "-314", "-314", "int", nil},
{"b.A.val", true, "-314", "-314", "int", nil}, {"b.A.val", true, "-314", "-314", "int", nil},
@ -460,7 +460,7 @@ func TestEmbeddedStruct(t *testing.T) {
{"w4.F", false, ``, ``, "", errors.New("w4 has no member F")}, {"w4.F", false, ``, ``, "", errors.New("w4 has no member F")},
{"w5.F", false, ``, ``, "", errors.New("w5 has no member F")}, {"w5.F", false, ``, ``, "", errors.New("w5 has no member F")},
} }
assertNoError(p.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
ver, _ := goversion.Parse(runtime.Version()) ver, _ := goversion.Parse(runtime.Version())
if ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9, Rev: -1}) { if ver.Major >= 0 && !ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 9, Rev: -1}) {
@ -491,8 +491,8 @@ func TestEmbeddedStruct(t *testing.T) {
} }
func TestComplexSetting(t *testing.T) { func TestComplexSetting(t *testing.T) {
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := p.Continue() err := grp.Continue()
assertNoError(err, t, "Continue() returned an error") assertNoError(err, t, "Continue() returned an error")
h := func(setExpr, value string) { h := func(setExpr, value string) {
@ -842,8 +842,8 @@ func TestEvalExpression(t *testing.T) {
} }
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error") assertNoError(grp.Continue(), t, "Continue() returned an error")
for i, tc := range testcases { for i, tc := range testcases {
t.Run(strconv.Itoa(i), func(t *testing.T) { t.Run(strconv.Itoa(i), func(t *testing.T) {
variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig) variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig)
@ -872,8 +872,8 @@ func TestEvalExpression(t *testing.T) {
func TestEvalAddrAndCast(t *testing.T) { func TestEvalAddrAndCast(t *testing.T) {
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error") assertNoError(grp.Continue(), t, "Continue() returned an error")
c1addr, err := evalVariableWithCfg(p, "&c1", pnormalLoadConfig) c1addr, err := evalVariableWithCfg(p, "&c1", pnormalLoadConfig)
assertNoError(err, t, "EvalExpression(&c1)") assertNoError(err, t, "EvalExpression(&c1)")
c1addrstr := api.ConvertVar(c1addr).SinglelineString() c1addrstr := api.ConvertVar(c1addr).SinglelineString()
@ -899,8 +899,8 @@ func TestEvalAddrAndCast(t *testing.T) {
func TestMapEvaluation(t *testing.T) { func TestMapEvaluation(t *testing.T) {
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error") assertNoError(grp.Continue(), t, "Continue() returned an error")
m1v, err := evalVariableWithCfg(p, "m1", pnormalLoadConfig) m1v, err := evalVariableWithCfg(p, "m1", pnormalLoadConfig)
assertNoError(err, t, "EvalVariable()") assertNoError(err, t, "EvalVariable()")
m1 := api.ConvertVar(m1v) m1 := api.ConvertVar(m1v)
@ -941,8 +941,8 @@ func TestMapEvaluation(t *testing.T) {
func TestUnsafePointer(t *testing.T) { func TestUnsafePointer(t *testing.T) {
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error") assertNoError(grp.Continue(), t, "Continue() returned an error")
up1v, err := evalVariableWithCfg(p, "up1", pnormalLoadConfig) up1v, err := evalVariableWithCfg(p, "up1", pnormalLoadConfig)
assertNoError(err, t, "EvalVariable(up1)") assertNoError(err, t, "EvalVariable(up1)")
up1 := api.ConvertVar(up1v) up1 := api.ConvertVar(up1v)
@ -979,8 +979,8 @@ func TestIssue426(t *testing.T) {
// Serialization of type expressions (go/ast.Expr) containing anonymous structs or interfaces // Serialization of type expressions (go/ast.Expr) containing anonymous structs or interfaces
// differs from the serialization used by the linker to produce DWARF type information // differs from the serialization used by the linker to produce DWARF type information
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error") assertNoError(grp.Continue(), t, "Continue() returned an error")
for _, testcase := range testcases { for _, testcase := range testcases {
v, err := evalVariableWithCfg(p, testcase.name, pnormalLoadConfig) v, err := evalVariableWithCfg(p, testcase.name, pnormalLoadConfig)
assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", testcase.name)) assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", testcase.name))
@ -1065,8 +1065,8 @@ func TestPackageRenames(t *testing.T) {
} }
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("pkgrenames", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("pkgrenames", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error") assertNoError(grp.Continue(), t, "Continue() returned an error")
testPackageRenamesHelper(t, p, testcases) testPackageRenamesHelper(t, p, testcases)
testPackageRenamesHelper(t, p, testcases1_9) testPackageRenamesHelper(t, p, testcases1_9)
@ -1101,8 +1101,8 @@ func TestConstants(t *testing.T) {
// Not supported on 1.9 or earlier // Not supported on 1.9 or earlier
t.Skip("constants added in go 1.10") t.Skip("constants added in go 1.10")
} }
withTestProcess("consts", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("consts", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue") assertNoError(grp.Continue(), t, "Continue")
for _, testcase := range testcases { for _, testcase := range testcases {
variable, err := evalVariableWithCfg(p, testcase.name, pnormalLoadConfig) variable, err := evalVariableWithCfg(p, testcase.name, pnormalLoadConfig)
assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", testcase.name)) assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", testcase.name))
@ -1112,9 +1112,9 @@ func TestConstants(t *testing.T) {
} }
func TestIssue1075(t *testing.T) { func TestIssue1075(t *testing.T) {
withTestProcess("clientdo", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("clientdo", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
setFunctionBreakpoint(p, t, "net/http.(*Client).Do") setFunctionBreakpoint(p, t, "net/http.(*Client).Do")
assertNoError(p.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
scope, err := proc.GoroutineScope(p, p.CurrentThread()) scope, err := proc.GoroutineScope(p, p.CurrentThread())
assertNoError(err, t, fmt.Sprintf("GoroutineScope (%d)", i)) assertNoError(err, t, fmt.Sprintf("GoroutineScope (%d)", i))
@ -1260,9 +1260,8 @@ func TestCallFunction(t *testing.T) {
{`floatsum(1, 2)`, []string{":float64:3"}, nil}, {`floatsum(1, 2)`, []string{":float64:3"}, nil},
} }
withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, fixture protest.Fixture) { withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
grp := proc.NewGroup(p) testCallFunctionSetBreakpoint(t, p, grp, fixture)
testCallFunctionSetBreakpoint(t, p, fixture)
assertNoError(grp.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
for _, tc := range testcases { for _, tc := range testcases {
@ -1303,7 +1302,7 @@ func TestCallFunction(t *testing.T) {
}) })
} }
func testCallFunctionSetBreakpoint(t *testing.T, p *proc.Target, fixture protest.Fixture) { func testCallFunctionSetBreakpoint(t *testing.T, p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
buf, err := ioutil.ReadFile(fixture.Source) buf, err := ioutil.ReadFile(fixture.Source)
assertNoError(err, t, "ReadFile") assertNoError(err, t, "ReadFile")
for i, line := range strings.Split(string(buf), "\n") { for i, line := range strings.Split(string(buf), "\n") {
@ -1390,8 +1389,8 @@ func testCallFunction(t *testing.T, grp *proc.TargetGroup, p *proc.Target, tc te
func TestIssue1531(t *testing.T) { func TestIssue1531(t *testing.T) {
// Go 1.12 introduced a change to the map representation where empty cells can be marked with 1 instead of just 0. // Go 1.12 introduced a change to the map representation where empty cells can be marked with 1 instead of just 0.
withTestProcess("issue1531", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("issue1531", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
hasKeys := func(mv *proc.Variable, keys ...string) { hasKeys := func(mv *proc.Variable, keys ...string) {
n := 0 n := 0
@ -1453,9 +1452,9 @@ func assertCurrentLocationFunction(p *proc.Target, t *testing.T, fnname string)
func TestPluginVariables(t *testing.T) { func TestPluginVariables(t *testing.T) {
pluginFixtures := protest.WithPlugins(t, protest.AllNonOptimized, "plugin1/", "plugin2/") pluginFixtures := protest.WithPlugins(t, protest.AllNonOptimized, "plugin1/", "plugin2/")
withTestProcessArgs("plugintest2", t, ".", []string{pluginFixtures[0].Path, pluginFixtures[1].Path}, protest.AllNonOptimized, func(p *proc.Target, fixture protest.Fixture) { withTestProcessArgs("plugintest2", t, ".", []string{pluginFixtures[0].Path, pluginFixtures[1].Path}, protest.AllNonOptimized, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
setFileBreakpoint(p, t, fixture.Source, 41) setFileBreakpoint(p, t, fixture.Source, 41)
assertNoError(p.Continue(), t, "Continue 1") assertNoError(grp.Continue(), t, "Continue 1")
bp := setFunctionBreakpoint(p, t, "github.com/go-delve/delve/_fixtures/plugin2.TypesTest") bp := setFunctionBreakpoint(p, t, "github.com/go-delve/delve/_fixtures/plugin2.TypesTest")
t.Logf("bp.Addr = %#x", bp.Addr) t.Logf("bp.Addr = %#x", bp.Addr)
@ -1465,7 +1464,7 @@ func TestPluginVariables(t *testing.T) {
t.Logf("%#x %s\n", image.StaticBase, image.Path) t.Logf("%#x %s\n", image.StaticBase, image.Path)
} }
assertNoError(p.Continue(), t, "Continue 2") assertNoError(grp.Continue(), t, "Continue 2")
// test that PackageVariables returns variables from the executable and plugins // test that PackageVariables returns variables from the executable and plugins
scope, err := evalScope(p) scope, err := evalScope(p)
@ -1495,15 +1494,15 @@ func TestPluginVariables(t *testing.T) {
// test that the concrete type -> interface{} conversion works across plugins (mostly tests proc.dwarfToRuntimeType) // test that the concrete type -> interface{} conversion works across plugins (mostly tests proc.dwarfToRuntimeType)
assertNoError(setVariable(p, "plugin2.A", "main.ExeGlobal"), t, "setVariable(plugin2.A = main.ExeGlobal)") assertNoError(setVariable(p, "plugin2.A", "main.ExeGlobal"), t, "setVariable(plugin2.A = main.ExeGlobal)")
assertNoError(p.Continue(), t, "Continue 3") assertNoError(grp.Continue(), t, "Continue 3")
assertCurrentLocationFunction(p, t, "github.com/go-delve/delve/_fixtures/plugin2.aIsNotNil") assertCurrentLocationFunction(p, t, "github.com/go-delve/delve/_fixtures/plugin2.aIsNotNil")
vstr, err := evalVariableWithCfg(p, "str", pnormalLoadConfig) vstr, err := evalVariableWithCfg(p, "str", pnormalLoadConfig)
assertNoError(err, t, "Eval(str)") assertNoError(err, t, "Eval(str)")
assertVariable(t, vstr, varTest{"str", true, `"success"`, ``, `string`, nil}) assertVariable(t, vstr, varTest{"str", true, `"success"`, ``, `string`, nil})
assertNoError(p.StepOut(), t, "StepOut") assertNoError(grp.StepOut(), t, "StepOut")
assertNoError(p.StepOut(), t, "StepOut") assertNoError(grp.StepOut(), t, "StepOut")
assertNoError(p.Next(), t, "Next") assertNoError(grp.Next(), t, "Next")
// read interface variable, inside executable code, with a concrete type defined in a plugin // read interface variable, inside executable code, with a concrete type defined in a plugin
vb, err := evalVariableWithCfg(p, "b", pnormalLoadConfig) vb, err := evalVariableWithCfg(p, "b", pnormalLoadConfig)
@ -1534,8 +1533,8 @@ func TestCgoEval(t *testing.T) {
} }
protest.AllowRecording(t) protest.AllowRecording(t)
withTestProcess("testvariablescgo/", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariablescgo/", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error") assertNoError(grp.Continue(), t, "Continue() returned an error")
for _, tc := range testcases { for _, tc := range testcases {
variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig) variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig)
if err != nil && err.Error() == "evaluating methods not supported on this version of Go" { if err != nil && err.Error() == "evaluating methods not supported on this version of Go" {
@ -1581,9 +1580,9 @@ func TestEvalExpressionGenerics(t *testing.T) {
}, },
} }
withTestProcess("testvariables_generic", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testvariables_generic", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
for i, tcs := range testcases { for i, tcs := range testcases {
assertNoError(p.Continue(), t, fmt.Sprintf("Continue() returned an error (%d)", i)) assertNoError(grp.Continue(), t, fmt.Sprintf("Continue() returned an error (%d)", i))
for _, tc := range tcs { for _, tc := range tcs {
variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig) variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig)
if tc.err == nil { if tc.err == nil {
@ -1604,8 +1603,8 @@ func TestEvalExpressionGenerics(t *testing.T) {
// Test the behavior when reading dangling pointers produced by unsafe code. // Test the behavior when reading dangling pointers produced by unsafe code.
func TestBadUnsafePtr(t *testing.T) { func TestBadUnsafePtr(t *testing.T) {
withTestProcess("testunsafepointers", t, func(p *proc.Target, fixture protest.Fixture) { withTestProcess("testunsafepointers", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue()") assertNoError(grp.Continue(), t, "Continue()")
// danglingPtrPtr is a pointer with value 0x42, which is an unreadable // danglingPtrPtr is a pointer with value 0x42, which is an unreadable
// address. // address.

@ -79,6 +79,7 @@ func (ft *FakeTerminal) ExecStarlark(starlarkProgram string) (outstr string, err
} }
func (ft *FakeTerminal) MustExec(cmdstr string) string { func (ft *FakeTerminal) MustExec(cmdstr string) string {
ft.t.Helper()
outstr, err := ft.Exec(cmdstr) outstr, err := ft.Exec(cmdstr)
if err != nil { if err != nil {
ft.t.Errorf("output of %q: %q", cmdstr, outstr) ft.t.Errorf("output of %q: %q", cmdstr, outstr)

@ -161,30 +161,28 @@ func New(config *Config, processArgs []string) (*Debugger, error) {
if len(d.processArgs) > 0 { if len(d.processArgs) > 0 {
path = d.processArgs[0] path = d.processArgs[0]
} }
p, err := d.Attach(d.config.AttachPid, path) var err error
d.target, err = d.Attach(d.config.AttachPid, path)
if err != nil { if err != nil {
err = go11DecodeErrorCheck(err) err = go11DecodeErrorCheck(err)
err = noDebugErrorWarning(err) err = noDebugErrorWarning(err)
return nil, attachErrorMessage(d.config.AttachPid, err) return nil, attachErrorMessage(d.config.AttachPid, err)
} }
d.target = proc.NewGroup(p)
case d.config.CoreFile != "": case d.config.CoreFile != "":
var p *proc.Target
var err error var err error
switch d.config.Backend { switch d.config.Backend {
case "rr": case "rr":
d.log.Infof("opening trace %s", d.config.CoreFile) d.log.Infof("opening trace %s", d.config.CoreFile)
p, err = gdbserial.Replay(d.config.CoreFile, false, false, d.config.DebugInfoDirectories) d.target, err = gdbserial.Replay(d.config.CoreFile, false, false, d.config.DebugInfoDirectories)
default: default:
d.log.Infof("opening core file %s (executable %s)", d.config.CoreFile, d.processArgs[0]) d.log.Infof("opening core file %s (executable %s)", d.config.CoreFile, d.processArgs[0])
p, err = core.OpenCore(d.config.CoreFile, d.processArgs[0], d.config.DebugInfoDirectories) d.target, err = core.OpenCore(d.config.CoreFile, d.processArgs[0], d.config.DebugInfoDirectories)
} }
if err != nil { if err != nil {
err = go11DecodeErrorCheck(err) err = go11DecodeErrorCheck(err)
return nil, err return nil, err
} }
d.target = proc.NewGroup(p)
if err := d.checkGoVersion(); err != nil { if err := d.checkGoVersion(); err != nil {
d.target.Detach(true) d.target.Detach(true)
return nil, err return nil, err
@ -192,7 +190,8 @@ func New(config *Config, processArgs []string) (*Debugger, error) {
default: default:
d.log.Infof("launching process with args: %v", d.processArgs) d.log.Infof("launching process with args: %v", d.processArgs)
p, err := d.Launch(d.processArgs, d.config.WorkingDir) var err error
d.target, err = d.Launch(d.processArgs, d.config.WorkingDir)
if err != nil { if err != nil {
if _, ok := err.(*proc.ErrUnsupportedArch); !ok { if _, ok := err.(*proc.ErrUnsupportedArch); !ok {
err = go11DecodeErrorCheck(err) err = go11DecodeErrorCheck(err)
@ -201,10 +200,6 @@ func New(config *Config, processArgs []string) (*Debugger, error) {
} }
return nil, err return nil, err
} }
if p != nil {
// if p == nil and err == nil then we are doing a recording, don't touch d.target
d.target = proc.NewGroup(p)
}
if err := d.checkGoVersion(); err != nil { if err := d.checkGoVersion(); err != nil {
d.target.Detach(true) d.target.Detach(true)
return nil, err return nil, err
@ -245,7 +240,7 @@ func (d *Debugger) TargetGoVersion() string {
} }
// Launch will start a process with the given args and working directory. // Launch will start a process with the given args and working directory.
func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error) { func (d *Debugger) Launch(processArgs []string, wd string) (*proc.TargetGroup, error) {
fullpath, err := verifyBinaryFormat(processArgs[0]) fullpath, err := verifyBinaryFormat(processArgs[0])
if err != nil { if err != nil {
return nil, err return nil, err
@ -285,7 +280,7 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error)
go func() { go func() {
defer d.targetMutex.Unlock() defer d.targetMutex.Unlock()
p, err := d.recordingRun(run) grp, err := d.recordingRun(run)
if err != nil { if err != nil {
d.log.Errorf("could not record target: %v", err) d.log.Errorf("could not record target: %v", err)
// this is ugly but we can't respond to any client requests at this // this is ugly but we can't respond to any client requests at this
@ -293,7 +288,7 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error)
os.Exit(1) os.Exit(1)
} }
d.recordingDone() d.recordingDone()
d.target = proc.NewGroup(p) d.target = grp
if err := d.checkGoVersion(); err != nil { if err := d.checkGoVersion(); err != nil {
d.log.Error(err) d.log.Error(err)
err := d.target.Detach(true) err := d.target.Detach(true)
@ -332,7 +327,7 @@ func (d *Debugger) isRecording() bool {
return d.stopRecording != nil return d.stopRecording != nil
} }
func (d *Debugger) recordingRun(run func() (string, error)) (*proc.Target, error) { func (d *Debugger) recordingRun(run func() (string, error)) (*proc.TargetGroup, error) {
tracedir, err := run() tracedir, err := run()
if err != nil && tracedir == "" { if err != nil && tracedir == "" {
return nil, err return nil, err
@ -342,7 +337,7 @@ func (d *Debugger) recordingRun(run func() (string, error)) (*proc.Target, error
} }
// Attach will attach to the process specified by 'pid'. // Attach will attach to the process specified by 'pid'.
func (d *Debugger) Attach(pid int, path string) (*proc.Target, error) { func (d *Debugger) Attach(pid int, path string) (*proc.TargetGroup, error) {
switch d.config.Backend { switch d.config.Backend {
case "native": case "native":
return native.Attach(pid, d.config.DebugInfoDirectories) return native.Attach(pid, d.config.DebugInfoDirectories)
@ -360,7 +355,7 @@ func (d *Debugger) Attach(pid int, path string) (*proc.Target, error) {
var errMacOSBackendUnavailable = errors.New("debugserver or lldb-server not found: install Xcode's command line tools or lldb-server") var errMacOSBackendUnavailable = errors.New("debugserver or lldb-server not found: install Xcode's command line tools or lldb-server")
func betterGdbserialLaunchError(p *proc.Target, err error) (*proc.Target, error) { func betterGdbserialLaunchError(p *proc.TargetGroup, err error) (*proc.TargetGroup, error) {
if runtime.GOOS != "darwin" { if runtime.GOOS != "darwin" {
return p, err return p, err
} }
@ -482,7 +477,7 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
d.processArgs = append([]string{d.processArgs[0]}, newArgs...) d.processArgs = append([]string{d.processArgs[0]}, newArgs...)
d.config.Redirects = newRedirects d.config.Redirects = newRedirects
} }
var p *proc.Target var grp *proc.TargetGroup
var err error var err error
if rebuild { if rebuild {
@ -510,19 +505,20 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
} }
d.recordingStart(stop) d.recordingStart(stop)
p, err = d.recordingRun(run) grp, err = d.recordingRun(run)
d.recordingDone() d.recordingDone()
} else { } else {
p, err = d.Launch(d.processArgs, d.config.WorkingDir) grp, err = d.Launch(d.processArgs, d.config.WorkingDir)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("could not launch process: %s", err) return nil, fmt.Errorf("could not launch process: %s", err)
} }
discarded := []api.DiscardedBreakpoint{} discarded := []api.DiscardedBreakpoint{}
d.target = proc.NewGroupRestart(p, d.target, func(oldBp *proc.LogicalBreakpoint, err error) { proc.Restart(grp, d.target, func(oldBp *proc.LogicalBreakpoint, err error) {
discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: api.ConvertLogicalBreakpoint(oldBp), Reason: err.Error()}) discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: api.ConvertLogicalBreakpoint(oldBp), Reason: err.Error()})
}) })
d.target = grp
return discarded, nil return discarded, nil
} }
@ -1321,7 +1317,7 @@ func (d *Debugger) collectBreakpointInformation(apiThread *api.Thread, thread pr
bpi := &api.BreakpointInfo{} bpi := &api.BreakpointInfo{}
apiThread.BreakpointInfo = bpi apiThread.BreakpointInfo = bpi
tgt := d.target.TargetForThread(thread) tgt := d.target.TargetForThread(thread.ThreadID())
if bp.Goroutine { if bp.Goroutine {
g, err := proc.GetG(thread) g, err := proc.GetG(thread)
@ -2141,7 +2137,7 @@ func (d *Debugger) DumpStart(dest string) error {
//TODO(aarzilli): what do we do if the user switches to a different target after starting a dump but before it's finished? //TODO(aarzilli): what do we do if the user switches to a different target after starting a dump but before it's finished?
if !d.target.Selected.CanDump { if !d.target.CanDump {
d.targetMutex.Unlock() d.targetMutex.Unlock()
return ErrCoreDumpNotSupported return ErrCoreDumpNotSupported
} }