-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proc: changed windows backend to deal with simultaneous breakpoints #598
Changes from all commits
d054cc1
7ff7a04
3a64c82
437b53e
678d220
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"runtime" | ||
) | ||
|
||
func dontsegfault() { | ||
var p *int | ||
func() int { | ||
defer func() { | ||
recover() | ||
}() | ||
return *p | ||
}() | ||
} | ||
|
||
func main() { | ||
dontsegfault() | ||
runtime.Breakpoint() | ||
fmt.Println("should stop here") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,19 +91,24 @@ func Launch(cmd []string) (*Process, error) { | |
si.StdOutput = sys.Handle(fd[1]) | ||
si.StdErr = sys.Handle(fd[2]) | ||
pi := new(sys.ProcessInformation) | ||
err = sys.CreateProcess(argv0, cmdLine, nil, nil, true, _DEBUG_ONLY_THIS_PROCESS, nil, nil, si, pi) | ||
|
||
dbp := New(0) | ||
dbp.execPtraceFunc(func() { | ||
err = sys.CreateProcess(argv0, cmdLine, nil, nil, true, _DEBUG_ONLY_THIS_PROCESS, nil, nil, si, pi) | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
sys.CloseHandle(sys.Handle(pi.Process)) | ||
sys.CloseHandle(sys.Handle(pi.Thread)) | ||
|
||
return newDebugProcess(int(pi.ProcessId), argv0Go) | ||
dbp.Pid = int(pi.ProcessId) | ||
|
||
return newDebugProcess(dbp, argv0Go) | ||
} | ||
|
||
// newDebugProcess prepares process pid for debugging. | ||
func newDebugProcess(pid int, exepath string) (*Process, error) { | ||
dbp := New(pid) | ||
func newDebugProcess(dbp *Process, exepath string) (*Process, error) { | ||
// It should not actually be possible for the | ||
// call to waitForDebugEvent to fail, since Windows | ||
// will always fire a CREATE_PROCESS_DEBUG_EVENT event | ||
|
@@ -112,7 +117,7 @@ func newDebugProcess(pid int, exepath string) (*Process, error) { | |
var err error | ||
var tid, exitCode int | ||
dbp.execPtraceFunc(func() { | ||
tid, exitCode, err = dbp.waitForDebugEvent() | ||
tid, exitCode, err = dbp.waitForDebugEvent(waitBlocking) | ||
}) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -121,6 +126,22 @@ func newDebugProcess(pid int, exepath string) (*Process, error) { | |
dbp.postExit() | ||
return nil, ProcessExitedError{Pid: dbp.Pid, Status: exitCode} | ||
} | ||
// Suspend all threads so that the call to _ContinueDebugEvent will | ||
// not resume the target. | ||
for _, thread := range dbp.Threads { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want to start process with all its threads suspended, you can pass CREATE_SUSPENDED into CreateProcess. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't work, the first WaitForDebugEvent call never returns (I'm not saying there is no way to make it work, just that I don't know how). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like maybe that is unnecessary (the first There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Fair enough. I don't know much about debugging to be useful here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to add a comment here as to why we're suspending and then continuing. It's hard to tell from here that we're trying to drain all events. It's probably worth putting this logic into a well named function so it's apparent to the reader what's going on. |
||
_, err := _SuspendThread(thread.os.hThread) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to wrap this in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lukehoban code didn't, their documentation doesn't mention it, and it doesn't seem to cause problems. |
||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
dbp.execPtraceFunc(func() { | ||
err = _ContinueDebugEvent(uint32(dbp.Pid), uint32(dbp.os.breakThread), _DBG_CONTINUE) | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return initializeDebugProcess(dbp, exepath, false) | ||
} | ||
|
||
|
@@ -169,7 +190,7 @@ func Attach(pid int) (*Process, error) { | |
if err != nil { | ||
return nil, err | ||
} | ||
return newDebugProcess(pid, exepath) | ||
return newDebugProcess(New(pid), exepath) | ||
} | ||
|
||
// Kill kills the process. | ||
|
@@ -198,7 +219,7 @@ func (dbp *Process) updateThreadList() error { | |
return nil | ||
} | ||
|
||
func (dbp *Process) addThread(hThread syscall.Handle, threadID int, attach bool) (*Thread, error) { | ||
func (dbp *Process) addThread(hThread syscall.Handle, threadID int, attach, suspendNewThreads bool) (*Thread, error) { | ||
if thread, ok := dbp.Threads[threadID]; ok { | ||
return thread, nil | ||
} | ||
|
@@ -212,6 +233,12 @@ func (dbp *Process) addThread(hThread syscall.Handle, threadID int, attach bool) | |
if dbp.CurrentThread == nil { | ||
dbp.SwitchThread(thread.ID) | ||
} | ||
if suspendNewThreads { | ||
_, err := _SuspendThread(thread.os.hThread) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
return thread, nil | ||
} | ||
|
||
|
@@ -402,12 +429,24 @@ func dwarfFromPE(f *pe.File) (*dwarf.Data, error) { | |
return dwarf.New(abbrev, nil, nil, info, line, nil, nil, str) | ||
} | ||
|
||
func (dbp *Process) waitForDebugEvent() (threadID, exitCode int, err error) { | ||
type waitForDebugEventFlags int | ||
|
||
const ( | ||
waitBlocking waitForDebugEventFlags = 1 << iota | ||
waitSuspendNewThreads | ||
) | ||
|
||
func (dbp *Process) waitForDebugEvent(flags waitForDebugEventFlags) (threadID, exitCode int, err error) { | ||
var debugEvent _DEBUG_EVENT | ||
shouldExit := false | ||
for { | ||
continueStatus := uint32(_DBG_CONTINUE) | ||
var milliseconds uint32 = 0 | ||
if flags&waitBlocking != 0 { | ||
milliseconds = syscall.INFINITE | ||
} | ||
// Wait for a debug event... | ||
err := _WaitForDebugEvent(&debugEvent, syscall.INFINITE) | ||
err := _WaitForDebugEvent(&debugEvent, milliseconds) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wander what the err is when milliseconds is set to 0 and you have no debug events pending. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The semaphore timeout period has expired. |
||
if err != nil { | ||
return 0, 0, err | ||
} | ||
|
@@ -425,14 +464,14 @@ func (dbp *Process) waitForDebugEvent() (threadID, exitCode int, err error) { | |
} | ||
} | ||
dbp.os.hProcess = debugInfo.Process | ||
_, err = dbp.addThread(debugInfo.Thread, int(debugEvent.ThreadId), false) | ||
_, err = dbp.addThread(debugInfo.Thread, int(debugEvent.ThreadId), false, flags&waitSuspendNewThreads != 0) | ||
if err != nil { | ||
return 0, 0, err | ||
} | ||
break | ||
case _CREATE_THREAD_DEBUG_EVENT: | ||
debugInfo := (*_CREATE_THREAD_DEBUG_INFO)(unionPtr) | ||
_, err = dbp.addThread(debugInfo.Thread, int(debugEvent.ThreadId), false) | ||
_, err = dbp.addThread(debugInfo.Thread, int(debugEvent.ThreadId), false, flags&waitSuspendNewThreads != 0) | ||
if err != nil { | ||
return 0, 0, err | ||
} | ||
|
@@ -458,9 +497,14 @@ func (dbp *Process) waitForDebugEvent() (threadID, exitCode int, err error) { | |
case _RIP_EVENT: | ||
break | ||
case _EXCEPTION_DEBUG_EVENT: | ||
tid := int(debugEvent.ThreadId) | ||
dbp.os.breakThread = tid | ||
return tid, 0, nil | ||
exception := (*_EXCEPTION_DEBUG_INFO)(unionPtr) | ||
if code := exception.ExceptionRecord.ExceptionCode; code == _EXCEPTION_BREAKPOINT || code == _EXCEPTION_SINGLE_STEP { | ||
tid := int(debugEvent.ThreadId) | ||
dbp.os.breakThread = tid | ||
return tid, 0, nil | ||
} else { | ||
continueStatus = _DBG_EXCEPTION_NOT_HANDLED | ||
} | ||
case _EXIT_PROCESS_DEBUG_EVENT: | ||
debugInfo := (*_EXIT_PROCESS_DEBUG_INFO)(unionPtr) | ||
exitCode = int(debugInfo.ExitCode) | ||
|
@@ -470,7 +514,7 @@ func (dbp *Process) waitForDebugEvent() (threadID, exitCode int, err error) { | |
} | ||
|
||
// .. and then continue unless we received an event that indicated we should break into debugger. | ||
err = _ContinueDebugEvent(debugEvent.ProcessId, debugEvent.ThreadId, _DBG_CONTINUE) | ||
err = _ContinueDebugEvent(debugEvent.ProcessId, debugEvent.ThreadId, continueStatus) | ||
if err != nil { | ||
return 0, 0, err | ||
} | ||
|
@@ -485,7 +529,7 @@ func (dbp *Process) trapWait(pid int) (*Thread, error) { | |
var err error | ||
var tid, exitCode int | ||
dbp.execPtraceFunc(func() { | ||
tid, exitCode, err = dbp.waitForDebugEvent() | ||
tid, exitCode, err = dbp.waitForDebugEvent(waitBlocking) | ||
}) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -507,39 +551,75 @@ func (dbp *Process) wait(pid, options int) (int, *sys.WaitStatus, error) { | |
} | ||
|
||
func (dbp *Process) setCurrentBreakpoints(trapthread *Thread) error { | ||
// TODO: In theory, we should also be setting the breakpoints on other | ||
// threads that happen to have hit this BP. But doing so leads to periodic | ||
// failures in the TestBreakpointsCounts test with hit counts being too high, | ||
// which can be traced back to occurrences of multiple threads hitting a BP | ||
// at the same time. | ||
// While the debug event that stopped the target was being propagated | ||
// other target threads could generate other debug events. | ||
// After this function we need to know about all the threads | ||
// stopped on a breakpoint. To do that we first suspend all target | ||
// threads and then repeatedly call _ContinueDebugEvent followed by | ||
// waitForDebugEvent in non-blocking mode. | ||
// We need to explicitly call SuspendThread because otherwise the | ||
// call to _ContinueDebugEvent will resume execution of some of the | ||
// target threads. | ||
|
||
err := trapthread.SetCurrentBreakpoint() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// My guess is that Windows will correctly trigger multiple DEBUG_EVENT's | ||
// in this case, one for each thread, so we should only handle the BP hit | ||
// on the thread that the debugger was evented on. | ||
for _, thread := range dbp.Threads { | ||
thread.running = false | ||
_, err := _SuspendThread(thread.os.hThread) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
for { | ||
var err error | ||
var tid int | ||
dbp.execPtraceFunc(func() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't like the idea of this function having the side effect of potentially causing the inferior to execute instructions. We continue and then immediately execute a non-blocking wait, which will return an error when we do not have any pending events. That means we never stop this thread, correct? Is is not possible to just loop through There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The loop on
|
||
err = _ContinueDebugEvent(uint32(dbp.Pid), uint32(dbp.os.breakThread), _DBG_CONTINUE) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only thing that sticks out to me in this patch set. Here, we're continuing a thread and waiting for a debug event, however in the meantime it's possible that the thread will be executing instructions. This function should not have that side effect. A non-blocking wait for debug event should suffice, yeah? If there's no events we simply move on. I don't understand the need to continue a thread in a function that is setting state on our thread objects. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we suspend all threads (which we did above this line) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, so if I understand correctly, it behaves similar to the Darwin API, where you can suspend a thread multiple times, and have to resume an equal number of times for it to actually execute instructions. Is that correct, from your understanding? If so, could you add a comment / link to docs to make it clear we're not actually continuing execution. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's like the darwin API, however I don't actually ever suspend the thread more than once, when one of the threads generates a debug event windows will halt the execution of all threads but this state doesn't count as suspended, it's weird. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a comment describing what we are doing on that function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. I think maybe we should alias There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Googling DBG_EXCEPTION_HANDLED leads me to this blog post which suggests that at some point DBG_EXCEPTION_HANDLED and DBG_CONTINUE did different things and had different values, I think appropriating the name as a synonym muddles the water but if you insist I'll switch. |
||
if err == nil { | ||
tid, _, _ = dbp.waitForDebugEvent(waitSuspendNewThreads) | ||
} | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
if tid == 0 { | ||
break | ||
} | ||
err = dbp.Threads[tid].SetCurrentBreakpoint() | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return trapthread.SetCurrentBreakpoint() | ||
return nil | ||
} | ||
|
||
func (dbp *Process) exitGuard(err error) error { | ||
return err | ||
} | ||
|
||
func (dbp *Process) resume() error { | ||
// Only resume the thread that broke into the debugger | ||
thread := dbp.Threads[dbp.os.breakThread] | ||
// This relies on the same assumptions as dbp.setCurrentBreakpoints | ||
if thread.CurrentBreakpoint != nil { | ||
if err := thread.StepInstruction(); err != nil { | ||
return err | ||
for _, thread := range dbp.Threads { | ||
if thread.CurrentBreakpoint != nil { | ||
if err := thread.StepInstruction(); err != nil { | ||
return err | ||
} | ||
thread.CurrentBreakpoint = nil | ||
} | ||
thread.CurrentBreakpoint = nil | ||
} | ||
// In case we are now on a different thread, make sure we resume | ||
// the thread that is broken. | ||
thread = dbp.Threads[dbp.os.breakThread] | ||
if err := thread.resume(); err != nil { | ||
return err | ||
|
||
for _, thread := range dbp.Threads { | ||
thread.running = true | ||
_, err := _ResumeThread(thread.os.hThread) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a comment / explanation / TODO about why we're ignoring errors.