Fix: properly handle random signals

* Ignore signals we do not care about
* Implement custom fork/exec for Darwin to convert signals to mach
  exceptions
This commit is contained in:
Derek Parker 2015-04-25 09:46:16 -05:00
parent 047a91af5b
commit 173ee20097
7 changed files with 228 additions and 82 deletions

49
proctl/exec_darwin.c Normal file

@ -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);
}

9
proctl/exec_darwin.h Normal file

@ -0,0 +1,9 @@
#include "proctl_darwin.h"
#include <unistd.h>
#include <sys/ptrace.h>
#include <errno.h>
#include <stdlib.h>
int
fork_exec(char *argv0, char **argv, mach_port_name_t*, mach_port_t*, mach_port_t*, mach_port_t*);

@ -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 {

@ -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, &notificationPort))
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) {

@ -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
}
}
}
}

@ -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)

@ -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")