delve/pkg/proc/target_group.go
Alessandro Arzilli d97b471292
pkg/proc,service/debugger: do not disable unsatisfiable breakpoints (#3868)
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.
2024-12-04 19:07:56 -08:00

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
}