delve/pkg/proc/native/proc.go

352 lines
8.9 KiB
Go
Raw Normal View History

package native
import (
"os"
"runtime"
"github.com/go-delve/delve/pkg/proc"
)
// Process represents all of the information the debugger
// is holding onto regarding the process we are debugging.
type nativeProcess struct {
bi *proc.BinaryInfo
pid int // Process Pid
// Breakpoint table, holds information on breakpoints.
// Maps instruction address to Breakpoint struct.
breakpoints proc.BreakpointMap
// List of threads mapped as such: pid -> *Thread
threads map[int]*nativeThread
proc/*: remove proc.Thread.Blocked, refactor memory access (#2206) On linux we can not read memory if the thread we use to do it is occupied doing certain system calls. The exact conditions when this happens have never been clear. This problem was worked around by using the Blocked method which recognized the most common circumstances where this would happen. However this is a hack: Blocked returning true doesn't mean that the problem will manifest and Blocked returning false doesn't necessarily mean the problem will not manifest. A side effect of this is issue #2151 where sometimes we can't read the memory of a thread and find its associated goroutine. This commit fixes this problem by always reading memory using a thread we know to be good for this, specifically the one returned by ContinueOnce. In particular the changes are as follows: 1. Remove (ProcessInternal).CurrentThread and (ProcessInternal).SetCurrentThread, the "current thread" becomes a field of Target, CurrentThread becomes a (*Target) method and (*Target).SwitchThread basically just sets a field Target. 2. The backends keep track of their own internal idea of what the current thread is, to use it to read memory, this is the thread they return from ContinueOnce as trapthread 3. The current thread in the backend and the current thread in Target only ever get synchronized in two places: when the backend creates a Target object the currentThread field of Target is initialized with the backend's current thread and when (*Target).Restart gets called (when a recording is rewound the currentThread used by Target might not exist anymore). 4. We remove the MemoryReadWriter interface embedded in Thread and instead add a Memory method to Process that returns a MemoryReadWriter. The backends will return something here that will read memory using the current thread saved by the backend. 5. The Thread.Blocked method is removed One possible problem with this change is processes that have threads with different memory maps. As far as I can determine this could happen on old versions of linux but this option was removed in linux 2.5. Fixes #2151
2020-11-09 19:28:40 +00:00
// 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
// Controlling terminal file descriptor for
// this process.
ctty *os.File
iscgo bool
exited, detached bool
}
// newProcess returns an initialized Process struct. Before returning,
// it will also launch a goroutine in order to handle ptrace(2)
// functions. For more information, see the documentation on
// `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),
}
go dbp.handlePtraceFuncs()
return dbp
}
// BinInfo will return the binary info struct associated with this process.
func (dbp *nativeProcess) BinInfo() *proc.BinaryInfo {
return dbp.bi
}
// StartCallInjection notifies the backend that we are about to inject a function call.
func (dbp *nativeProcess) StartCallInjection() (func(), error) { return func() {}, nil }
// Detach from the process being debugged, optionally killing it.
func (dbp *nativeProcess) Detach(kill bool) (err error) {
if dbp.exited {
return nil
}
if kill && dbp.childProcess {
err := dbp.kill()
if err != nil {
return err
}
dbp.bi.Close()
return nil
}
dbp.execPtraceFunc(func() {
err = dbp.detach(kill)
if err != nil {
return
}
if kill {
err = killProcess(dbp.pid)
}
})
dbp.detached = true
dbp.postExit()
return
}
// Valid returns whether the process is still attached to and
// has not exited.
func (dbp *nativeProcess) Valid() (bool, error) {
if dbp.detached {
return false, proc.ErrProcessDetached
}
if dbp.exited {
return false, proc.ErrProcessExited{Pid: dbp.pid}
}
return true, nil
}
// ThreadList returns a list of threads in the process.
func (dbp *nativeProcess) ThreadList() []proc.Thread {
r := make([]proc.Thread, 0, len(dbp.threads))
for _, v := range dbp.threads {
r = append(r, v)
}
return r
}
// FindThread attempts to find the thread with the specified ID.
func (dbp *nativeProcess) FindThread(threadID int) (proc.Thread, bool) {
th, ok := dbp.threads[threadID]
return th, ok
}
proc/*: remove proc.Thread.Blocked, refactor memory access (#2206) On linux we can not read memory if the thread we use to do it is occupied doing certain system calls. The exact conditions when this happens have never been clear. This problem was worked around by using the Blocked method which recognized the most common circumstances where this would happen. However this is a hack: Blocked returning true doesn't mean that the problem will manifest and Blocked returning false doesn't necessarily mean the problem will not manifest. A side effect of this is issue #2151 where sometimes we can't read the memory of a thread and find its associated goroutine. This commit fixes this problem by always reading memory using a thread we know to be good for this, specifically the one returned by ContinueOnce. In particular the changes are as follows: 1. Remove (ProcessInternal).CurrentThread and (ProcessInternal).SetCurrentThread, the "current thread" becomes a field of Target, CurrentThread becomes a (*Target) method and (*Target).SwitchThread basically just sets a field Target. 2. The backends keep track of their own internal idea of what the current thread is, to use it to read memory, this is the thread they return from ContinueOnce as trapthread 3. The current thread in the backend and the current thread in Target only ever get synchronized in two places: when the backend creates a Target object the currentThread field of Target is initialized with the backend's current thread and when (*Target).Restart gets called (when a recording is rewound the currentThread used by Target might not exist anymore). 4. We remove the MemoryReadWriter interface embedded in Thread and instead add a Memory method to Process that returns a MemoryReadWriter. The backends will return something here that will read memory using the current thread saved by the backend. 5. The Thread.Blocked method is removed One possible problem with this change is processes that have threads with different memory maps. As far as I can determine this could happen on old versions of linux but this option was removed in linux 2.5. Fixes #2151
2020-11-09 19:28:40 +00:00
// Memory returns the process memory.
func (dbp *nativeProcess) Memory() proc.MemoryReadWriter {
return dbp.memthread
}
// Breakpoints returns a list of breakpoints currently set.
func (dbp *nativeProcess) Breakpoints() *proc.BreakpointMap {
return &dbp.breakpoints
}
// RequestManualStop sets the `manualStopRequested` flag and
// sends SIGSTOP to all threads.
func (dbp *nativeProcess) RequestManualStop(cctx *proc.ContinueOnceContext) error {
if dbp.exited {
return proc.ErrProcessExited{Pid: dbp.pid}
}
return dbp.requestManualStop()
}
func (dbp *nativeProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
if bp.WatchType != 0 {
for _, thread := range dbp.threads {
err := thread.writeHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
if err != nil {
return err
}
}
return nil
}
bp.OriginalData = make([]byte, dbp.bi.Arch.BreakpointSize())
_, err := dbp.memthread.ReadMemory(bp.OriginalData, bp.Addr)
if err != nil {
return err
}
return dbp.writeSoftwareBreakpoint(dbp.memthread, bp.Addr)
}
func (dbp *nativeProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
if bp.WatchType != 0 {
for _, thread := range dbp.threads {
err := thread.clearHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
if err != nil {
return err
}
}
return nil
}
return dbp.memthread.clearSoftwareBreakpoint(bp)
}
func continueOnce(procs []proc.ProcessInternal, cctx *proc.ContinueOnceContext) (proc.Thread, proc.StopReason, error) {
if len(procs) != 1 {
panic("not implemented")
}
dbp := procs[0].(*nativeProcess)
if dbp.exited {
return nil, proc.StopExited, proc.ErrProcessExited{Pid: dbp.pid}
}
for {
if err := dbp.resume(); err != nil {
return nil, proc.StopUnknown, err
}
for _, th := range dbp.threads {
th.CurrentBreakpoint.Clear()
}
if cctx.ResumeChan != nil {
close(cctx.ResumeChan)
cctx.ResumeChan = nil
}
trapthread, err := dbp.trapWait(-1)
if err != nil {
return nil, proc.StopUnknown, err
}
trapthread, err = dbp.stop(cctx, trapthread)
if err != nil {
return nil, proc.StopUnknown, err
}
if trapthread != nil {
proc/*: remove proc.Thread.Blocked, refactor memory access (#2206) On linux we can not read memory if the thread we use to do it is occupied doing certain system calls. The exact conditions when this happens have never been clear. This problem was worked around by using the Blocked method which recognized the most common circumstances where this would happen. However this is a hack: Blocked returning true doesn't mean that the problem will manifest and Blocked returning false doesn't necessarily mean the problem will not manifest. A side effect of this is issue #2151 where sometimes we can't read the memory of a thread and find its associated goroutine. This commit fixes this problem by always reading memory using a thread we know to be good for this, specifically the one returned by ContinueOnce. In particular the changes are as follows: 1. Remove (ProcessInternal).CurrentThread and (ProcessInternal).SetCurrentThread, the "current thread" becomes a field of Target, CurrentThread becomes a (*Target) method and (*Target).SwitchThread basically just sets a field Target. 2. The backends keep track of their own internal idea of what the current thread is, to use it to read memory, this is the thread they return from ContinueOnce as trapthread 3. The current thread in the backend and the current thread in Target only ever get synchronized in two places: when the backend creates a Target object the currentThread field of Target is initialized with the backend's current thread and when (*Target).Restart gets called (when a recording is rewound the currentThread used by Target might not exist anymore). 4. We remove the MemoryReadWriter interface embedded in Thread and instead add a Memory method to Process that returns a MemoryReadWriter. The backends will return something here that will read memory using the current thread saved by the backend. 5. The Thread.Blocked method is removed One possible problem with this change is processes that have threads with different memory maps. As far as I can determine this could happen on old versions of linux but this option was removed in linux 2.5. Fixes #2151
2020-11-09 19:28:40 +00:00
dbp.memthread = trapthread
return trapthread, proc.StopUnknown, nil
}
}
}
// FindBreakpoint finds the breakpoint for the given pc.
func (dbp *nativeProcess) FindBreakpoint(pc uint64, adjustPC bool) (*proc.Breakpoint, bool) {
if adjustPC {
// Check to see if address is past the breakpoint, (i.e. breakpoint was hit).
if bp, ok := dbp.breakpoints.M[pc-uint64(dbp.bi.Arch.BreakpointSize())]; ok {
return bp, true
}
}
// Directly use addr to lookup breakpoint.
if bp, ok := dbp.breakpoints.M[pc]; ok {
return bp, true
}
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) {
if err := initialize(dbp); err != nil {
return nil, err
}
if err := dbp.updateThreadList(); err != nil {
return nil, err
}
stopReason := proc.StopLaunched
if !dbp.childProcess {
stopReason = proc.StopAttached
}
tgt, err := proc.NewTarget(dbp, dbp.pid, dbp.memthread, proc.NewTargetConfig{
Path: path,
DebugInfoDirs: debugInfoDirs,
// We disable asyncpreempt for the following reasons:
// - on Windows asyncpreempt is incompatible with debuggers, see:
// https://github.com/golang/go/issues/36494
// - on linux/arm64 asyncpreempt can sometimes restart a sequence of
// instructions, if the sequence happens to contain a breakpoint it will
// look like the breakpoint was hit twice when it was "logically" only
// executed once.
// 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,
})
if err != nil {
return nil, err
}
if dbp.bi.Arch.Name == "arm64" {
dbp.iscgo = tgt.IsCgo()
}
return tgt, nil
}
func (dbp *nativeProcess) 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.
runtime.LockOSThread()
// Leaving the OS thread locked currently leads to segfaults in the
// Go runtime while running on FreeBSD and OpenBSD:
// https://github.com/golang/go/issues/52394
if runtime.GOOS == "freebsd" || runtime.GOOS == "openbsd" {
defer runtime.UnlockOSThread()
}
for fn := range dbp.ptraceChan {
fn()
dbp.ptraceDoneChan <- nil
}
}
func (dbp *nativeProcess) execPtraceFunc(fn func()) {
dbp.ptraceChan <- fn
<-dbp.ptraceDoneChan
}
func (dbp *nativeProcess) postExit() {
dbp.exited = true
close(dbp.ptraceChan)
close(dbp.ptraceDoneChan)
dbp.bi.Close()
if dbp.ctty != nil {
dbp.ctty.Close()
}
dbp.os.Close()
}
func (dbp *nativeProcess) writeSoftwareBreakpoint(thread *nativeThread, addr uint64) error {
_, err := thread.WriteMemory(addr, dbp.bi.Arch.BreakpointInstruction())
return err
}
func openRedirects(redirects [3]string, foreground bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
toclose := []*os.File{}
if redirects[0] != "" {
stdin, err = os.Open(redirects[0])
if err != nil {
return nil, nil, nil, nil, err
}
toclose = append(toclose, stdin)
} else if foreground {
stdin = os.Stdin
}
create := func(path string, dflt *os.File) *os.File {
if path == "" {
return dflt
}
var f *os.File
f, err = os.Create(path)
if f != nil {
toclose = append(toclose, f)
}
return f
}
stdout = create(redirects[1], os.Stdout)
if err != nil {
return nil, nil, nil, nil, err
}
stderr = create(redirects[2], os.Stderr)
if err != nil {
return nil, nil, nil, nil, err
}
closefn = func() {
for _, f := range toclose {
_ = f.Close()
}
}
return stdin, stdout, stderr, closefn, nil
}