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.
// If the DWARF information cannot be found in the binary, Delve will look
// 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 currentThread proc.Thread
var err error
@ -222,14 +222,13 @@ func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.Target, e
return nil, ErrNoThreads
}
return proc.NewTarget(p, p.pid, currentThread, proc.NewTargetConfig{
Path: exePath,
grp, addTarget := proc.NewGroup(p, proc.NewTargetGroupConfig{
DebugInfoDirs: debugInfoDirs,
DisableAsyncPreempt: false,
StopReason: proc.StopAttached,
CanDump: false,
ContinueOnce: continueOnce,
})
_, err = addTarget(p, p.pid, currentThread, exePath, proc.StopAttached)
return grp, err
}
// BinInfo will return the binary info.
@ -310,6 +309,11 @@ func (p *process) WriteMemory(addr uint64, data []byte) (int, error) {
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.
func (t *thread) ProcessMemory() proc.MemoryReadWriter {
return t.p
@ -419,7 +423,7 @@ func (p *process) ClearInternalBreakpoints() error {
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
}

@ -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.
// Might be better to check in the binary and core?
tempDir := t.TempDir()
@ -251,8 +251,8 @@ func TestCore(t *testing.T) {
if runtime.GOOS == "linux" && os.Getenv("CI") == "true" && buildMode == "pie" {
t.Skip("disabled on linux, Github Actions, with PIE buildmode")
}
p := withCoreFile(t, "panic", "")
grp := proc.NewGroup(p)
grp := withCoreFile(t, "panic", "")
p := grp.Selected
recorded, _ := grp.Recorded()
if !recorded {
@ -324,7 +324,8 @@ func TestCoreFpRegisters(t *testing.T) {
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)
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" {
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)
assertNoError(err, t, "GoroutinesInfo")
@ -452,10 +454,11 @@ func TestMinidump(t *testing.T) {
fix := test.BuildFixture("sleep", buildFlags)
mdmpPath := procdump(t, fix.Path)
p, err := OpenCore(mdmpPath, fix.Path, []string{})
grp, err := OpenCore(mdmpPath, fix.Path, []string{})
if err != nil {
t.Fatalf("OpenCore: %v", err)
}
p := grp.Selected
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 {
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.
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)
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.
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 {
conn, err := net.Dial("tcp", addr)
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
// some stubs do not provide ways to determine path and pid automatically
// 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.pid = pid
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
// it to launch the specified target program with the specified arguments
// (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" {
return nil, ErrUnsupportedOS
}
@ -567,11 +567,11 @@ func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs [
p := newProcess(process.Process)
p.conn.isDebugserver = isDebugserver
var tgt *proc.Target
var grp *proc.TargetGroup
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 {
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()) {
// 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)
}
}
return tgt, err
return grp, err
}
// 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
// for some stubs that do not provide an automated way of determining it
// (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" {
return nil, ErrUnsupportedOS
}
@ -633,13 +633,13 @@ func LLDBAttach(pid int, path string, debugInfoDirs []string) (*proc.Target, err
p := newProcess(process.Process)
p.conn.isDebugserver = isDebugserver
var tgt *proc.Target
var grp *proc.TargetGroup
if listener != nil {
tgt, err = p.Listen(listener, path, pid, debugInfoDirs, proc.StopAttached)
grp, err = p.Listen(listener, path, pid, debugInfoDirs, proc.StopAttached)
} 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
@ -673,7 +673,7 @@ func (p *gdbProcess) EntryPoint() (uint64, error) {
// initialize uses qProcessInfo to load the inferior's PID and
// executable path. This command is not supported by all stubs and not all
// 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
if path == "" {
// 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
}
}
tgt, err := proc.NewTarget(p, p.conn.pid, p.currentThread, proc.NewTargetConfig{
Path: path,
grp, addTarget := proc.NewGroup(p, proc.NewTargetGroupConfig{
DebugInfoDirs: debugInfoDirs,
DisableAsyncPreempt: runtime.GOOS == "darwin",
StopReason: stopReason,
CanDump: runtime.GOOS == "darwin",
ContinueOnce: continueOnce,
})
_, err = addTarget(p, p.conn.pid, p.currentThread, path, stopReason)
if err != nil {
p.Detach(true)
return nil, err
}
return tgt, nil
return grp, nil
}
func queryProcessInfo(p *gdbProcess, pid int) (int, string, error) {
@ -821,11 +820,7 @@ const (
debugServerTargetExcBreakpoint = 0x96
)
func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) {
if len(procs) != 1 {
panic("not implemented")
}
p := procs[0].(*gdbProcess)
func (p *gdbProcess) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) {
if p.exited {
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)
}
// FollowExec enables (or disables) follow exec mode
func (p *gdbProcess) FollowExec(bool) error {
return errors.New("follow exec not supported")
}
type threadUpdater struct {
p *gdbProcess
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
// 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 {
return nil, err
}
@ -279,7 +279,7 @@ func rrParseGdbCommand(line string) rrInit {
}
// 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)
if tracedir == "" {
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.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 {
t.Fatal("Launch():", err)
}
t.Logf("replaying %q", tracedir)
grp := proc.NewGroup(p)
defer grp.Detach(true)
fn(grp, fixture)

@ -7,6 +7,11 @@ import (
"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
// 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() (func(), error)
// FollowExec enables (or disables) follow exec mode
FollowExec(bool) error
}
// 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")
// 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
}
// Attach returns ErrNativeBackendDisabled.
func Attach(_ int, _ []string) (*proc.Target, error) {
func Attach(_ int, _ []string) (*proc.TargetGroup, error) {
return nil, ErrNativeBackendDisabled
}
@ -57,11 +57,11 @@ func (dbp *nativeProcess) resume() error {
panic(ErrNativeBackendDisabled)
}
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) {
func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
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)
}

@ -24,11 +24,11 @@ type nativeProcess struct {
// Thread used to read and write memory
memthread *nativeThread
os *osProcessDetails
firstStart bool
ptraceChan chan func()
ptraceDoneChan chan interface{}
childProcess bool // this process was launched, not attached to
os *osProcessDetails
firstStart bool
ptraceThread *ptraceThread
childProcess bool // this process was launched, not attached to
followExec bool // automatically attach to new processes
// Controlling terminal file descriptor for
// this process.
@ -45,19 +45,30 @@ type nativeProcess struct {
// `handlePtraceFuncs`.
func newProcess(pid int) *nativeProcess {
dbp := &nativeProcess{
pid: pid,
threads: make(map[int]*nativeThread),
breakpoints: proc.NewBreakpointMap(),
firstStart: true,
os: new(osProcessDetails),
ptraceChan: make(chan func()),
ptraceDoneChan: make(chan interface{}),
bi: proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
pid: pid,
threads: make(map[int]*nativeThread),
breakpoints: proc.NewBreakpointMap(),
firstStart: true,
os: new(osProcessDetails),
ptraceThread: newPtraceThread(),
bi: proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
}
go dbp.handlePtraceFuncs()
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.
func (dbp *nativeProcess) BinInfo() *proc.BinaryInfo {
return dbp.bi
@ -172,23 +183,58 @@ func (dbp *nativeProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
return dbp.memthread.clearSoftwareBreakpoint(bp)
}
func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) {
if len(procs) != 1 {
type processGroup struct {
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")
}
dbp := procs[0].(*nativeProcess)
if dbp.exited {
return nil, proc.StopExited, proc.ErrProcessExited{Pid: dbp.pid}
if procgrp.numValid() == 0 {
return nil, proc.StopExited, proc.ErrProcessExited{Pid: procgrp.procs[0].pid}
}
for {
if err := dbp.resume(); err != nil {
return nil, proc.StopUnknown, err
}
for _, th := range dbp.threads {
th.CurrentBreakpoint.Clear()
for _, dbp := range procgrp.procs {
if dbp.exited {
continue
}
if err := dbp.resume(); err != nil {
return nil, proc.StopUnknown, err
}
for _, th := range dbp.threads {
th.CurrentBreakpoint.Clear()
}
}
if cctx.ResumeChan != nil {
@ -196,16 +242,29 @@ func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext)
cctx.ResumeChan = nil
}
trapthread, err := dbp.trapWait(-1)
trapthread, err := trapWait(procgrp, -1)
if err != nil {
return nil, proc.StopUnknown, err
}
trapthread, err = dbp.stop(cctx, trapthread)
trapthread, err = procgrp.stop(cctx, trapthread)
if err != nil {
return nil, proc.StopUnknown, err
}
if trapthread != nil {
dbp := procgrp.procForThread(trapthread.ID)
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
}
}
@ -226,21 +285,26 @@ func (dbp *nativeProcess) FindBreakpoint(pc uint64, adjustPC bool) (*proc.Breakp
return nil, false
}
// 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.Target, error) {
func (dbp *nativeProcess) initializeBasic() error {
if err := initialize(dbp); err != nil {
return nil, err
return err
}
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
if !dbp.childProcess {
stopReason = proc.StopAttached
}
tgt, err := proc.NewTarget(dbp, dbp.pid, dbp.memthread, proc.NewTargetConfig{
Path: path,
procgrp := &processGroup{}
grp, addTarget := proc.NewGroup(procgrp, proc.NewTargetGroupConfig{
DebugInfoDirs: debugInfoDirs,
// 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
DisableAsyncPreempt: runtime.GOOS == "windows" || (runtime.GOOS == "linux" && runtime.GOARCH == "arm64"),
StopReason: stopReason,
CanDump: runtime.GOOS == "linux" || (runtime.GOOS == "windows" && runtime.GOARCH == "amd64"),
ContinueOnce: continueOnce,
StopReason: stopReason,
CanDump: runtime.GOOS == "linux" || (runtime.GOOS == "windows" && runtime.GOARCH == "amd64"),
})
procgrp.addTarget = addTarget
tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, path, stopReason)
if err != nil {
return nil, err
}
if dbp.bi.Arch.Name == "arm64" {
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
// 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.
@ -279,21 +344,20 @@ func (dbp *nativeProcess) handlePtraceFuncs() {
defer runtime.UnlockOSThread()
}
for fn := range dbp.ptraceChan {
for fn := range pt.ptraceChan {
fn()
dbp.ptraceDoneChan <- nil
pt.ptraceDoneChan <- nil
}
}
func (dbp *nativeProcess) execPtraceFunc(fn func()) {
dbp.ptraceChan <- fn
<-dbp.ptraceDoneChan
dbp.ptraceThread.ptraceChan <- fn
<-dbp.ptraceThread.ptraceDoneChan
}
func (dbp *nativeProcess) postExit() {
dbp.exited = true
close(dbp.ptraceChan)
close(dbp.ptraceDoneChan)
dbp.ptraceThread.release()
dbp.bi.Close()
if dbp.ctty != nil {
dbp.ctty.Close()
@ -349,3 +413,32 @@ func openRedirects(redirects [3]string, foreground bool) (stdin, stdout, stderr
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
// PT_SIGEXC on Darwin which will turn Unix signals into
// 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])
if err != nil {
return nil, err
@ -121,7 +121,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ strin
if err != nil {
return nil, err
}
if _, err := dbp.stop(nil, nil); err != nil {
if _, err := dbp.stop(nil); err != nil {
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.
func Attach(pid int, _ []string) (*proc.Target, error) {
func Attach(pid int, _ []string) (*proc.TargetGroup, error) {
if err := macutil.CheckRosetta(); err != nil {
return nil, err
}
@ -291,6 +291,10 @@ func findExecutable(path string, pid int) string {
return path
}
func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
return procgrp.procs[0].trapWait(pid)
}
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) {
for {
task := dbp.os.task
@ -429,7 +433,11 @@ func (dbp *nativeProcess) resume() error {
}
// 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 {
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.
// If the DWARF information cannot be found in the binary, Delve will look
// 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 (
process *exec.Cmd
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
// the DWARF information cannot be found in the binary, Delve will look
// 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)
var err error
@ -227,8 +227,8 @@ func findExecutable(path string, pid int) string {
return path
}
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) {
return dbp.trapWaitInternal(pid, trapWaitNormal)
func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
return procgrp.procs[0].trapWaitInternal(pid, trapWaitNormal)
}
type trapWaitMode uint8
@ -403,7 +403,11 @@ func (dbp *nativeProcess) resume() error {
// Used by ContinueOnce
// 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 {
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.
// If the DWARF information cannot be found in the binary, Delve will look
// 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 (
process *exec.Cmd
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
// the DWARF information cannot be found in the binary, Delve will look
// 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)
var err error
@ -237,6 +237,11 @@ func (dbp *nativeProcess) requestManualStop() (err error) {
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
// known threads.
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
}
ptraceOptions := ptraceOptionsNormal
if dbp.followExec {
ptraceOptions = ptraceOptionsFollowExec
}
var err error
if attach {
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 = dbp.waitFast(tid); err != nil {
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 {
return nil, err
}
@ -318,8 +328,8 @@ func findExecutable(path string, pid int) string {
return path
}
func (dbp *nativeProcess) trapWait(pid int) (*nativeThread, error) {
return dbp.trapWaitInternal(pid, 0)
func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {
return trapWaitInternal(procgrp, pid, 0)
}
type trapWaitOptions uint8
@ -330,14 +340,21 @@ const (
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
for {
wopt := 0
if options&trapWaitNohang != 0 {
wopt = sys.WNOHANG
}
wpid, status, err := dbp.wait(pid, wopt)
wpid, status, err := waitdbp.wait(pid, wopt)
if err != nil {
return nil, fmt.Errorf("wait err %s %d", err, pid)
}
@ -347,14 +364,27 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
}
continue
}
th, ok := dbp.threads[wpid]
if ok {
th.Status = (*waitStatus)(status)
dbp := procgrp.procForThread(wpid)
var th *nativeThread
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 wpid == dbp.pid {
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)
continue
@ -363,15 +393,24 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
// Signaled means the thread was terminated due to a signal.
if wpid == dbp.pid {
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?
delete(dbp.threads, wpid)
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
// 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
dbp.execPtraceFunc(func() { cloned, err = sys.PtraceGetEventMsg(wpid) })
if err != nil {
@ -410,6 +449,34 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
}
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 {
// Sometimes we get an unknown thread, ignore it?
continue
@ -439,7 +506,10 @@ func (dbp *nativeProcess) trapWaitInternal(pid int, options trapWaitOptions) (*n
// do the same thing we do if a thread quit
if wpid == dbp.pid {
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)
}
@ -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) {
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)
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 {
return err
}
if status(dbp.pid, dbp.os.comm) == statusZombie {
_, err := dbp.trapWaitInternal(-1, trapWaitDontCallExitGuard)
_, err := trapWaitInternal(procgrp, -1, trapWaitDontCallExitGuard)
return err
}
@ -538,21 +608,27 @@ func (dbp *nativeProcess) resume() error {
}
// stop stops all running threads and sets breakpoints
func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
if dbp.exited {
return nil, proc.ErrProcessExited{Pid: dbp.pid}
func (procgrp *processGroup) stop(cctx *proc.ContinueOnceContext, trapthread *nativeThread) (*nativeThread, error) {
if procgrp.numValid() == 0 {
return nil, proc.ErrProcessExited{Pid: procgrp.procs[0].pid}
}
for _, th := range dbp.threads {
th.os.setbp = false
for _, dbp := range procgrp.procs {
if dbp.exited {
continue
}
for _, th := range dbp.threads {
th.os.setbp = false
}
}
trapthread.os.setbp = true
// check if any other thread simultaneously received a SIGTRAP
for {
th, err := dbp.trapWaitInternal(-1, trapWaitNohang)
th, err := trapWaitInternal(procgrp, -1, trapWaitNohang)
if err != nil {
return nil, dbp.exitGuard(err)
p := procgrp.procForThread(th.ID)
return nil, exitGuard(p, procgrp, err)
}
if th == nil {
break
@ -560,10 +636,20 @@ func (dbp *nativeProcess) stop(cctx *proc.ContinueOnceContext, trapthread *nativ
}
// stop all threads that are still running
for _, th := range dbp.threads {
if th.os.running {
if err := th.stop(); err != nil {
return nil, dbp.exitGuard(err)
for _, dbp := range procgrp.procs {
if dbp.exited {
continue
}
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
for {
allstopped := true
for _, th := range dbp.threads {
if th.os.running {
allstopped = false
break
for _, dbp := range procgrp.procs {
if dbp.exited {
continue
}
for _, th := range dbp.threads {
if th.os.running {
allstopped = false
break
}
}
}
if allstopped {
break
}
_, err := dbp.trapWaitInternal(-1, trapWaitHalt)
_, err := trapWaitInternal(procgrp, -1, trapWaitHalt)
if err != nil {
return nil, err
}
}
if err := linutil.ElfUpdateSharedObjects(dbp); err != nil {
return nil, err
switchTrapthread := false
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
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
// want pkg/proc to believe that this thread was stopped by a
// hardcoded breakpoint.
switchTrapthread = true
*switchTrapthread = true
}
}
}
}
}
if err1 != nil {
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
return err1
}
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)
}
// 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 {
return sys.Kill(pid, sys.SIGINT)
}

@ -23,7 +23,7 @@ type osProcessDetails struct {
func (os *osProcessDetails) Close() {}
// 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]
env := proc.DisableAsyncPreemptEnv()
@ -140,7 +140,7 @@ func findExePath(pid int) (string, error) {
var debugPrivilegeRequested = false
// 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
if !debugPrivilegeRequested {
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 tid, exitCode int
dbp.execPtraceFunc(func() {
@ -474,7 +475,8 @@ func (dbp *nativeProcess) resume() error {
}
// 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 {
return nil, proc.ErrProcessExited{Pid: dbp.pid}
}

@ -22,6 +22,9 @@ type osSpecificDetails struct {
func (t *nativeThread) stop() (err error) {
err = sys.Tgkill(t.dbp.pid, t.ID, sys.SIGSTOP)
if err != nil {
if err == sys.ESRCH {
return
}
err = fmt.Errorf("stop err %s on thread %d", err, t.ID)
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)
// SIGINT directed at the inferior should be passed along not swallowed by delve
withTestProcess("issue419", t, func(p *proc.Target, fixture protest.Fixture) {
grp := proc.NewGroup(p)
withTestProcess("issue419", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
setFunctionBreakpoint(p, t, "main.main")
assertNoError(grp.Continue(), t, "Continue()")
resumeChan := make(chan struct{}, 1)

@ -23,8 +23,8 @@ func TestScopeWithEscapedVariable(t *testing.T) {
return
}
withTestProcess("scopeescapevareval", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue")
withTestProcess("scopeescapevareval", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue")
// 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
@ -72,7 +72,7 @@ func TestScope(t *testing.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 {
setFileBreakpoint(p, t, fixture.Source, scopeChecks[i].line)
}
@ -80,7 +80,7 @@ func TestScope(t *testing.T) {
t.Logf("%d breakpoints set", len(scopeChecks))
for {
if err := p.Continue(); err != nil {
if err := grp.Continue(); err != nil {
if _, exited := err.(proc.ErrProcessExited); exited {
break
}

@ -38,9 +38,8 @@ const (
type Target struct {
Process
proc ProcessInternal
recman RecordingManipulationInternal
continueOnce ContinueOnceFunc
proc ProcessInternal
recman RecordingManipulationInternal
pid int
@ -49,9 +48,6 @@ type Target struct {
// case only one will be reported.
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 Thread
@ -148,18 +144,6 @@ const (
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)
// where asyncpreemptoff is set to 1.
func DisableAsyncPreemptEnv() []string {
@ -174,15 +158,15 @@ func DisableAsyncPreemptEnv() []string {
return env
}
// NewTarget returns an initialized Target object.
// newTarget returns an initialized Target object.
// 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()
if err != nil {
return nil, err
}
err = p.BinInfo().LoadBinaryInfo(cfg.Path, entryPoint, cfg.DebugInfoDirs)
err = p.BinInfo().LoadBinaryInfo(path, entryPoint, grp.cfg.DebugInfoDirs)
if err != nil {
return nil, err
}
@ -196,11 +180,8 @@ func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetCo
Process: p,
proc: p,
fncallForG: make(map[int64]*callInjection),
StopReason: cfg.StopReason,
currentThread: currentThread,
CanDump: cfg.CanDump,
pid: pid,
continueOnce: cfg.ContinueOnce,
}
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.fakeMemoryRegistryMap = make(map[string]*compositeMemory)
if cfg.DisableAsyncPreempt {
if grp.cfg.DisableAsyncPreempt {
setAsyncPreemptOff(t, 1)
}

@ -48,48 +48,55 @@ func (grp *TargetGroup) Next() (err error) {
// processes. It will continue until it hits a breakpoint
// or is otherwise stopped.
func (grp *TargetGroup) Continue() error {
if len(grp.targets) != 1 {
panic("multiple targets not implemented")
}
dbp := grp.Selected
if _, err := dbp.Valid(); err != nil {
if grp.numValid() == 0 {
_, err := grp.targets[0].Valid()
return err
}
for _, thread := range dbp.ThreadList() {
thread.Common().CallReturn = false
thread.Common().returnValues = nil
for _, dbp := range grp.targets {
if isvalid, _ := dbp.Valid(); !isvalid {
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()
defer func() {
// Make sure we clear internal breakpoints if we simultaneously receive a
// manual stop request and hit a breakpoint.
if grp.cctx.CheckAndClearManualStopRequest() {
dbp.StopReason = StopManual
dbp.clearHardcodedBreakpoints()
if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 {
dbp.ClearSteppingBreakpoints()
}
grp.finishManualStop()
}
}()
for {
if grp.cctx.CheckAndClearManualStopRequest() {
dbp.StopReason = StopManual
dbp.clearHardcodedBreakpoints()
if grp.KeepSteppingBreakpoints&HaltKeepsSteppingBreakpoints == 0 {
dbp.ClearSteppingBreakpoints()
}
grp.finishManualStop()
return nil
}
dbp.ClearCaches()
trapthread, stopReason, contOnceErr := grp.continueOnce([]ProcessInternal{grp.targets[0].proc}, grp.cctx)
dbp.StopReason = stopReason
for _, dbp := range grp.targets {
dbp.ClearCaches()
}
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()
for _, thread := range threads {
if thread.Breakpoint().Breakpoint != nil {
thread.Breakpoint().Breakpoint.checkCondition(dbp, thread, thread.Breakpoint())
it := ValidTargets{Group: grp}
for it.Next() {
for _, thread := range it.ThreadList() {
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.
// Errors are ignored because depending on why ContinueOnce failed this
// might very well not work.
if valid, _ := dbp.Valid(); valid {
if trapthread != nil {
_ = dbp.SwitchThread(trapthread.ThreadID())
} else if curth := dbp.CurrentThread(); curth != nil {
dbp.selectedGoroutine, _ = GetG(curth)
}
}
_ = grp.setCurrentThreads(traptgt, trapthread)
if pe, ok := contOnceErr.(ErrProcessExited); ok {
dbp.exitStatus = pe.Status
traptgt.exitStatus = pe.Status
}
return contOnceErr
}
if dbp.StopReason == StopLaunched {
dbp.ClearSteppingBreakpoints()
if stopReason == StopLaunched {
it.Reset()
for it.Next() {
it.Target.ClearSteppingBreakpoints()
}
}
callInjectionDone, callErr := callInjectionProtocol(dbp, threads)
hcbpErr := dbp.handleHardcodedBreakpoints(trapthread, threads)
var callInjectionDone bool
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
// must always happen, otherwise the debugger could be left in an
// inconsistent state.
if err := pickCurrentThread(dbp, trapthread, threads); err != nil {
return err
it = ValidTargets{Group: grp}
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 {
return callErr
@ -138,7 +170,7 @@ func (grp *TargetGroup) Continue() error {
case curbp.Active && curbp.Stepping:
if curbp.SteppingInto {
// 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
}
if grp.GetDirection() == Forward {
@ -168,7 +200,7 @@ func (grp *TargetGroup) Continue() error {
return err
}
dbp.StopReason = StopNextFinished
return conditionErrors(threads)
return conditionErrors(grp)
}
case curbp.Active:
onNextGoroutine, err := onNextGoroutine(dbp, curthread, dbp.Breakpoints())
@ -191,19 +223,58 @@ func (grp *TargetGroup) Continue() error {
if curbp.Breakpoint.WatchType != 0 {
dbp.StopReason = StopWatchpoint
}
return conditionErrors(threads)
return conditionErrors(grp)
default:
// not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat
}
if callInjectionDone {
// a call injection was finished, don't let a breakpoint with a failed
// condition or a step breakpoint shadow this.
dbp.StopReason = StopCallReturned
return conditionErrors(threads)
return conditionErrors(grp)
}
}
}
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 {
if bp.Logical == nil {
return false
@ -211,14 +282,19 @@ func isTraceOrTraceReturn(bp *Breakpoint) bool {
return bp.Logical.Tracepoint || bp.Logical.TraceReturn
}
func conditionErrors(threads []Thread) error {
func conditionErrors(grp *TargetGroup) error {
var condErr error
for _, th := range threads {
if bp := th.Breakpoint(); bp.Breakpoint != nil && bp.CondError != nil {
if condErr == nil {
condErr = bp.CondError
} else {
return fmt.Errorf("multiple errors evaluating conditions")
for _, dbp := range grp.targets {
if isvalid, _ := dbp.Valid(); !isvalid {
continue
}
for _, th := range dbp.ThreadList() {
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:
//
// - a thread with an active stepping breakpoint
// - a thread with an active breakpoint, prioritizing trapthread
// - trapthread
func pickCurrentThread(dbp *Target, trapthread Thread, threads []Thread) error {
// - trapthread if it is not nil
// - 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 {
if bp := th.Breakpoint(); bp.Active && bp.Stepping {
return dbp.SwitchThread(th.ThreadID())
}
}
if bp := trapthread.Breakpoint(); bp.Active {
return dbp.SwitchThread(trapthread.ThreadID())
if trapthread != nil {
if bp := trapthread.Breakpoint(); bp.Active {
return dbp.SwitchThread(trapthread.ThreadID())
}
}
for _, th := range threads {
if bp := th.Breakpoint(); bp.Active {
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) {

@ -2,8 +2,11 @@ package proc
import (
"bytes"
"errors"
"fmt"
"strings"
"github.com/go-delve/delve/pkg/logflags"
)
// 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
// contain a single target process.
type TargetGroup struct {
targets []*Target
Selected *Target
procgrp ProcessGroup
targets []*Target
Selected *Target
followExecEnabled bool
RecordingManipulation
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)
// will keep the stepping breakpoints instead of clearing them.
KeepSteppingBreakpoints KeepSteppingBreakpoints
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.
func NewGroup(t *Target) *TargetGroup {
if t.partOfGroup {
panic("internal error: target is already part of a group")
}
t.partOfGroup = true
if t.Breakpoints().Logical == nil {
t.Breakpoints().Logical = make(map[int]*LogicalBreakpoint)
}
return &TargetGroup{
RecordingManipulation: t.recman,
targets: []*Target{t},
Selected: t,
cctx: &ContinueOnceContext{},
recman: t.recman,
LogicalBreakpoints: t.Breakpoints().Logical,
continueOnce: t.continueOnce,
func NewGroup(procgrp ProcessGroup, cfg NewTargetGroupConfig) (*TargetGroup, AddTargetFunc) {
grp := &TargetGroup{
procgrp: procgrp,
cctx: &ContinueOnceContext{},
LogicalBreakpoints: make(map[int]*LogicalBreakpoint),
StopReason: cfg.StopReason,
cfg: cfg,
CanDump: cfg.CanDump,
}
return grp, grp.addTarget
}
// NewGroupRestart creates a new group of targets containing t and
// sets breakpoints and other attributes from oldgrp.
// Restart copies breakpoints and follow exec status from oldgrp into grp.
// Breakpoints that can not be set will be discarded, if discard is not nil
// it will be called for each discarded breakpoint.
func NewGroupRestart(t *Target, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) *TargetGroup {
grp := NewGroup(t)
grp.LogicalBreakpoints = oldgrp.LogicalBreakpoints
t.Breakpoints().Logical = grp.LogicalBreakpoints
for _, bp := range grp.LogicalBreakpoints {
if bp.LogicalID < 0 || !bp.Enabled {
func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) {
for _, bp := range oldgrp.LogicalBreakpoints {
if _, ok := grp.LogicalBreakpoints[bp.LogicalID]; ok {
continue
}
grp.LogicalBreakpoints[bp.LogicalID] = bp
bp.TotalHitCount = 0
bp.HitCount = make(map[int64]uint64)
bp.Set.PidAddrs = nil // breakpoints set through a list of addresses can not be restored after a restart
err := grp.EnableBreakpoint(bp)
if err != nil {
if discard != nil {
discard(bp, err)
if bp.Enabled {
err := grp.EnableBreakpoint(bp)
if err != nil {
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
@ -95,10 +146,22 @@ func (grp *TargetGroup) Valid() (bool, error) {
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.
func (grp *TargetGroup) Detach(kill bool) error {
var errs []string
for _, t := range grp.targets {
for i := len(grp.targets) - 1; i >= 0; i-- {
t := grp.targets[i]
isvalid, _ := t.Valid()
if !isvalid {
continue
@ -144,12 +207,10 @@ func (grp *TargetGroup) ThreadList() []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 _, th := range t.ThreadList() {
if th == thread {
return t
}
if _, ok := t.FindThread(tid); ok {
return t
}
}
return nil
@ -274,6 +335,26 @@ func (grp *TargetGroup) DisableBreakpoint(lbp *LogicalBreakpoint) error {
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.
type ValidTargets struct {
*Target
@ -295,3 +376,9 @@ func (it *ValidTargets) Next() bool {
it.Target = nil
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) {
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")
assertNoError(p.Continue(), t, "Continue()")
assertNoError(grp.Continue(), t, "Continue()")
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
assertNoError(err, t, "GoroutinesInfo")
@ -39,6 +39,6 @@ func TestGoroutineCreationLocation(t *testing.T) {
}
p.ClearBreakpoint(bp.Addr)
p.Continue()
grp.Continue()
})
}

@ -140,8 +140,8 @@ func TestVariableEvaluation2(t *testing.T) {
}
protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) {
err := p.Continue()
withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := grp.Continue()
assertNoError(err, t, "Continue() returned an error")
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]"},
}
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue()")
withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue()")
for _, tc := range testcases {
if tc.name == "iface1" && tc.expr == "parr" {
@ -267,8 +267,8 @@ func TestVariableEvaluationShort(t *testing.T) {
}
protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) {
err := p.Continue()
withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := grp.Continue()
assertNoError(err, t, "Continue() returned an error")
for _, tc := range testcases {
@ -323,8 +323,8 @@ func TestMultilineVariableEvaluation(t *testing.T) {
}
protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) {
err := p.Continue()
withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := grp.Continue()
assertNoError(err, t, "Continue() returned an error")
for _, tc := range testcases {
@ -399,8 +399,8 @@ func TestLocalVariables(t *testing.T) {
}
protest.AllowRecording(t)
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) {
err := p.Continue()
withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := grp.Continue()
assertNoError(err, t, "Continue() returned an error")
for _, tc := range testcases {
@ -436,7 +436,7 @@ func TestLocalVariables(t *testing.T) {
func TestEmbeddedStruct(t *testing.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{
{"b.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")},
{"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())
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) {
withTestProcess("testvariables", t, func(p *proc.Target, fixture protest.Fixture) {
err := p.Continue()
withTestProcess("testvariables", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
err := grp.Continue()
assertNoError(err, t, "Continue() returned an error")
h := func(setExpr, value string) {
@ -842,8 +842,8 @@ func TestEvalExpression(t *testing.T) {
}
protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error")
withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
for i, tc := range testcases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig)
@ -872,8 +872,8 @@ func TestEvalExpression(t *testing.T) {
func TestEvalAddrAndCast(t *testing.T) {
protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error")
withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
c1addr, err := evalVariableWithCfg(p, "&c1", pnormalLoadConfig)
assertNoError(err, t, "EvalExpression(&c1)")
c1addrstr := api.ConvertVar(c1addr).SinglelineString()
@ -899,8 +899,8 @@ func TestEvalAddrAndCast(t *testing.T) {
func TestMapEvaluation(t *testing.T) {
protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error")
withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
m1v, err := evalVariableWithCfg(p, "m1", pnormalLoadConfig)
assertNoError(err, t, "EvalVariable()")
m1 := api.ConvertVar(m1v)
@ -941,8 +941,8 @@ func TestMapEvaluation(t *testing.T) {
func TestUnsafePointer(t *testing.T) {
protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error")
withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
up1v, err := evalVariableWithCfg(p, "up1", pnormalLoadConfig)
assertNoError(err, t, "EvalVariable(up1)")
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
// differs from the serialization used by the linker to produce DWARF type information
protest.AllowRecording(t)
withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error")
withTestProcess("testvariables2", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
for _, testcase := range testcases {
v, err := evalVariableWithCfg(p, testcase.name, pnormalLoadConfig)
assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", testcase.name))
@ -1065,8 +1065,8 @@ func TestPackageRenames(t *testing.T) {
}
protest.AllowRecording(t)
withTestProcess("pkgrenames", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error")
withTestProcess("pkgrenames", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
testPackageRenamesHelper(t, p, testcases)
testPackageRenamesHelper(t, p, testcases1_9)
@ -1101,8 +1101,8 @@ func TestConstants(t *testing.T) {
// Not supported on 1.9 or earlier
t.Skip("constants added in go 1.10")
}
withTestProcess("consts", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue")
withTestProcess("consts", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue")
for _, testcase := range testcases {
variable, err := evalVariableWithCfg(p, testcase.name, pnormalLoadConfig)
assertNoError(err, t, fmt.Sprintf("EvalVariable(%s)", testcase.name))
@ -1112,9 +1112,9 @@ func TestConstants(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")
assertNoError(p.Continue(), t, "Continue()")
assertNoError(grp.Continue(), t, "Continue()")
for i := 0; i < 10; i++ {
scope, err := proc.GoroutineScope(p, p.CurrentThread())
assertNoError(err, t, fmt.Sprintf("GoroutineScope (%d)", i))
@ -1260,9 +1260,8 @@ func TestCallFunction(t *testing.T) {
{`floatsum(1, 2)`, []string{":float64:3"}, nil},
}
withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, fixture protest.Fixture) {
grp := proc.NewGroup(p)
testCallFunctionSetBreakpoint(t, p, fixture)
withTestProcessArgs("fncall", t, ".", nil, protest.AllNonOptimized, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
testCallFunctionSetBreakpoint(t, p, grp, fixture)
assertNoError(grp.Continue(), t, "Continue()")
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)
assertNoError(err, t, "ReadFile")
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) {
// 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) {
assertNoError(p.Continue(), t, "Continue()")
withTestProcess("issue1531", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue()")
hasKeys := func(mv *proc.Variable, keys ...string) {
n := 0
@ -1453,9 +1452,9 @@ func assertCurrentLocationFunction(p *proc.Target, t *testing.T, fnname string)
func TestPluginVariables(t *testing.T) {
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)
assertNoError(p.Continue(), t, "Continue 1")
assertNoError(grp.Continue(), t, "Continue 1")
bp := setFunctionBreakpoint(p, t, "github.com/go-delve/delve/_fixtures/plugin2.TypesTest")
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)
}
assertNoError(p.Continue(), t, "Continue 2")
assertNoError(grp.Continue(), t, "Continue 2")
// test that PackageVariables returns variables from the executable and plugins
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)
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")
vstr, err := evalVariableWithCfg(p, "str", pnormalLoadConfig)
assertNoError(err, t, "Eval(str)")
assertVariable(t, vstr, varTest{"str", true, `"success"`, ``, `string`, nil})
assertNoError(p.StepOut(), t, "StepOut")
assertNoError(p.StepOut(), t, "StepOut")
assertNoError(p.Next(), t, "Next")
assertNoError(grp.StepOut(), t, "StepOut")
assertNoError(grp.StepOut(), t, "StepOut")
assertNoError(grp.Next(), t, "Next")
// read interface variable, inside executable code, with a concrete type defined in a plugin
vb, err := evalVariableWithCfg(p, "b", pnormalLoadConfig)
@ -1534,8 +1533,8 @@ func TestCgoEval(t *testing.T) {
}
protest.AllowRecording(t)
withTestProcess("testvariablescgo/", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue() returned an error")
withTestProcess("testvariablescgo/", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue() returned an error")
for _, tc := range testcases {
variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig)
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 {
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 {
variable, err := evalVariableWithCfg(p, tc.name, pnormalLoadConfig)
if tc.err == nil {
@ -1604,8 +1603,8 @@ func TestEvalExpressionGenerics(t *testing.T) {
// Test the behavior when reading dangling pointers produced by unsafe code.
func TestBadUnsafePtr(t *testing.T) {
withTestProcess("testunsafepointers", t, func(p *proc.Target, fixture protest.Fixture) {
assertNoError(p.Continue(), t, "Continue()")
withTestProcess("testunsafepointers", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.Continue(), t, "Continue()")
// danglingPtrPtr is a pointer with value 0x42, which is an unreadable
// address.

@ -79,6 +79,7 @@ func (ft *FakeTerminal) ExecStarlark(starlarkProgram string) (outstr string, err
}
func (ft *FakeTerminal) MustExec(cmdstr string) string {
ft.t.Helper()
outstr, err := ft.Exec(cmdstr)
if err != nil {
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 {
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 {
err = go11DecodeErrorCheck(err)
err = noDebugErrorWarning(err)
return nil, attachErrorMessage(d.config.AttachPid, err)
}
d.target = proc.NewGroup(p)
case d.config.CoreFile != "":
var p *proc.Target
var err error
switch d.config.Backend {
case "rr":
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:
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 {
err = go11DecodeErrorCheck(err)
return nil, err
}
d.target = proc.NewGroup(p)
if err := d.checkGoVersion(); err != nil {
d.target.Detach(true)
return nil, err
@ -192,7 +190,8 @@ func New(config *Config, processArgs []string) (*Debugger, error) {
default:
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 _, ok := err.(*proc.ErrUnsupportedArch); !ok {
err = go11DecodeErrorCheck(err)
@ -201,10 +200,6 @@ func New(config *Config, processArgs []string) (*Debugger, error) {
}
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 {
d.target.Detach(true)
return nil, err
@ -245,7 +240,7 @@ func (d *Debugger) TargetGoVersion() string {
}
// 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])
if err != nil {
return nil, err
@ -285,7 +280,7 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error)
go func() {
defer d.targetMutex.Unlock()
p, err := d.recordingRun(run)
grp, err := d.recordingRun(run)
if err != nil {
d.log.Errorf("could not record target: %v", err)
// 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)
}
d.recordingDone()
d.target = proc.NewGroup(p)
d.target = grp
if err := d.checkGoVersion(); err != nil {
d.log.Error(err)
err := d.target.Detach(true)
@ -332,7 +327,7 @@ func (d *Debugger) isRecording() bool {
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()
if err != nil && tracedir == "" {
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'.
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 {
case "native":
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")
func betterGdbserialLaunchError(p *proc.Target, err error) (*proc.Target, error) {
func betterGdbserialLaunchError(p *proc.TargetGroup, err error) (*proc.TargetGroup, error) {
if runtime.GOOS != "darwin" {
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.config.Redirects = newRedirects
}
var p *proc.Target
var grp *proc.TargetGroup
var err error
if rebuild {
@ -510,19 +505,20 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
}
d.recordingStart(stop)
p, err = d.recordingRun(run)
grp, err = d.recordingRun(run)
d.recordingDone()
} else {
p, err = d.Launch(d.processArgs, d.config.WorkingDir)
grp, err = d.Launch(d.processArgs, d.config.WorkingDir)
}
if err != nil {
return nil, fmt.Errorf("could not launch process: %s", err)
}
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()})
})
d.target = grp
return discarded, nil
}
@ -1321,7 +1317,7 @@ func (d *Debugger) collectBreakpointInformation(apiThread *api.Thread, thread pr
bpi := &api.BreakpointInfo{}
apiThread.BreakpointInfo = bpi
tgt := d.target.TargetForThread(thread)
tgt := d.target.TargetForThread(thread.ThreadID())
if bp.Goroutine {
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?
if !d.target.Selected.CanDump {
if !d.target.CanDump {
d.targetMutex.Unlock()
return ErrCoreDumpNotSupported
}