
We do not need to verify a current breakpoint, nor do a redundant check on whether we have been asked to manually halt. Assume trapWait has done its due diligence and stop the world once it returns.
690 lines
16 KiB
Go
690 lines
16 KiB
Go
package proc
|
|
|
|
import (
|
|
"debug/dwarf"
|
|
"debug/gosym"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
sys "golang.org/x/sys/unix"
|
|
|
|
"github.com/derekparker/delve/dwarf/frame"
|
|
"github.com/derekparker/delve/dwarf/line"
|
|
"github.com/derekparker/delve/dwarf/reader"
|
|
"github.com/derekparker/delve/source"
|
|
)
|
|
|
|
func init() {
|
|
runtime.GOMAXPROCS(2)
|
|
}
|
|
|
|
// Process represents all of the information the debugger
|
|
// is holding onto regarding the process we are debugging.
|
|
type Process struct {
|
|
Pid int // Process Pid
|
|
Process *os.Process // Pointer to process struct for the actual process we are debugging
|
|
|
|
// Breakpoint table, hold information on software / hardware breakpoints.
|
|
// Maps instruction address to Breakpoint struct.
|
|
Breakpoints map[uint64]*Breakpoint
|
|
|
|
// List of threads mapped as such: pid -> *Thread
|
|
Threads map[int]*Thread
|
|
|
|
// Active thread. This is the default thread used for setting breakpoints, evaluating variables, etc..
|
|
CurrentThread *Thread
|
|
|
|
dwarf *dwarf.Data
|
|
goSymTable *gosym.Table
|
|
frameEntries frame.FrameDescriptionEntries
|
|
lineInfo *line.DebugLineInfo
|
|
firstStart bool
|
|
singleStepping bool
|
|
os *OSProcessDetails
|
|
arch Arch
|
|
ast *source.Searcher
|
|
breakpointIDCounter int
|
|
tempBreakpointIDCounter int
|
|
halt bool
|
|
exited bool
|
|
ptraceChan chan func()
|
|
ptraceDoneChan chan interface{}
|
|
}
|
|
|
|
func New(pid int) *Process {
|
|
dbp := &Process{
|
|
Pid: pid,
|
|
Threads: make(map[int]*Thread),
|
|
Breakpoints: make(map[uint64]*Breakpoint),
|
|
firstStart: true,
|
|
os: new(OSProcessDetails),
|
|
ast: source.New(),
|
|
ptraceChan: make(chan func()),
|
|
ptraceDoneChan: make(chan interface{}),
|
|
}
|
|
go dbp.handlePtraceFuncs()
|
|
return dbp
|
|
}
|
|
|
|
// ProcessExitedError indicates that the process has exited and contains both
|
|
// process id and exit status.
|
|
type ProcessExitedError struct {
|
|
Pid int
|
|
Status int
|
|
}
|
|
|
|
func (pe ProcessExitedError) Error() string {
|
|
return fmt.Sprintf("Process %d has exited with status %d", pe.Pid, pe.Status)
|
|
}
|
|
|
|
// Attach to an existing process with the given PID.
|
|
func Attach(pid int) (*Process, error) {
|
|
dbp, err := initializeDebugProcess(New(pid), "", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dbp, nil
|
|
}
|
|
|
|
func (dbp *Process) Detach(kill bool) (err error) {
|
|
// Clean up any breakpoints we've set.
|
|
for _, bp := range dbp.Breakpoints {
|
|
if bp != nil {
|
|
_, err := dbp.ClearBreakpoint(bp.Addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
dbp.execPtraceFunc(func() {
|
|
var sig int
|
|
if kill {
|
|
sig = int(sys.SIGINT)
|
|
}
|
|
err = PtraceDetach(dbp.Pid, sig)
|
|
})
|
|
return
|
|
}
|
|
|
|
// Returns whether or not Delve thinks the debugged
|
|
// process has exited.
|
|
func (dbp *Process) Exited() bool {
|
|
return dbp.exited
|
|
}
|
|
|
|
// Returns whether or not Delve thinks the debugged
|
|
// process is currently executing.
|
|
func (dbp *Process) Running() bool {
|
|
for _, th := range dbp.Threads {
|
|
if th.running {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Finds the executable and then uses it
|
|
// to parse the following information:
|
|
// * Dwarf .debug_frame section
|
|
// * Dwarf .debug_line section
|
|
// * Go symbol table.
|
|
func (dbp *Process) LoadInformation(path string) error {
|
|
var wg sync.WaitGroup
|
|
|
|
exe, err := dbp.findExecutable(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wg.Add(3)
|
|
go dbp.parseDebugFrame(exe, &wg)
|
|
go dbp.obtainGoSymbols(exe, &wg)
|
|
go dbp.parseDebugLineInfo(exe, &wg)
|
|
wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Find a location by string (file+line, function, breakpoint id, addr)
|
|
func (dbp *Process) FindLocation(str string) (uint64, error) {
|
|
// File + Line
|
|
if strings.ContainsRune(str, ':') {
|
|
fl := strings.Split(str, ":")
|
|
|
|
fileName, err := filepath.Abs(fl[0])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
line, err := strconv.Atoi(fl[1])
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
pc, _, err := dbp.goSymTable.LineToPC(fileName, line)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return pc, nil
|
|
}
|
|
|
|
// Try to lookup by function name
|
|
fn := dbp.goSymTable.LookupFunc(str)
|
|
if fn != nil {
|
|
return fn.Entry, nil
|
|
}
|
|
|
|
// Attempt to parse as number for breakpoint id or raw address
|
|
id, err := strconv.ParseUint(str, 0, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("unable to find location for %s", str)
|
|
}
|
|
if bp, ok := dbp.FindBreakpointByID(int(id)); ok {
|
|
return bp.Addr, nil
|
|
}
|
|
|
|
// Last resort, use as raw address
|
|
return id, nil
|
|
}
|
|
|
|
// Sends out a request that the debugged process halt
|
|
// execution. Sends SIGSTOP to all threads.
|
|
func (dbp *Process) RequestManualStop() error {
|
|
dbp.halt = true
|
|
return dbp.requestManualStop()
|
|
}
|
|
|
|
// Sets a breakpoint at addr, and stores it in the process wide
|
|
// break point table. Setting a break point must be thread specific due to
|
|
// ptrace actions needing the thread to be in a signal-delivery-stop.
|
|
//
|
|
// Depending on hardware support, Delve will choose to either
|
|
// set a hardware or software breakpoint. Essentially, if the
|
|
// hardware supports it, and there are free debug registers, Delve
|
|
// will set a hardware breakpoint. Otherwise we fall back to software
|
|
// breakpoints, which are a bit more work for us.
|
|
func (dbp *Process) SetBreakpoint(addr uint64) (*Breakpoint, error) {
|
|
return dbp.setBreakpoint(dbp.CurrentThread.Id, addr, false)
|
|
}
|
|
|
|
// Sets a temp breakpoint, for the 'next' command.
|
|
func (dbp *Process) SetTempBreakpoint(addr uint64) (*Breakpoint, error) {
|
|
return dbp.setBreakpoint(dbp.CurrentThread.Id, addr, true)
|
|
}
|
|
|
|
// Sets a breakpoint by location string (function, file+line, address)
|
|
func (dbp *Process) SetBreakpointByLocation(loc string) (*Breakpoint, error) {
|
|
addr, err := dbp.FindLocation(loc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dbp.SetBreakpoint(addr)
|
|
}
|
|
|
|
// Clears a breakpoint.
|
|
//
|
|
// If it is a hardware assisted breakpoint, iterate through all threads
|
|
// and clear the debug register. Otherwise, restore original instruction.
|
|
func (dbp *Process) ClearBreakpoint(addr uint64) (*Breakpoint, error) {
|
|
bp, ok := dbp.Breakpoints[addr]
|
|
if !ok {
|
|
return nil, NoBreakpointError{addr: addr}
|
|
}
|
|
|
|
for _, thread := range dbp.Threads {
|
|
if _, err := bp.Clear(thread); err != nil {
|
|
return nil, err
|
|
}
|
|
if !bp.hardware {
|
|
break
|
|
}
|
|
}
|
|
|
|
if bp.hardware {
|
|
dbp.arch.SetHardwareBreakpointUsage(bp.reg, false)
|
|
}
|
|
delete(dbp.Breakpoints, addr)
|
|
|
|
return bp, nil
|
|
}
|
|
|
|
// Clears a breakpoint by location (function, file+line, address, breakpoint id)
|
|
func (dbp *Process) ClearBreakpointByLocation(loc string) (*Breakpoint, error) {
|
|
addr, err := dbp.FindLocation(loc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return dbp.ClearBreakpoint(addr)
|
|
}
|
|
|
|
// Returns the status of the current main thread context.
|
|
func (dbp *Process) Status() *sys.WaitStatus {
|
|
return dbp.CurrentThread.Status
|
|
}
|
|
|
|
// Step over function calls.
|
|
func (dbp *Process) Next() error {
|
|
return dbp.run(dbp.next)
|
|
}
|
|
|
|
func (dbp *Process) next() error {
|
|
// Make sure we clean up the temp breakpoints created by thread.Next
|
|
defer dbp.clearTempBreakpoints()
|
|
|
|
// Set breakpoints for any goroutine that is currently
|
|
// blocked trying to read from a channel. This is so that
|
|
// if control flow switches to that goroutine, we end up
|
|
// somewhere useful instead of in runtime code.
|
|
chanRecvCount, err := dbp.setChanRecvBreakpoints()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g, err := dbp.CurrentThread.GetG()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if g.DeferPC != 0 {
|
|
_, err = dbp.SetTempBreakpoint(g.DeferPC)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var goroutineExiting bool
|
|
var waitCount int
|
|
for _, th := range dbp.Threads {
|
|
if th.blocked() {
|
|
// Ignore threads that aren't running go code.
|
|
continue
|
|
}
|
|
waitCount++
|
|
if err = th.SetNextBreakpoints(); err != nil {
|
|
if err, ok := err.(GoroutineExitingError); ok {
|
|
waitCount = waitCount - 1 + chanRecvCount
|
|
if err.goid == g.Id {
|
|
goroutineExiting = true
|
|
}
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
for _, th := range dbp.Threads {
|
|
if err = th.Continue(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for waitCount > 0 {
|
|
thread, err := dbp.trapWait(-1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tg, err := thread.GetG()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Make sure we're on the same goroutine, unless it has exited.
|
|
if tg.Id == g.Id || goroutineExiting {
|
|
if dbp.CurrentThread != thread {
|
|
dbp.SwitchThread(thread.Id)
|
|
}
|
|
}
|
|
waitCount--
|
|
}
|
|
return dbp.Halt()
|
|
}
|
|
|
|
func (dbp *Process) setChanRecvBreakpoints() (int, error) {
|
|
var count int
|
|
allg, err := dbp.GoroutinesInfo()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for _, g := range allg {
|
|
if g.ChanRecvBlocked() {
|
|
ret, err := g.chanRecvReturnAddr(dbp)
|
|
if err != nil {
|
|
if _, ok := err.(NullAddrError); ok {
|
|
continue
|
|
}
|
|
return 0, err
|
|
}
|
|
if _, err = dbp.SetTempBreakpoint(ret); err != nil {
|
|
return 0, err
|
|
}
|
|
count++
|
|
}
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// Resume process.
|
|
func (dbp *Process) Continue() error {
|
|
for _, thread := range dbp.Threads {
|
|
err := thread.Continue()
|
|
if err != nil {
|
|
return fmt.Errorf("could not continue thread %d %s", thread.Id, err)
|
|
}
|
|
}
|
|
return dbp.run(dbp.resume)
|
|
}
|
|
|
|
func (dbp *Process) resume() error {
|
|
thread, err := dbp.trapWait(-1)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := dbp.Halt(); err != nil {
|
|
return err
|
|
}
|
|
if dbp.CurrentThread != thread {
|
|
dbp.SwitchThread(thread.Id)
|
|
}
|
|
pc, err := thread.PC()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dbp.CurrentBreakpoint() != nil || dbp.halt {
|
|
return dbp.Halt()
|
|
}
|
|
// Check to see if we hit a runtime.breakpoint
|
|
fn := dbp.goSymTable.PCToFunc(pc)
|
|
if fn != nil && fn.Name == "runtime.breakpoint" {
|
|
// step twice to get back to user code
|
|
for i := 0; i < 2; i++ {
|
|
if err = thread.Step(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Single step, will execute a single instruction.
|
|
func (dbp *Process) Step() (err error) {
|
|
fn := func() error {
|
|
dbp.singleStepping = true
|
|
defer func() { dbp.singleStepping = false }()
|
|
for _, th := range dbp.Threads {
|
|
if th.blocked() {
|
|
continue
|
|
}
|
|
err := th.Step()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return dbp.run(fn)
|
|
}
|
|
|
|
// Change from current thread to the thread specified by `tid`.
|
|
func (dbp *Process) SwitchThread(tid int) error {
|
|
if th, ok := dbp.Threads[tid]; ok {
|
|
fmt.Printf("thread context changed from %d to %d\n", dbp.CurrentThread.Id, tid)
|
|
dbp.CurrentThread = th
|
|
return nil
|
|
}
|
|
return fmt.Errorf("thread %d does not exist", tid)
|
|
}
|
|
|
|
// Returns an array of G structures representing the information
|
|
// Delve cares about from the internal runtime G structure.
|
|
func (dbp *Process) GoroutinesInfo() ([]*G, error) {
|
|
var (
|
|
threadg = map[int]*Thread{}
|
|
allg []*G
|
|
rdr = dbp.DwarfReader()
|
|
)
|
|
|
|
for i := range dbp.Threads {
|
|
if dbp.Threads[i].blocked() {
|
|
continue
|
|
}
|
|
g, _ := dbp.Threads[i].GetG()
|
|
if g != nil {
|
|
threadg[g.Id] = dbp.Threads[i]
|
|
}
|
|
}
|
|
|
|
addr, err := rdr.AddrFor("runtime.allglen")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allglenBytes, err := dbp.CurrentThread.readMemory(uintptr(addr), 8)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allglen := binary.LittleEndian.Uint64(allglenBytes)
|
|
|
|
rdr.Seek(0)
|
|
allgentryaddr, err := rdr.AddrFor("runtime.allg")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
faddr, err := dbp.CurrentThread.readMemory(uintptr(allgentryaddr), dbp.arch.PtrSize())
|
|
allgptr := binary.LittleEndian.Uint64(faddr)
|
|
|
|
for i := uint64(0); i < allglen; i++ {
|
|
g, err := parseG(dbp.CurrentThread, allgptr+(i*uint64(dbp.arch.PtrSize())), true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if thread, allocated := threadg[g.Id]; allocated {
|
|
loc, err := thread.Location()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g.thread = thread
|
|
// Prefer actual thread location information.
|
|
g.File = loc.File
|
|
g.Line = loc.Line
|
|
g.Func = loc.Fn
|
|
}
|
|
allg = append(allg, g)
|
|
}
|
|
return allg, nil
|
|
}
|
|
|
|
// Stop all threads.
|
|
func (dbp *Process) Halt() (err error) {
|
|
for _, th := range dbp.Threads {
|
|
if err := th.Halt(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Obtains register values from what Delve considers to be the current
|
|
// thread of the traced process.
|
|
func (dbp *Process) Registers() (Registers, error) {
|
|
return dbp.CurrentThread.Registers()
|
|
}
|
|
|
|
// Returns the PC of the current thread.
|
|
func (dbp *Process) PC() (uint64, error) {
|
|
return dbp.CurrentThread.PC()
|
|
}
|
|
|
|
// Returns the PC of the current thread.
|
|
func (dbp *Process) CurrentBreakpoint() *Breakpoint {
|
|
return dbp.CurrentThread.CurrentBreakpoint
|
|
}
|
|
|
|
// Returns the value of the named symbol.
|
|
func (dbp *Process) EvalVariable(name string) (*Variable, error) {
|
|
return dbp.CurrentThread.EvalVariable(name)
|
|
}
|
|
|
|
// Returns a reader for the dwarf data
|
|
func (dbp *Process) DwarfReader() *reader.Reader {
|
|
return reader.New(dbp.dwarf)
|
|
}
|
|
|
|
// Returns list of source files that comprise the debugged binary.
|
|
func (dbp *Process) Sources() map[string]*gosym.Obj {
|
|
return dbp.goSymTable.Files
|
|
}
|
|
|
|
// Returns list of functions present in the debugged program.
|
|
func (dbp *Process) Funcs() []gosym.Func {
|
|
return dbp.goSymTable.Funcs
|
|
}
|
|
|
|
// Converts an instruction address to a file/line/function.
|
|
func (dbp *Process) PCToLine(pc uint64) (string, int, *gosym.Func) {
|
|
return dbp.goSymTable.PCToLine(pc)
|
|
}
|
|
|
|
// Finds the breakpoint for the given ID.
|
|
func (dbp *Process) FindBreakpointByID(id int) (*Breakpoint, bool) {
|
|
for _, bp := range dbp.Breakpoints {
|
|
if bp.ID == id {
|
|
return bp, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// Finds the breakpoint for the given pc.
|
|
func (dbp *Process) FindBreakpoint(pc uint64) (*Breakpoint, bool) {
|
|
// Check for software breakpoint. PC will be at
|
|
// breakpoint instruction + size of breakpoint.
|
|
if bp, ok := dbp.Breakpoints[pc-uint64(dbp.arch.BreakpointSize())]; ok {
|
|
return bp, true
|
|
}
|
|
// Check for hardware breakpoint. PC will equal
|
|
// the breakpoint address since the CPU will stop
|
|
// the process without executing the instruction at
|
|
// this address.
|
|
if bp, ok := dbp.Breakpoints[pc]; ok {
|
|
return bp, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// Returns a new Process struct.
|
|
func initializeDebugProcess(dbp *Process, path string, attach bool) (*Process, error) {
|
|
if attach {
|
|
var err error
|
|
dbp.execPtraceFunc(func() { err = sys.PtraceAttach(dbp.Pid) })
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, _, err = wait(dbp.Pid, dbp.Pid, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
proc, err := os.FindProcess(dbp.Pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dbp.Process = proc
|
|
err = dbp.LoadInformation(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := dbp.updateThreadList(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch runtime.GOARCH {
|
|
case "amd64":
|
|
dbp.arch = AMD64Arch()
|
|
}
|
|
|
|
return dbp, nil
|
|
}
|
|
|
|
func (dbp *Process) clearTempBreakpoints() error {
|
|
for _, bp := range dbp.Breakpoints {
|
|
if !bp.Temp {
|
|
continue
|
|
}
|
|
if _, err := dbp.ClearBreakpoint(bp.Addr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dbp *Process) handleBreakpointOnThread(id int) (*Thread, error) {
|
|
thread, ok := dbp.Threads[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("could not find thread for %d", id)
|
|
}
|
|
pc, err := thread.PC()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Check to see if we have hit a breakpoint.
|
|
if bp, ok := dbp.FindBreakpoint(pc); ok {
|
|
thread.CurrentBreakpoint = bp
|
|
if err = thread.SetPC(bp.Addr); err != nil {
|
|
return nil, err
|
|
}
|
|
return thread, nil
|
|
}
|
|
if dbp.halt {
|
|
return thread, nil
|
|
}
|
|
fn := dbp.goSymTable.PCToFunc(pc)
|
|
if fn != nil && fn.Name == "runtime.breakpoint" {
|
|
thread.singleStepping = true
|
|
defer func() { thread.singleStepping = false }()
|
|
for i := 0; i < 2; i++ {
|
|
if err := thread.Step(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return thread, nil
|
|
}
|
|
return nil, NoBreakpointError{addr: pc}
|
|
}
|
|
|
|
func (dbp *Process) run(fn func() error) error {
|
|
if dbp.exited {
|
|
return fmt.Errorf("process has already exited")
|
|
}
|
|
for _, th := range dbp.Threads {
|
|
th.CurrentBreakpoint = nil
|
|
}
|
|
if err := fn(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (dbp *Process) handlePtraceFuncs() {
|
|
// We must ensure here that we are running on the same thread during
|
|
// the execution of dbg. 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 *Process) execPtraceFunc(fn func()) {
|
|
dbp.ptraceChan <- fn
|
|
<-dbp.ptraceDoneChan
|
|
}
|