
Hardware breakpoints are by definition architecture dependant. Move them off the DebuggedProcess struct and onto the associated arch struct.
535 lines
13 KiB
Go
535 lines
13 KiB
Go
package debugger
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"regexp"
|
|
"runtime"
|
|
|
|
"github.com/derekparker/delve/proctl"
|
|
"github.com/derekparker/delve/service/api"
|
|
)
|
|
|
|
// Debugger provides a thread-safe DebuggedProcess service. Instances of
|
|
// Debugger can be exposed by other services.
|
|
type Debugger struct {
|
|
config *Config
|
|
process *proctl.DebuggedProcess
|
|
processOps chan func(*proctl.DebuggedProcess)
|
|
stop chan stopSignal
|
|
running bool
|
|
}
|
|
|
|
// Config provides the configuration to start a Debugger.
|
|
//
|
|
// Only one of ProcessArgs or AttachPid should be specified. If ProcessArgs is
|
|
// provided, a new process will be launched. Otherwise, the debugger will try
|
|
// to attach to an existing process with AttachPid.
|
|
type Config struct {
|
|
// ProcessArgs are the arguments to launch a new process.
|
|
ProcessArgs []string
|
|
// AttachPid is the PID of an existing process to which the debugger should
|
|
// attach.
|
|
AttachPid int
|
|
}
|
|
|
|
// stopSignal is used to stop the debugger.
|
|
type stopSignal struct {
|
|
// KillProcess indicates whether to kill the debugee following detachment.
|
|
KillProcess bool
|
|
}
|
|
|
|
// New creates a new Debugger.
|
|
func New(config *Config) *Debugger {
|
|
debugger := &Debugger{
|
|
processOps: make(chan func(*proctl.DebuggedProcess)),
|
|
config: config,
|
|
stop: make(chan stopSignal),
|
|
}
|
|
return debugger
|
|
}
|
|
|
|
// withProcess facilitates thread-safe access to the DebuggedProcess. Most
|
|
// interaction with DebuggedProcess should occur via calls to withProcess[1],
|
|
// and the functions placed on the processOps channel should be consumed and
|
|
// executed from the same thread as the DebuggedProcess.
|
|
//
|
|
// This is convenient because it allows things like HTTP handlers in
|
|
// goroutines to work with the DebuggedProcess with synchronous semantics.
|
|
//
|
|
// [1] There are some exceptional cases where direct access is okay; for
|
|
// instance, when performing an operation like halt which merely sends a
|
|
// signal to the process rather than performing something like a ptrace
|
|
// operation.
|
|
func (d *Debugger) withProcess(f func(*proctl.DebuggedProcess) error) error {
|
|
if !d.running {
|
|
return fmt.Errorf("debugger isn't running")
|
|
}
|
|
|
|
result := make(chan error)
|
|
d.processOps <- func(proc *proctl.DebuggedProcess) {
|
|
result <- f(proc)
|
|
}
|
|
return <-result
|
|
}
|
|
|
|
// Run starts debugging a process until Detach is called.
|
|
func (d *Debugger) Run() error {
|
|
// 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()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
d.running = true
|
|
defer func() { d.running = false }()
|
|
|
|
// Create the process by either attaching or launching.
|
|
if d.config.AttachPid > 0 {
|
|
log.Printf("attaching to pid %d", d.config.AttachPid)
|
|
p, err := proctl.Attach(d.config.AttachPid)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't attach to pid %d: %s", d.config.AttachPid, err)
|
|
}
|
|
d.process = p
|
|
} else {
|
|
log.Printf("launching process with args: %v", d.config.ProcessArgs)
|
|
p, err := proctl.Launch(d.config.ProcessArgs)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't launch process: %s", err)
|
|
}
|
|
d.process = p
|
|
}
|
|
|
|
// Handle access to the process from the current thread.
|
|
log.Print("debugger started")
|
|
for {
|
|
select {
|
|
case f := <-d.processOps:
|
|
// Execute the function
|
|
f(d.process)
|
|
case s := <-d.stop:
|
|
// Handle shutdown
|
|
log.Print("debugger is stopping")
|
|
|
|
// Clear breakpoints
|
|
bps := []*proctl.BreakPoint{}
|
|
for _, bp := range d.process.BreakPoints {
|
|
if bp != nil {
|
|
bps = append(bps, bp)
|
|
}
|
|
}
|
|
for _, bp := range d.process.HardwareBreakPoints() {
|
|
if bp != nil {
|
|
bps = append(bps, bp)
|
|
}
|
|
}
|
|
for _, bp := range bps {
|
|
_, err := d.process.Clear(bp.Addr)
|
|
if err != nil {
|
|
log.Printf("warning: couldn't clear breakpoint @ %#v: %s", bp.Addr, err)
|
|
} else {
|
|
log.Printf("cleared breakpoint @ %#v", bp.Addr)
|
|
}
|
|
}
|
|
|
|
// Kill the process if requested
|
|
if s.KillProcess {
|
|
if err := d.process.Detach(); err == nil {
|
|
log.Print("killed process")
|
|
} else {
|
|
log.Printf("couldn't kill process: %s", err)
|
|
}
|
|
} else {
|
|
// Detach
|
|
if !d.process.Exited() {
|
|
if err := proctl.PtraceDetach(d.process.Pid, 0); err == nil {
|
|
log.Print("detached from process")
|
|
} else {
|
|
log.Printf("couldn't detach from process: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Detach stops the debugger.
|
|
func (d *Debugger) Detach(kill bool) error {
|
|
if !d.running {
|
|
return fmt.Errorf("debugger isn't running")
|
|
}
|
|
|
|
d.stop <- stopSignal{KillProcess: kill}
|
|
return nil
|
|
}
|
|
|
|
func (d *Debugger) State() (*api.DebuggerState, error) {
|
|
var state *api.DebuggerState
|
|
|
|
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
var thread *api.Thread
|
|
th := p.CurrentThread
|
|
if th != nil {
|
|
thread = convertThread(th)
|
|
}
|
|
|
|
var breakpoint *api.BreakPoint
|
|
bp := p.CurrentBreakpoint()
|
|
if bp != nil {
|
|
breakpoint = convertBreakPoint(bp)
|
|
}
|
|
|
|
state = &api.DebuggerState{
|
|
BreakPoint: breakpoint,
|
|
CurrentThread: thread,
|
|
Exited: p.Exited(),
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return state, err
|
|
}
|
|
|
|
func (d *Debugger) CreateBreakPoint(requestedBp *api.BreakPoint) (*api.BreakPoint, error) {
|
|
var createdBp *api.BreakPoint
|
|
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
var loc string
|
|
switch {
|
|
case len(requestedBp.File) > 0:
|
|
loc = fmt.Sprintf("%s:%d", requestedBp.File, requestedBp.Line)
|
|
case len(requestedBp.FunctionName) > 0:
|
|
loc = requestedBp.FunctionName
|
|
default:
|
|
return fmt.Errorf("no file or function name specified")
|
|
}
|
|
|
|
bp, breakError := p.BreakByLocation(loc)
|
|
if breakError != nil {
|
|
return breakError
|
|
}
|
|
createdBp = convertBreakPoint(bp)
|
|
log.Printf("created breakpoint: %#v", createdBp)
|
|
return nil
|
|
})
|
|
return createdBp, err
|
|
}
|
|
|
|
func (d *Debugger) ClearBreakPoint(requestedBp *api.BreakPoint) (*api.BreakPoint, error) {
|
|
var clearedBp *api.BreakPoint
|
|
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
bp, err := p.Clear(requestedBp.Addr)
|
|
if err != nil {
|
|
return fmt.Errorf("Can't clear breakpoint @%x: %s", requestedBp.Addr, err)
|
|
}
|
|
clearedBp = convertBreakPoint(bp)
|
|
log.Printf("cleared breakpoint: %#v", clearedBp)
|
|
return nil
|
|
})
|
|
return clearedBp, err
|
|
}
|
|
|
|
func (d *Debugger) BreakPoints() []*api.BreakPoint {
|
|
bps := []*api.BreakPoint{}
|
|
d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
for _, bp := range p.HardwareBreakPoints() {
|
|
if bp == nil {
|
|
continue
|
|
}
|
|
bps = append(bps, convertBreakPoint(bp))
|
|
}
|
|
|
|
for _, bp := range p.BreakPoints {
|
|
if bp.Temp {
|
|
continue
|
|
}
|
|
bps = append(bps, convertBreakPoint(bp))
|
|
}
|
|
return nil
|
|
})
|
|
return bps
|
|
}
|
|
|
|
func (d *Debugger) FindBreakPoint(id int) *api.BreakPoint {
|
|
for _, bp := range d.BreakPoints() {
|
|
if bp.ID == id {
|
|
return bp
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Debugger) Threads() []*api.Thread {
|
|
threads := []*api.Thread{}
|
|
d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
for _, th := range p.Threads {
|
|
threads = append(threads, convertThread(th))
|
|
}
|
|
return nil
|
|
})
|
|
return threads
|
|
}
|
|
|
|
func (d *Debugger) FindThread(id int) *api.Thread {
|
|
for _, thread := range d.Threads() {
|
|
if thread.ID == id {
|
|
return thread
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Command handles commands which control the debugger lifecycle. Like other
|
|
// debugger operations, these are executed one at a time as part of the
|
|
// process operation pipeline.
|
|
//
|
|
// The one exception is the Halt command, which can be executed concurrently
|
|
// with any operation.
|
|
func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, error) {
|
|
var err error
|
|
switch command.Name {
|
|
case api.Continue:
|
|
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
log.Print("continuing")
|
|
e := p.Continue()
|
|
return e
|
|
})
|
|
case api.Next:
|
|
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
log.Print("nexting")
|
|
return p.Next()
|
|
})
|
|
case api.Step:
|
|
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
log.Print("stepping")
|
|
return p.Step()
|
|
})
|
|
case api.SwitchThread:
|
|
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
log.Printf("switching to thread %d", command.ThreadID)
|
|
return p.SwitchThread(command.ThreadID)
|
|
})
|
|
case api.Halt:
|
|
// RequestManualStop does not invoke any ptrace syscalls, so it's safe to
|
|
// access the process directly.
|
|
log.Print("halting")
|
|
err = d.process.RequestManualStop()
|
|
}
|
|
if err != nil {
|
|
// Only report the error if it's not a process exit.
|
|
if _, exited := err.(proctl.ProcessExitedError); !exited {
|
|
return nil, err
|
|
}
|
|
}
|
|
return d.State()
|
|
}
|
|
|
|
func (d *Debugger) Sources(filter string) ([]string, error) {
|
|
regex, err := regexp.Compile(filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
|
|
}
|
|
|
|
files := []string{}
|
|
d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
for f := range p.Sources() {
|
|
if regex.Match([]byte(f)) {
|
|
files = append(files, f)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return files, nil
|
|
}
|
|
|
|
func (d *Debugger) Functions(filter string) ([]string, error) {
|
|
regex, err := regexp.Compile(filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
|
|
}
|
|
|
|
funcs := []string{}
|
|
d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
for _, f := range p.Funcs() {
|
|
if f.Sym != nil && regex.Match([]byte(f.Name)) {
|
|
funcs = append(funcs, f.Name)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return funcs, nil
|
|
}
|
|
|
|
func (d *Debugger) PackageVariables(threadID int, filter string) ([]api.Variable, error) {
|
|
regex, err := regexp.Compile(filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
|
|
}
|
|
|
|
vars := []api.Variable{}
|
|
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
thread, found := p.Threads[threadID]
|
|
if !found {
|
|
return fmt.Errorf("couldn't find thread %d", threadID)
|
|
}
|
|
pv, err := thread.PackageVariables()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, v := range pv {
|
|
if regex.Match([]byte(v.Name)) {
|
|
vars = append(vars, convertVar(v))
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return vars, err
|
|
}
|
|
|
|
func (d *Debugger) LocalVariables(threadID int) ([]api.Variable, error) {
|
|
vars := []api.Variable{}
|
|
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
thread, found := p.Threads[threadID]
|
|
if !found {
|
|
return fmt.Errorf("couldn't find thread %d", threadID)
|
|
}
|
|
pv, err := thread.LocalVariables()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, v := range pv {
|
|
vars = append(vars, convertVar(v))
|
|
}
|
|
return nil
|
|
})
|
|
return vars, err
|
|
}
|
|
|
|
func (d *Debugger) FunctionArguments(threadID int) ([]api.Variable, error) {
|
|
vars := []api.Variable{}
|
|
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
thread, found := p.Threads[threadID]
|
|
if !found {
|
|
return fmt.Errorf("couldn't find thread %d", threadID)
|
|
}
|
|
pv, err := thread.FunctionArguments()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, v := range pv {
|
|
vars = append(vars, convertVar(v))
|
|
}
|
|
return nil
|
|
})
|
|
return vars, err
|
|
}
|
|
|
|
func (d *Debugger) EvalVariableInThread(threadID int, symbol string) (*api.Variable, error) {
|
|
var variable *api.Variable
|
|
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
thread, found := p.Threads[threadID]
|
|
if !found {
|
|
return fmt.Errorf("couldn't find thread %d", threadID)
|
|
}
|
|
v, err := thread.EvalVariable(symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
converted := convertVar(v)
|
|
variable = &converted
|
|
return nil
|
|
})
|
|
return variable, err
|
|
}
|
|
|
|
func (d *Debugger) Goroutines() ([]*api.Goroutine, error) {
|
|
goroutines := []*api.Goroutine{}
|
|
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
|
|
gs, err := p.GoroutinesInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, g := range gs {
|
|
goroutines = append(goroutines, convertGoroutine(g))
|
|
}
|
|
return nil
|
|
})
|
|
return goroutines, err
|
|
}
|
|
|
|
// convertBreakPoint converts an internal breakpoint to an API BreakPoint.
|
|
func convertBreakPoint(bp *proctl.BreakPoint) *api.BreakPoint {
|
|
return &api.BreakPoint{
|
|
ID: bp.ID,
|
|
FunctionName: bp.FunctionName,
|
|
File: bp.File,
|
|
Line: bp.Line,
|
|
Addr: bp.Addr,
|
|
}
|
|
}
|
|
|
|
// convertThread converts an internal thread to an API Thread.
|
|
func convertThread(th *proctl.ThreadContext) *api.Thread {
|
|
var (
|
|
function *api.Function
|
|
file string
|
|
line int
|
|
pc uint64
|
|
)
|
|
|
|
loc, err := th.Location()
|
|
if err == nil {
|
|
pc = loc.PC
|
|
file = loc.File
|
|
line = loc.Line
|
|
if loc.Fn != nil {
|
|
function = &api.Function{
|
|
Name: loc.Fn.Name,
|
|
Type: loc.Fn.Type,
|
|
Value: loc.Fn.Value,
|
|
GoType: loc.Fn.GoType,
|
|
}
|
|
}
|
|
}
|
|
|
|
return &api.Thread{
|
|
ID: th.Id,
|
|
PC: pc,
|
|
File: file,
|
|
Line: line,
|
|
Function: function,
|
|
}
|
|
}
|
|
|
|
// convertVar converts an internal variable to an API Variable.
|
|
func convertVar(v *proctl.Variable) api.Variable {
|
|
return api.Variable{
|
|
Name: v.Name,
|
|
Value: v.Value,
|
|
Type: v.Type,
|
|
}
|
|
}
|
|
|
|
// convertGoroutine converts an internal Goroutine to an API Goroutine.
|
|
func convertGoroutine(g *proctl.G) *api.Goroutine {
|
|
var function *api.Function
|
|
if g.Func != nil {
|
|
function = &api.Function{
|
|
Name: g.Func.Name,
|
|
Type: g.Func.Type,
|
|
Value: g.Func.Value,
|
|
GoType: g.Func.GoType,
|
|
}
|
|
}
|
|
|
|
return &api.Goroutine{
|
|
ID: g.Id,
|
|
PC: g.PC,
|
|
File: g.File,
|
|
Line: g.Line,
|
|
Function: function,
|
|
}
|
|
}
|