diff --git a/proctl/exec_darwin.c b/proctl/exec_darwin.c new file mode 100644 index 00000000..1784c0c6 --- /dev/null +++ b/proctl/exec_darwin.c @@ -0,0 +1,49 @@ +#include "exec_darwin.h" + +int +fork_exec(char *argv0, char **argv, + mach_port_name_t *task, + mach_port_t *port_set, + mach_port_t *exception_port, + mach_port_t *notification_port) +{ + int fd[2]; + if (pipe(fd) < 0) return -1; + + kern_return_t kret; + pid_t pid = fork(); + if (pid > 0) { + // In parent. + close(fd[0]); + kret = acquire_mach_task(pid, task, port_set, exception_port, notification_port); + if (kret != KERN_SUCCESS) return -1; + + char msg = 'c'; + write(fd[1], &msg, 1); + close(fd[1]); + return pid; + } + + // Fork succeeded, we are in the child. + int pret; + char sig; + + close(fd[1]); + read(fd[0], &sig, 1); + close(fd[0]); + + // Set errno to zero before a call to ptrace. + // It is documented that ptrace can return -1 even + // for successful calls. + errno = 0; + pret = ptrace(PT_TRACE_ME, 0, 0, 0); + if (pret != 0 && errno != 0) return -errno; + + errno = 0; + pret = ptrace(PT_SIGEXC, 0, 0, 0); + if (pret != 0 && errno != 0) return -errno; + + // Create the child process. + execve(argv0, argv, NULL); + exit(1); +} diff --git a/proctl/exec_darwin.h b/proctl/exec_darwin.h new file mode 100644 index 00000000..c89dd65e --- /dev/null +++ b/proctl/exec_darwin.h @@ -0,0 +1,9 @@ +#include "proctl_darwin.h" + +#include +#include +#include +#include + +int +fork_exec(char *argv0, char **argv, mach_port_name_t*, mach_port_t*, mach_port_t*, mach_port_t*); diff --git a/proctl/proctl.go b/proctl/proctl.go index 2de9db95..c4c92d6f 100644 --- a/proctl/proctl.go +++ b/proctl/proctl.go @@ -6,12 +6,10 @@ import ( "encoding/binary" "fmt" "os" - "os/exec" "path/filepath" "strconv" "strings" "sync" - "syscall" sys "golang.org/x/sys/unix" @@ -34,6 +32,8 @@ type DebuggedProcess struct { goSymTable *gosym.Table frameEntries frame.FrameDescriptionEntries lineInfo *line.DebugLineInfo + firstStart bool + singleStep bool os *OSProcessDetails ast *source.Searcher breakpointIDCounter int @@ -63,35 +63,20 @@ func (pe ProcessExitedError) Error() string { // Attach to an existing process with the given PID. func Attach(pid int) (*DebuggedProcess, error) { - dbp, err := newDebugProcess(pid, true) + dbp := &DebuggedProcess{ + Pid: pid, + Threads: make(map[int]*ThreadContext), + BreakPoints: make(map[uint64]*BreakPoint), + os: new(OSProcessDetails), + ast: source.New(), + } + dbp, err := initializeDebugProcess(dbp, "", true) if err != nil { return nil, err } return dbp, nil } -// 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) (*DebuggedProcess, error) { - proc := exec.Command(cmd[0]) - proc.Args = cmd - proc.Stdout = os.Stdout - proc.Stderr = os.Stderr - proc.SysProcAttr = &syscall.SysProcAttr{Ptrace: true} - - if err := proc.Start(); err != nil { - return nil, err - } - - _, _, err := wait(proc.Process.Pid, 0) - if err != nil { - return nil, fmt.Errorf("waiting for target execve failed: %s", err) - } - - return newDebugProcess(proc.Process.Pid, false) -} - // Returns whether or not Delve thinks the debugged // process has exited. func (dbp *DebuggedProcess) Exited() bool { @@ -109,10 +94,10 @@ func (dbp *DebuggedProcess) Running() bool { // * Dwarf .debug_frame section // * Dwarf .debug_line section // * Go symbol table. -func (dbp *DebuggedProcess) LoadInformation() error { +func (dbp *DebuggedProcess) LoadInformation(path string) error { var wg sync.WaitGroup - exe, err := dbp.findExecutable() + exe, err := dbp.findExecutable(path) if err != nil { return err } @@ -367,6 +352,8 @@ func (dbp *DebuggedProcess) resume() error { // Single step, will execute a single instruction. func (dbp *DebuggedProcess) Step() (err error) { fn := func() error { + dbp.singleStep = true + defer func() { dbp.singleStep = false }() for _, th := range dbp.Threads { if th.blocked() { continue @@ -491,33 +478,25 @@ func (dbp *DebuggedProcess) FindBreakpoint(pc uint64) (*BreakPoint, bool) { } // Returns a new DebuggedProcess struct. -func newDebugProcess(pid int, attach bool) (*DebuggedProcess, error) { - dbp := DebuggedProcess{ - Pid: pid, - Threads: make(map[int]*ThreadContext), - BreakPoints: make(map[uint64]*BreakPoint), - os: new(OSProcessDetails), - ast: source.New(), - } - +func initializeDebugProcess(dbp *DebuggedProcess, path string, attach bool) (*DebuggedProcess, error) { if attach { - err := sys.PtraceAttach(pid) + err := sys.PtraceAttach(dbp.Pid) if err != nil { return nil, err } - _, _, err = wait(pid, 0) + _, _, err = wait(dbp.Pid, 0) if err != nil { return nil, err } } - proc, err := os.FindProcess(pid) + proc, err := os.FindProcess(dbp.Pid) if err != nil { return nil, err } dbp.Process = proc - err = dbp.LoadInformation() + err = dbp.LoadInformation(path) if err != nil { return nil, err } @@ -526,7 +505,7 @@ func newDebugProcess(pid int, attach bool) (*DebuggedProcess, error) { return nil, err } - return &dbp, nil + return dbp, nil } func (dbp *DebuggedProcess) clearTempBreakpoints() error { @@ -575,7 +554,7 @@ func (dbp *DebuggedProcess) handleBreakpointOnThread(id int) (*ThreadContext, er if dbp.halt { return thread, nil } - return nil, fmt.Errorf("no breakpoint at %#v", pc) + return nil, NoBreakPointError{addr: pc} } func (dbp *DebuggedProcess) run(fn func() error) error { diff --git a/proctl/proctl_darwin.go b/proctl/proctl_darwin.go index 3cc6d1b2..83f66be1 100644 --- a/proctl/proctl_darwin.go +++ b/proctl/proctl_darwin.go @@ -1,17 +1,20 @@ package proctl // #include "proctl_darwin.h" +// #include "exec_darwin.h" import "C" import ( "debug/gosym" "debug/macho" "fmt" "os" + "path/filepath" "sync" "unsafe" "github.com/derekparker/delve/dwarf/frame" "github.com/derekparker/delve/dwarf/line" + "github.com/derekparker/delve/source" sys "golang.org/x/sys/unix" ) @@ -22,6 +25,55 @@ type OSProcessDetails struct { notificationPort C.mach_port_t } +// Create and begin debugging a new process. Uses a +// custom fork/exec process in order to take advantage of +// PT_SIGEXC on Darwin. +func Launch(cmd []string) (*DebuggedProcess, error) { + argv0, err := filepath.Abs(cmd[0]) + if err != nil { + return nil, err + } + var ( + task C.mach_port_name_t + portSet C.mach_port_t + exceptionPort C.mach_port_t + notificationPort C.mach_port_t + + argv = C.CString(cmd[0]) + ) + + if len(cmd) == 1 { + argv = nil + } + + pid := int(C.fork_exec(C.CString(argv0), &argv, &task, &portSet, &exceptionPort, ¬ificationPort)) + if pid <= 0 { + return nil, fmt.Errorf("could not fork/exec") + } + + dbp := &DebuggedProcess{ + Pid: pid, + Threads: make(map[int]*ThreadContext), + BreakPoints: make(map[uint64]*BreakPoint), + firstStart: true, + os: new(OSProcessDetails), + ast: source.New(), + } + + dbp.os = &OSProcessDetails{ + task: task, + portSet: portSet, + exceptionPort: exceptionPort, + notificationPort: notificationPort, + } + dbp, err = initializeDebugProcess(dbp, argv0, false) + if err != nil { + return nil, err + } + err = dbp.Continue() + return dbp, err +} + func (dbp *DebuggedProcess) requestManualStop() (err error) { var ( task = C.mach_port_t(dbp.os.task) @@ -153,16 +205,11 @@ func (dbp *DebuggedProcess) parseDebugLineInfo(exe *macho.File, wg *sync.WaitGro } } -func (dbp *DebuggedProcess) findExecutable() (*macho.File, error) { - ret := C.acquire_mach_task(C.int(dbp.Pid), &dbp.os.task, &dbp.os.portSet, &dbp.os.exceptionPort, &dbp.os.notificationPort) - if ret != C.KERN_SUCCESS { - return nil, fmt.Errorf("could not acquire mach task %d", ret) +func (dbp *DebuggedProcess) findExecutable(path string) (*macho.File, error) { + if path == "" { + path = C.GoString(C.find_executable(C.int(dbp.Pid))) } - pathptr, err := C.find_executable(C.int(dbp.Pid)) - if err != nil { - return nil, err - } - exe, err := macho.Open(C.GoString(pathptr)) + exe, err := macho.Open(path) if err != nil { return nil, err } @@ -175,32 +222,53 @@ func (dbp *DebuggedProcess) findExecutable() (*macho.File, error) { } func (dbp *DebuggedProcess) trapWait(pid int) (*ThreadContext, error) { - port := C.mach_port_wait(dbp.os.portSet) + var ( + th *ThreadContext + err error + ) + for { + port := C.mach_port_wait(dbp.os.portSet) - switch port { - case dbp.os.notificationPort: - _, status, err := wait(dbp.Pid, 0) + switch port { + case dbp.os.notificationPort: + _, status, err := wait(dbp.Pid, 0) + if err != nil { + return nil, err + } + dbp.exited = true + return nil, ProcessExitedError{Pid: dbp.Pid, Status: status.ExitStatus()} + case C.MACH_RCV_INTERRUPTED: + if !dbp.halt { + // Call trapWait again, it seems + // MACH_RCV_INTERRUPTED is emitted before + // process natural death _sometimes_. + return dbp.trapWait(pid) + } + return nil, ManualStopError{} + case 0: + return nil, fmt.Errorf("error while waiting for task") + } + + // Since we cannot be notified of new threads on OS X + // this is as good a time as any to check for them. + dbp.updateThreadList() + th, err = dbp.handleBreakpointOnThread(int(port)) if err != nil { + if _, ok := err.(NoBreakPointError); ok { + if dbp.firstStart || dbp.singleStep { + dbp.firstStart = false + return dbp.Threads[int(port)], nil + } + if th, ok := dbp.Threads[int(port)]; ok { + th.Continue() + } + continue + } return nil, err } - dbp.exited = true - return nil, ProcessExitedError{Pid: dbp.Pid, Status: status.ExitStatus()} - case C.MACH_RCV_INTERRUPTED: - if !dbp.halt { - // Call trapWait again, it seems - // MACH_RCV_INTERRUPTED is emitted before - // process natural death _sometimes_. - return dbp.trapWait(pid) - } - return nil, ManualStopError{} - case 0: - return nil, fmt.Errorf("error while waiting for task") + return th, nil } - - // Since we cannot be notified of new threads on OS X - // this is as good a time as any to check for them. - dbp.updateThreadList() - return dbp.handleBreakpointOnThread(int(port)) + return th, nil } func wait(pid, options int) (int, *sys.WaitStatus, error) { diff --git a/proctl/proctl_linux.go b/proctl/proctl_linux.go index 92850e9d..a88d94a3 100644 --- a/proctl/proctl_linux.go +++ b/proctl/proctl_linux.go @@ -5,6 +5,7 @@ import ( "debug/gosym" "fmt" "os" + "os/exec" "path/filepath" "strconv" "sync" @@ -14,6 +15,7 @@ import ( "github.com/derekparker/delve/dwarf/frame" "github.com/derekparker/delve/dwarf/line" + "github.com/derekparker/delve/source" ) const ( @@ -25,6 +27,34 @@ const ( // 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) (*DebuggedProcess, error) { + proc := exec.Command(cmd[0]) + proc.Args = cmd + proc.Stdout = os.Stdout + proc.Stderr = os.Stderr + proc.SysProcAttr = &syscall.SysProcAttr{Ptrace: true} + + if err := proc.Start(); err != nil { + return nil, err + } + _, _, err := wait(proc.Process.Pid, 0) + if err != nil { + return nil, fmt.Errorf("waiting for target execve failed: %s", err) + } + dbp := &DebuggedProcess{ + Pid: proc.Process.Pid, + Threads: make(map[int]*ThreadContext), + BreakPoints: make(map[uint64]*BreakPoint), + os: new(OSProcessDetails), + ast: source.New(), + } + + return initializeDebugProcess(dbp, proc.Path, false) +} + func (dbp *DebuggedProcess) requestManualStop() (err error) { return sys.Kill(dbp.Pid, sys.SIGSTOP) } @@ -102,10 +132,11 @@ func (dbp *DebuggedProcess) updateThreadList() error { return nil } -func (dbp *DebuggedProcess) findExecutable() (*elf.File, error) { - procpath := fmt.Sprintf("/proc/%d/exe", dbp.Pid) - - f, err := os.OpenFile(procpath, 0, os.ModePerm) +func (dbp *DebuggedProcess) 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 } @@ -200,13 +231,17 @@ func (dbp *DebuggedProcess) trapWait(pid int) (*ThreadContext, error) { if wpid == 0 { continue } - if th, ok := dbp.Threads[wpid]; ok { + th, ok := dbp.Threads[wpid] + if ok { th.Status = status } - if status.Exited() && wpid == dbp.Pid { - dbp.exited = true - return nil, ProcessExitedError{Pid: wpid, Status: status.ExitStatus()} + if status.Exited() { + if wpid == dbp.Pid { + dbp.exited = true + return nil, ProcessExitedError{Pid: wpid, Status: status.ExitStatus()} + } + continue } if status.StopSignal() == sys.SIGTRAP && status.TrapCause() == sys.PTRACE_EVENT_CLONE { // A traced thread has cloned a new thread, grab the pid and @@ -216,7 +251,7 @@ func (dbp *DebuggedProcess) trapWait(pid int) (*ThreadContext, error) { return nil, fmt.Errorf("could not get event message: %s", err) } - th, err := dbp.addThread(int(cloned), false) + th, err = dbp.addThread(int(cloned), false) if err != nil { return nil, err } @@ -238,6 +273,11 @@ func (dbp *DebuggedProcess) trapWait(pid int) (*ThreadContext, error) { if status.StopSignal() == sys.SIGSTOP && dbp.halt { return nil, ManualStopError{} } + if th != nil { + if err := th.Continue(); err != nil { + return nil, err + } + } } } diff --git a/proctl/proctl_test.go b/proctl/proctl_test.go index cab0d31a..be49cbd9 100644 --- a/proctl/proctl_test.go +++ b/proctl/proctl_test.go @@ -77,7 +77,7 @@ func TestExit(t *testing.T) { err := p.Continue() pe, ok := err.(ProcessExitedError) if !ok { - t.Fatalf("Continue() returned unexpected error type") + t.Fatalf("Continue() returned unexpected error type %s", err) } if pe.Status != 0 { t.Errorf("Unexpected error status: %d", pe.Status) diff --git a/proctl/variables_test.go b/proctl/variables_test.go index e0e9f9bd..5f212148 100644 --- a/proctl/variables_test.go +++ b/proctl/variables_test.go @@ -112,6 +112,7 @@ func TestVariableFunctionScoping(t *testing.T) { err = p.Continue() assertNoError(err, t, "Continue() returned an error") + p.Clear(pc) _, err = p.EvalSymbol("a1") assertNoError(err, t, "Unable to find variable a1")