
Previously breakpoints with hitcount conditions that became unsatisfiable would become disabled, this was done as an optimization so that the continue loop would no longer need to stop on them and evaluate their conditions. As a side effect this meant that on restart these breakpoints would remain disabled, even though their hit condition returned satisfiable. This commit changes Delve behavior so that breakpoints with unsatisifiable hitcount conditions are no longer disabled but the associated physical breakpoints are removed anyway, preserving the optimization. Some refactoring is done to the way conditions are represented and the enable status is managed so that in the future it will be possible to use hitcount conditions to implement "chained" breakpoints (also known as dependet breakpoints), i.e. breakpoints that become active only after a second breakpoint has been hit.
555 lines
14 KiB
Go
555 lines
14 KiB
Go
package proc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/parser"
|
|
"go/token"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/go-delve/delve/pkg/logflags"
|
|
)
|
|
|
|
// TargetGroup represents a group of target processes being debugged that
|
|
// will be resumed and stopped simultaneously.
|
|
// New targets are automatically added to the group if exec catching is
|
|
// enabled and the backend supports it, otherwise the group will always
|
|
// contain a single target process.
|
|
type TargetGroup struct {
|
|
procgrp ProcessGroup
|
|
|
|
targets []*Target
|
|
Selected *Target
|
|
followExecEnabled bool
|
|
followExecRegex *regexp.Regexp
|
|
|
|
RecordingManipulation
|
|
recman RecordingManipulationInternal
|
|
|
|
// StopReason describes the reason why the selected target process is stopped.
|
|
// A process could be stopped for multiple simultaneous reasons, in which
|
|
// case only one will be reported.
|
|
StopReason StopReason
|
|
|
|
// KeepSteppingBreakpoints determines whether certain stop reasons (e.g. manual halts)
|
|
// will keep the stepping breakpoints instead of clearing them.
|
|
KeepSteppingBreakpoints KeepSteppingBreakpoints
|
|
|
|
LogicalBreakpoints map[int]*LogicalBreakpoint
|
|
|
|
cctx *ContinueOnceContext
|
|
cfg NewTargetGroupConfig
|
|
CanDump bool
|
|
}
|
|
|
|
// NewTargetGroupConfig contains the configuration for a new TargetGroup object,
|
|
type NewTargetGroupConfig struct {
|
|
DebugInfoDirs []string // Directories to search for split debug info
|
|
DisableAsyncPreempt bool // Go 1.14 asynchronous preemption should be disabled
|
|
StopReason StopReason // Initial stop reason
|
|
CanDump bool // Can create core dumps (must implement ProcessInternal.MemoryMap)
|
|
}
|
|
|
|
type AddTargetFunc func(ProcessInternal, int, Thread, string, StopReason, string) (*Target, error)
|
|
|
|
// NewGroup creates a TargetGroup containing the specified Target.
|
|
func NewGroup(procgrp ProcessGroup, cfg NewTargetGroupConfig) (*TargetGroup, AddTargetFunc) {
|
|
grp := &TargetGroup{
|
|
procgrp: procgrp,
|
|
cctx: &ContinueOnceContext{},
|
|
LogicalBreakpoints: make(map[int]*LogicalBreakpoint),
|
|
StopReason: cfg.StopReason,
|
|
cfg: cfg,
|
|
CanDump: cfg.CanDump,
|
|
}
|
|
return grp, grp.addTarget
|
|
}
|
|
|
|
// Restart copies breakpoints and follow exec status from oldgrp into grp.
|
|
// Breakpoints that can not be set will be discarded, if discard is not nil
|
|
// it will be called for each discarded breakpoint.
|
|
func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) {
|
|
for _, bp := range oldgrp.LogicalBreakpoints {
|
|
if _, ok := grp.LogicalBreakpoints[bp.LogicalID]; ok {
|
|
continue
|
|
}
|
|
grp.LogicalBreakpoints[bp.LogicalID] = bp
|
|
bp.TotalHitCount = 0
|
|
bp.HitCount = make(map[int64]uint64)
|
|
bp.Set.PidAddrs = nil // breakpoints set through a list of addresses can not be restored after a restart
|
|
if bp.enabled {
|
|
bp.condSatisfiable = breakpointConditionSatisfiable(bp)
|
|
err := grp.enableBreakpoint(bp)
|
|
if err != nil {
|
|
if discard != nil {
|
|
discard(bp, err)
|
|
}
|
|
delete(grp.LogicalBreakpoints, bp.LogicalID)
|
|
}
|
|
}
|
|
}
|
|
if oldgrp.followExecEnabled {
|
|
rgx := ""
|
|
if oldgrp.followExecRegex != nil {
|
|
rgx = oldgrp.followExecRegex.String()
|
|
}
|
|
grp.FollowExec(true, rgx)
|
|
}
|
|
}
|
|
|
|
func (grp *TargetGroup) addTarget(p ProcessInternal, pid int, currentThread Thread, path string, stopReason StopReason, cmdline string) (*Target, error) {
|
|
logger := logflags.DebuggerLogger()
|
|
if len(grp.targets) > 0 {
|
|
if !grp.followExecEnabled {
|
|
logger.Debugf("Detaching from child target (follow-exec disabled) %d %q", pid, cmdline)
|
|
return nil, nil
|
|
}
|
|
if grp.followExecRegex != nil && !grp.followExecRegex.MatchString(cmdline) {
|
|
logger.Debugf("Detaching from child target (follow-exec regex not matched) %d %q", pid, cmdline)
|
|
return nil, nil
|
|
}
|
|
}
|
|
t, err := grp.newTarget(p, pid, currentThread, path, cmdline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.StopReason = stopReason
|
|
logger.Debugf("Adding target %d %q", t.Pid(), t.CmdLine)
|
|
if t.partOfGroup {
|
|
panic("internal error: target is already part of group")
|
|
}
|
|
t.partOfGroup = true
|
|
if grp.RecordingManipulation == nil {
|
|
grp.RecordingManipulation = t.recman
|
|
grp.recman = t.recman
|
|
}
|
|
if grp.Selected == nil {
|
|
grp.Selected = t
|
|
}
|
|
t.Breakpoints().Logical = grp.LogicalBreakpoints
|
|
for _, lbp := range grp.LogicalBreakpoints {
|
|
if lbp.LogicalID < 0 {
|
|
continue
|
|
}
|
|
err := enableBreakpointOnTarget(t, lbp)
|
|
if err != nil {
|
|
logger.Debugf("could not enable breakpoint %d on new target %d: %v", lbp.LogicalID, t.Pid(), err)
|
|
} else {
|
|
logger.Debugf("breakpoint %d enabled on new target %d: %v", lbp.LogicalID, t.Pid(), err)
|
|
}
|
|
}
|
|
grp.targets = append(grp.targets, t)
|
|
return t, nil
|
|
}
|
|
|
|
// Targets returns a slice of all targets in the group, including the
|
|
// ones that are no longer valid.
|
|
func (grp *TargetGroup) Targets() []*Target {
|
|
return grp.targets
|
|
}
|
|
|
|
// Valid returns true if any target in the target group is valid.
|
|
func (grp *TargetGroup) Valid() (bool, error) {
|
|
var err0 error
|
|
for _, t := range grp.targets {
|
|
ok, err := t.Valid()
|
|
if ok {
|
|
return true, nil
|
|
}
|
|
if err0 == nil {
|
|
err0 = err
|
|
}
|
|
}
|
|
return false, err0
|
|
}
|
|
|
|
func (grp *TargetGroup) numValid() int {
|
|
r := 0
|
|
for _, t := range grp.targets {
|
|
ok, _ := t.Valid()
|
|
if ok {
|
|
r++
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Detach detaches all targets in the group.
|
|
func (grp *TargetGroup) Detach(kill bool) error {
|
|
var errs []string
|
|
for i := len(grp.targets) - 1; i >= 0; i-- {
|
|
t := grp.targets[i]
|
|
isvalid, _ := t.Valid()
|
|
if !isvalid {
|
|
continue
|
|
}
|
|
err := grp.detachTarget(t, kill)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("could not detach process %d: %v", t.Pid(), err))
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
|
}
|
|
return grp.procgrp.Close()
|
|
}
|
|
|
|
// detachTarget will detach the target from the underlying process.
|
|
// This means the debugger will no longer receive events from the process
|
|
// we were previously debugging.
|
|
// If kill is true then the process will be killed when we detach.
|
|
func (grp *TargetGroup) detachTarget(t *Target, kill bool) error {
|
|
if !kill {
|
|
if t.asyncPreemptChanged {
|
|
setAsyncPreemptOff(t, t.asyncPreemptOff)
|
|
}
|
|
for _, bp := range t.Breakpoints().M {
|
|
if bp != nil {
|
|
err := t.ClearBreakpoint(bp.Addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
t.StopReason = StopUnknown
|
|
return grp.procgrp.Detach(t.Pid(), kill)
|
|
}
|
|
|
|
// HasSteppingBreakpoints returns true if any of the targets has stepping breakpoints set.
|
|
func (grp *TargetGroup) HasSteppingBreakpoints() bool {
|
|
for _, t := range grp.targets {
|
|
if t.Breakpoints().HasSteppingBreakpoints() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ClearSteppingBreakpoints removes all stepping breakpoints.
|
|
func (grp *TargetGroup) ClearSteppingBreakpoints() error {
|
|
for _, t := range grp.targets {
|
|
if t.Breakpoints().HasSteppingBreakpoints() {
|
|
return t.ClearSteppingBreakpoints()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ThreadList returns a list of all threads in all target processes.
|
|
func (grp *TargetGroup) ThreadList() []Thread {
|
|
r := []Thread{}
|
|
for _, t := range grp.targets {
|
|
r = append(r, t.ThreadList()...)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// TargetForThread returns the target containing the given thread.
|
|
func (grp *TargetGroup) TargetForThread(tid int) *Target {
|
|
for _, t := range grp.targets {
|
|
if _, ok := t.FindThread(tid); ok {
|
|
return t
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetBreakpointEnabled either enables or disabled the specified breakpoint based on the value of enabled.
|
|
func (grp *TargetGroup) SetBreakpointEnabled(lbp *LogicalBreakpoint, enabled bool) (err error) {
|
|
switch {
|
|
case lbp.enabled && !enabled:
|
|
lbp.enabled = false
|
|
err = grp.disableBreakpoint(lbp)
|
|
case !lbp.enabled && enabled:
|
|
lbp.enabled = true
|
|
lbp.condSatisfiable = breakpointConditionSatisfiable(lbp)
|
|
err = grp.enableBreakpoint(lbp)
|
|
}
|
|
return
|
|
}
|
|
|
|
// enableBreakpoint re-enables a disabled logical breakpoint.
|
|
func (grp *TargetGroup) enableBreakpoint(lbp *LogicalBreakpoint) error {
|
|
var err0, errNotFound, errExists error
|
|
didSet := false
|
|
targetLoop:
|
|
for _, p := range grp.targets {
|
|
err := enableBreakpointOnTarget(p, lbp)
|
|
|
|
switch err.(type) {
|
|
case nil:
|
|
didSet = true
|
|
case *ErrFunctionNotFound, *ErrCouldNotFindLine:
|
|
errNotFound = err
|
|
case BreakpointExistsError:
|
|
errExists = err
|
|
default:
|
|
err0 = err
|
|
break targetLoop
|
|
}
|
|
}
|
|
if errNotFound != nil && !didSet {
|
|
return errNotFound
|
|
}
|
|
if errExists != nil && !didSet {
|
|
return errExists
|
|
}
|
|
if !didSet {
|
|
if _, err := grp.Valid(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err0 != nil {
|
|
it := ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
for _, bp := range it.Breakpoints().M {
|
|
if bp.LogicalID() == lbp.LogicalID {
|
|
if err1 := it.ClearBreakpoint(bp.Addr); err1 != nil {
|
|
return fmt.Errorf("error while creating breakpoint: %v, additionally the breakpoint could not be properly rolled back: %v", err0, err1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return err0
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func enableBreakpointOnTarget(p *Target, lbp *LogicalBreakpoint) error {
|
|
if !lbp.enabled || !lbp.condSatisfiable {
|
|
return nil
|
|
}
|
|
var err error
|
|
var addrs []uint64
|
|
switch {
|
|
case lbp.Set.File != "":
|
|
addrs, err = FindFileLocation(p, lbp.Set.File, lbp.Set.Line)
|
|
case lbp.Set.FunctionName != "":
|
|
addrs, err = FindFunctionLocation(p, lbp.Set.FunctionName, lbp.Set.Line)
|
|
case len(lbp.Set.PidAddrs) > 0:
|
|
for _, pidAddr := range lbp.Set.PidAddrs {
|
|
if pidAddr.Pid == p.Pid() {
|
|
addrs = append(addrs, pidAddr.Addr)
|
|
}
|
|
}
|
|
case lbp.Set.Expr != nil:
|
|
addrs = lbp.Set.Expr(p)
|
|
default:
|
|
return fmt.Errorf("breakpoint %d can not be enabled", lbp.LogicalID)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
_, err = p.SetBreakpoint(lbp.LogicalID, addr, UserBreakpoint, nil)
|
|
if err != nil {
|
|
if _, isexists := err.(BreakpointExistsError); isexists {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// disableBreakpoint disables a logical breakpoint.
|
|
func (grp *TargetGroup) disableBreakpoint(lbp *LogicalBreakpoint) error {
|
|
var errs []error
|
|
n := 0
|
|
it := ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
for _, bp := range it.Breakpoints().M {
|
|
if bp.LogicalID() == lbp.LogicalID {
|
|
n++
|
|
err := it.ClearBreakpoint(bp.Addr)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
buf := new(bytes.Buffer)
|
|
for i, err := range errs {
|
|
fmt.Fprintf(buf, "%s", err)
|
|
if i != len(errs)-1 {
|
|
fmt.Fprintf(buf, ", ")
|
|
}
|
|
}
|
|
|
|
if len(errs) == n {
|
|
return fmt.Errorf("unable to clear breakpoint %d: %v", lbp.LogicalID, buf.String())
|
|
}
|
|
return fmt.Errorf("unable to clear breakpoint %d (partial): %s", lbp.LogicalID, buf.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ChangeBreakpointCondition changes the breakpoint condition of lbp.
|
|
func (grp *TargetGroup) ChangeBreakpointCondition(lbp *LogicalBreakpoint, cond, hitCond string, hitCondPerG bool) error {
|
|
lbp.cond = nil
|
|
if cond != "" {
|
|
var err error
|
|
lbp.cond, err = parser.ParseExpr(cond)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
t := ValidTargets{Group: grp}
|
|
for t.Next() {
|
|
for _, bp := range t.Breakpoints().M {
|
|
if bp.LogicalID() == lbp.LogicalID {
|
|
bp.UserBreaklet().Cond = lbp.cond
|
|
}
|
|
}
|
|
}
|
|
|
|
lbp.hitCond = nil
|
|
if hitCond != "" {
|
|
opTok, val, err := parseHitCondition(hitCond)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lbp.hitCond = &struct {
|
|
Op token.Token
|
|
Val int
|
|
}{opTok, val}
|
|
lbp.HitCondPerG = hitCondPerG
|
|
}
|
|
|
|
if lbp.enabled {
|
|
switch {
|
|
case lbp.condSatisfiable && !breakpointConditionSatisfiable(lbp):
|
|
lbp.condSatisfiable = false
|
|
grp.disableBreakpoint(lbp)
|
|
case !lbp.condSatisfiable && breakpointConditionSatisfiable(lbp):
|
|
lbp.condSatisfiable = true
|
|
grp.enableBreakpoint(lbp)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func parseHitCondition(hitCond string) (token.Token, int, error) {
|
|
// A hit condition can be in the following formats:
|
|
// - "number"
|
|
// - "OP number"
|
|
hitConditionRegex := regexp.MustCompile(`(([=><%!])+|)( |)((\d|_)+)`)
|
|
|
|
match := hitConditionRegex.FindStringSubmatch(strings.TrimSpace(hitCond))
|
|
if match == nil || len(match) != 6 {
|
|
return 0, 0, fmt.Errorf("unable to parse breakpoint hit condition: %q\nhit conditions should be of the form \"number\" or \"OP number\"", hitCond)
|
|
}
|
|
|
|
opStr := match[1]
|
|
var opTok token.Token
|
|
switch opStr {
|
|
case "==", "":
|
|
opTok = token.EQL
|
|
case ">=":
|
|
opTok = token.GEQ
|
|
case "<=":
|
|
opTok = token.LEQ
|
|
case ">":
|
|
opTok = token.GTR
|
|
case "<":
|
|
opTok = token.LSS
|
|
case "%":
|
|
opTok = token.REM
|
|
case "!=":
|
|
opTok = token.NEQ
|
|
default:
|
|
return 0, 0, fmt.Errorf("unable to parse breakpoint hit condition: %q\ninvalid operator: %q", hitCond, opStr)
|
|
}
|
|
|
|
numStr := match[4]
|
|
val, parseErr := strconv.Atoi(numStr)
|
|
if parseErr != nil {
|
|
return 0, 0, fmt.Errorf("unable to parse breakpoint hit condition: %q\ninvalid number: %q", hitCond, numStr)
|
|
}
|
|
|
|
return opTok, val, nil
|
|
}
|
|
|
|
// manageUnsatisfiableBreakpoints automatically disables breakpoints with unsatisifiable hit conditions.
|
|
func (grp *TargetGroup) manageUnsatisfiableBreakpoints() error {
|
|
for _, lbp := range grp.LogicalBreakpoints {
|
|
if lbp.enabled {
|
|
if lbp.condSatisfiable && !breakpointConditionSatisfiable(lbp) {
|
|
lbp.condSatisfiable = false
|
|
err := grp.disableBreakpoint(lbp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FollowExec enables or disables follow exec mode. When follow exec mode is
|
|
// enabled new processes spawned by the target process are automatically
|
|
// added to the target group.
|
|
// If regex is not the empty string only processes whose command line
|
|
// matches regex will be added to the target group.
|
|
func (grp *TargetGroup) FollowExec(v bool, regex string) error {
|
|
grp.followExecRegex = nil
|
|
if regex != "" && v {
|
|
var err error
|
|
grp.followExecRegex, err = regexp.Compile(regex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
it := ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
err := it.proc.FollowExec(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
grp.followExecEnabled = v
|
|
return nil
|
|
}
|
|
|
|
// FollowExecEnabled returns true if follow exec is enabled
|
|
func (grp *TargetGroup) FollowExecEnabled() bool {
|
|
return grp.followExecEnabled
|
|
}
|
|
|
|
// ValidTargets iterates through all valid targets in Group.
|
|
type ValidTargets struct {
|
|
*Target
|
|
Group *TargetGroup
|
|
start int
|
|
}
|
|
|
|
// Next moves to the next valid target, returns false if there aren't more
|
|
// valid targets in the group.
|
|
func (it *ValidTargets) Next() bool {
|
|
for i := it.start; i < len(it.Group.targets); i++ {
|
|
if ok, _ := it.Group.targets[i].Valid(); ok {
|
|
it.Target = it.Group.targets[i]
|
|
it.start = i + 1
|
|
return true
|
|
}
|
|
}
|
|
it.start = len(it.Group.targets)
|
|
it.Target = nil
|
|
return false
|
|
}
|
|
|
|
// Reset returns the iterator to the start of the group.
|
|
func (it *ValidTargets) Reset() {
|
|
it.Target = nil
|
|
it.start = 0
|
|
}
|