
This patch aims to improve how Delve tracks the current goroutine, especially in very highly parallel programs. The main spirit of this patch is to ensure that even in situations where the goroutine we care about is not executing (common for len(g) > len(m)) we still end up back on that goroutine as a result of executing the 'next' command. We accomplish this by tracking our original goroutine id, and any time a breakpoint is hit or a threads stops, we examine the stopped threads and see if any are executing the goroutine we care about. If not, we set 'next' breakpoint for them again and continue them. This is done so that one of those threads can eventually pick up the goroutine we care about and begin executing it again.
357 lines
8.8 KiB
Go
357 lines
8.8 KiB
Go
package proc
|
|
|
|
import (
|
|
"debug/elf"
|
|
"debug/gosym"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
sys "golang.org/x/sys/unix"
|
|
|
|
"github.com/derekparker/delve/dwarf/frame"
|
|
"github.com/derekparker/delve/dwarf/line"
|
|
)
|
|
|
|
// Process statuses
|
|
const (
|
|
STATUS_SLEEPING = 'S'
|
|
STATUS_RUNNING = 'R'
|
|
STATUS_TRACE_STOP = 't'
|
|
STATUS_ZOMBIE = 'Z'
|
|
)
|
|
|
|
// Not actually needed for Linux.
|
|
type OSProcessDetails interface{}
|
|
|
|
// Create and begin debugging a new process. First entry in
|
|
// `cmd` is the program to run, and then rest are the arguments
|
|
// to be supplied to that process.
|
|
func Launch(cmd []string) (*Process, error) {
|
|
var (
|
|
proc *exec.Cmd
|
|
err error
|
|
)
|
|
dbp := New(0)
|
|
dbp.execPtraceFunc(func() {
|
|
proc = exec.Command(cmd[0])
|
|
proc.Args = cmd
|
|
proc.Stdout = os.Stdout
|
|
proc.Stderr = os.Stderr
|
|
proc.SysProcAttr = &syscall.SysProcAttr{Ptrace: true, Setpgid: true}
|
|
err = proc.Start()
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbp.Pid = proc.Process.Pid
|
|
_, _, err = wait(proc.Process.Pid, proc.Process.Pid, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("waiting for target execve failed: %s", err)
|
|
}
|
|
return initializeDebugProcess(dbp, proc.Path, false)
|
|
}
|
|
|
|
// Attach to an existing process with the given PID.
|
|
func Attach(pid int) (*Process, error) {
|
|
return initializeDebugProcess(New(pid), "", true)
|
|
}
|
|
|
|
func (dbp *Process) Kill() (err error) {
|
|
if dbp.exited {
|
|
return nil
|
|
}
|
|
if !dbp.Threads[dbp.Pid].Stopped() {
|
|
return errors.New("process must be stopped in order to kill it")
|
|
}
|
|
if err = sys.Kill(-dbp.Pid, sys.SIGKILL); err != nil {
|
|
return errors.New("could not deliver signal " + err.Error())
|
|
}
|
|
if _, _, err = wait(dbp.Pid, dbp.Pid, 0); err != nil {
|
|
return
|
|
}
|
|
dbp.exited = true
|
|
return
|
|
}
|
|
|
|
func (dbp *Process) requestManualStop() (err error) {
|
|
return sys.Kill(dbp.Pid, sys.SIGTRAP)
|
|
}
|
|
|
|
// Attach to a newly created thread, and store that thread in our list of
|
|
// known threads.
|
|
func (dbp *Process) addThread(tid int, attach bool) (*Thread, error) {
|
|
if thread, ok := dbp.Threads[tid]; ok {
|
|
return thread, nil
|
|
}
|
|
|
|
var err error
|
|
if attach {
|
|
dbp.execPtraceFunc(func() { err = sys.PtraceAttach(tid) })
|
|
if err != nil && err != sys.EPERM {
|
|
// Do not return err if err == EPERM,
|
|
// we may already be tracing this thread due to
|
|
// PTRACE_O_TRACECLONE. We will surely blow up later
|
|
// if we truly don't have permissions.
|
|
return nil, fmt.Errorf("could not attach to new thread %d %s", tid, err)
|
|
}
|
|
pid, status, err := wait(tid, dbp.Pid, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if status.Exited() {
|
|
return nil, fmt.Errorf("thread already exited %d", pid)
|
|
}
|
|
}
|
|
|
|
dbp.execPtraceFunc(func() { err = syscall.PtraceSetOptions(tid, syscall.PTRACE_O_TRACECLONE) })
|
|
if err == syscall.ESRCH {
|
|
if _, _, err = wait(tid, dbp.Pid, 0); 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) })
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not set options for new traced thread %d %s", tid, err)
|
|
}
|
|
}
|
|
|
|
dbp.Threads[tid] = &Thread{
|
|
Id: tid,
|
|
dbp: dbp,
|
|
os: new(OSSpecificDetails),
|
|
}
|
|
if dbp.CurrentThread == nil {
|
|
dbp.CurrentThread = dbp.Threads[tid]
|
|
}
|
|
return dbp.Threads[tid], nil
|
|
}
|
|
|
|
func (dbp *Process) updateThreadList() error {
|
|
tids, _ := filepath.Glob(fmt.Sprintf("/proc/%d/task/*", dbp.Pid))
|
|
for _, tidpath := range tids {
|
|
tidstr := filepath.Base(tidpath)
|
|
tid, err := strconv.Atoi(tidstr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := dbp.addThread(tid, tid != dbp.Pid); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dbp *Process) findExecutable(path string) (*elf.File, error) {
|
|
if path == "" {
|
|
path = fmt.Sprintf("/proc/%d/exe", dbp.Pid)
|
|
}
|
|
f, err := os.OpenFile(path, 0, os.ModePerm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
elfFile, err := elf.NewFile(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := elfFile.DWARF()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dbp.dwarf = data
|
|
return elfFile, nil
|
|
}
|
|
|
|
func (dbp *Process) parseDebugFrame(exe *elf.File, wg *sync.WaitGroup) {
|
|
defer wg.Done()
|
|
|
|
if sec := exe.Section(".debug_frame"); sec != nil {
|
|
debugFrame, err := exe.Section(".debug_frame").Data()
|
|
if err != nil {
|
|
fmt.Println("could not get .debug_frame section", err)
|
|
os.Exit(1)
|
|
}
|
|
dbp.frameEntries = frame.Parse(debugFrame)
|
|
} else {
|
|
fmt.Println("could not find .debug_frame section in binary")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func (dbp *Process) obtainGoSymbols(exe *elf.File, wg *sync.WaitGroup) {
|
|
defer wg.Done()
|
|
|
|
var (
|
|
symdat []byte
|
|
pclndat []byte
|
|
err error
|
|
)
|
|
|
|
if sec := exe.Section(".gosymtab"); sec != nil {
|
|
symdat, err = sec.Data()
|
|
if err != nil {
|
|
fmt.Println("could not get .gosymtab section", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if sec := exe.Section(".gopclntab"); sec != nil {
|
|
pclndat, err = sec.Data()
|
|
if err != nil {
|
|
fmt.Println("could not get .gopclntab section", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
pcln := gosym.NewLineTable(pclndat, exe.Section(".text").Addr)
|
|
tab, err := gosym.NewTable(symdat, pcln)
|
|
if err != nil {
|
|
fmt.Println("could not get initialize line table", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
dbp.goSymTable = tab
|
|
}
|
|
|
|
func (dbp *Process) parseDebugLineInfo(exe *elf.File, wg *sync.WaitGroup) {
|
|
defer wg.Done()
|
|
|
|
if sec := exe.Section(".debug_line"); sec != nil {
|
|
debugLine, err := exe.Section(".debug_line").Data()
|
|
if err != nil {
|
|
fmt.Println("could not get .debug_line section", err)
|
|
os.Exit(1)
|
|
}
|
|
dbp.lineInfo = line.Parse(debugLine)
|
|
} else {
|
|
fmt.Println("could not find .debug_line section in binary")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func (dbp *Process) trapWait(pid int) (*Thread, error) {
|
|
for {
|
|
wpid, status, err := wait(pid, dbp.Pid, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wait err %s %d", err, pid)
|
|
}
|
|
if wpid == 0 {
|
|
continue
|
|
}
|
|
th, ok := dbp.Threads[wpid]
|
|
if ok {
|
|
th.Status = status
|
|
}
|
|
if status.Exited() {
|
|
if wpid == dbp.Pid {
|
|
dbp.exited = true
|
|
return nil, ProcessExitedError{Pid: wpid, Status: status.ExitStatus()}
|
|
}
|
|
delete(dbp.Threads, wpid)
|
|
continue
|
|
}
|
|
if status.StopSignal() == sys.SIGTRAP && status.TrapCause() == sys.PTRACE_EVENT_CLONE {
|
|
// A traced thread has cloned a new thread, grab the pid and
|
|
// add it to our list of traced threads.
|
|
var cloned uint
|
|
dbp.execPtraceFunc(func() { cloned, err = sys.PtraceGetEventMsg(wpid) })
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get event message: %s", err)
|
|
}
|
|
th, err = dbp.addThread(int(cloned), false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Set all hardware breakpoints on the new thread.
|
|
for _, bp := range dbp.Breakpoints {
|
|
if !bp.hardware {
|
|
continue
|
|
}
|
|
if err = dbp.setHardwareBreakpoint(bp.reg, th.Id, bp.Addr); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err = th.Continue(); err != nil {
|
|
return nil, fmt.Errorf("could not continue new thread %d %s", cloned, err)
|
|
}
|
|
if err = dbp.Threads[int(wpid)].Continue(); err != nil {
|
|
return nil, fmt.Errorf("could not continue existing thread %d %s", cloned, err)
|
|
}
|
|
continue
|
|
}
|
|
if th == nil {
|
|
// Sometimes we get an unknown thread, ignore it?
|
|
continue
|
|
}
|
|
if status.StopSignal() == sys.SIGTRAP && dbp.halt {
|
|
th.running = false
|
|
dbp.halt = false
|
|
return th, nil
|
|
}
|
|
if status.StopSignal() == sys.SIGTRAP {
|
|
th.running = false
|
|
return dbp.handleBreakpointOnThread(wpid)
|
|
}
|
|
if th != nil {
|
|
// TODO(dp) alert user about unexpected signals here.
|
|
if err := th.Continue(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func status(pid int) rune {
|
|
f, err := os.Open(fmt.Sprintf("/proc/%d/stat", pid))
|
|
if err != nil {
|
|
return '\000'
|
|
}
|
|
defer f.Close()
|
|
|
|
var (
|
|
p int
|
|
comm string
|
|
state rune
|
|
)
|
|
fmt.Fscanf(f, "%d %s %c", &p, &comm, &state)
|
|
return state
|
|
}
|
|
|
|
func wait(pid, tgid, options int) (int, *sys.WaitStatus, error) {
|
|
var s sys.WaitStatus
|
|
if (pid != tgid) || (options != 0) {
|
|
wpid, err := sys.Wait4(pid, &s, sys.WALL|options, nil)
|
|
return wpid, &s, err
|
|
} else {
|
|
// If we call wait4/waitpid on a thread that is the leader of its group,
|
|
// with options == 0, while ptracing and the thread leader has exited leaving
|
|
// zombies of its own then waitpid hangs forever this is apparently intended
|
|
// behaviour in the linux kernel because it's just so convenient.
|
|
// Therefore we call wait4 in a loop with WNOHANG, sleeping a while between
|
|
// calls and exiting when either wait4 succeeds or we find out that the thread
|
|
// has become a zombie.
|
|
// References:
|
|
// https://sourceware.org/bugzilla/show_bug.cgi?id=12702
|
|
// https://sourceware.org/bugzilla/show_bug.cgi?id=10095
|
|
// https://sourceware.org/bugzilla/attachment.cgi?id=5685
|
|
for {
|
|
wpid, err := sys.Wait4(pid, &s, sys.WNOHANG|sys.WALL|options, nil)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
if wpid != 0 {
|
|
return wpid, &s, err
|
|
}
|
|
if status(pid) == STATUS_ZOMBIE {
|
|
return pid, nil, nil
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
}
|
|
}
|