delve/pkg/proc/native/proc.go

397 lines
10 KiB
Go
Raw Normal View History

package native
import (
"os"
"runtime"
"sync"
"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
resumeChan chan<- struct{}
ptraceChan chan func()
ptraceDoneChan chan interface{}
childProcess bool // this process was launched, not attached to
stopMu sync.Mutex // protects manualStopRequested
// manualStopRequested is set if all the threads in the process were
// signalled to stop as a result of a Halt API call. Used to disambiguate
// why a thread is found to have stopped.
manualStopRequested bool
// Controlling terminal file descriptor for
// this process.
ctty *os.File
iscgo bool
exited, detached bool
}
var _ proc.ProcessInternal = &nativeProcess{}
// 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
}
// Recorded always returns false for the native proc backend.
func (dbp *nativeProcess) Recorded() (bool, string) { return false, "" }
// Restart will always return an error in the native proc backend, only for
// recorded traces.
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
func (dbp *nativeProcess) Restart(string) (proc.Thread, error) { return nil, proc.ErrNotRecorded }
// ChangeDirection will always return an error in the native proc backend, only for
// recorded traces.
func (dbp *nativeProcess) ChangeDirection(dir proc.Direction) error {
if dir != proc.Forward {
return proc.ErrNotRecorded
}
return nil
}
// GetDirection will always return Forward.
func (p *nativeProcess) GetDirection() proc.Direction { return proc.Forward }
// When will always return an empty string and nil, not supported on native proc backend.
func (dbp *nativeProcess) When() (string, error) { return "", nil }
// Checkpoint will always return an error on the native proc backend,
// only supported for recorded traces.
func (dbp *nativeProcess) Checkpoint(string) (int, error) { return -1, proc.ErrNotRecorded }
// Checkpoints will always return an error on the native proc backend,
// only supported for recorded traces.
func (dbp *nativeProcess) Checkpoints() ([]proc.Checkpoint, error) { return nil, proc.ErrNotRecorded }
// ClearCheckpoint will always return an error on the native proc backend,
// only supported in recorded traces.
func (dbp *nativeProcess) ClearCheckpoint(int) error { return proc.ErrNotRecorded }
// 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
}
// ResumeNotify specifies a channel that will be closed the next time
// ContinueOnce finishes resuming the target.
func (dbp *nativeProcess) ResumeNotify(ch chan<- struct{}) {
dbp.resumeChan = ch
}
// Pid returns the process ID.
func (dbp *nativeProcess) Pid() int {
return dbp.pid
}
// 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() error {
if dbp.exited {
return proc.ErrProcessExited{Pid: dbp.Pid()}
}
dbp.stopMu.Lock()
defer dbp.stopMu.Unlock()
dbp.manualStopRequested = true
return dbp.requestManualStop()
}
// CheckAndClearManualStopRequest checks if a manual stop has
// been requested, and then clears that state.
func (dbp *nativeProcess) CheckAndClearManualStopRequest() bool {
dbp.stopMu.Lock()
defer dbp.stopMu.Unlock()
msr := dbp.manualStopRequested
dbp.manualStopRequested = false
return msr
}
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)
}
// ContinueOnce will continue the target until it stops.
// This could be the result of a breakpoint or signal.
func (dbp *nativeProcess) ContinueOnce() (proc.Thread, proc.StopReason, error) {
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 dbp.resumeChan != nil {
close(dbp.resumeChan)
dbp.resumeChan = nil
}
trapthread, err := dbp.trapWait(-1)
if err != nil {
return nil, proc.StopUnknown, err
}
trapthread, err = dbp.stop(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.memthread, proc.NewTargetConfig{
Path: path,
DebugInfoDirs: debugInfoDirs,
DisableAsyncPreempt: runtime.GOOS == "windows" || runtime.GOOS == "freebsd",
StopReason: stopReason,
CanDump: runtime.GOOS == "linux"})
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()
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
}