Skip to content

Commit

Permalink
proc: expose breakpoint hitcounts in expressions
Browse files Browse the repository at this point in the history
Expose breakpoint hitcounts in the expression language through the
special variable runtime.bphitcount:
  runtime.bphitcount[1]
  runtime.bphitcount["bpname"]
will evaluate respectively to the hitcount of breakpoint with id == 1
and to the hitcount of the breakpoint named "bpname".

This is intended to be used in breakpoint conditions and allows
breakpoints to be chained such that one breakpoint is only hit after a
different is hit first.

A few optimizations are implemented so that chained breakpoints are
evaluated efficiently.
  • Loading branch information
aarzilli committed Dec 5, 2024
1 parent 477e46e commit 33165b0
Show file tree
Hide file tree
Showing 15 changed files with 459 additions and 29 deletions.
2 changes: 1 addition & 1 deletion Documentation/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ Set breakpoint condition.

Specifies that the breakpoint, tracepoint or watchpoint should break only if the boolean expression is true.

See [Documentation/cli/expr.md](//github.com/go-delve/delve/tree/master/Documentation/cli/expr.md) for a description of supported expressions.
See [Documentation/cli/expr.md](//github.com/go-delve/delve/tree/master/Documentation/cli/expr.md) for a description of supported expressions and [Documentation/cli/cond.md](//github.com/go-delve/delve/tree/master/Documentation/cli/cond.md) for a description of how breakpoint conditions are evaluated.

With the -hitcount option a condition on the breakpoint hit count can be set, the following operators are supported

Expand Down
15 changes: 15 additions & 0 deletions Documentation/cli/cond.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Breakpoint conditions

Breakpoints have two conditions:

* The normal condition, which is specified using the command `cond <breakpoint> <expr>` (or by setting the Cond field when amending a breakpoint via the API), is any [expression](expr.md) which evaluates to true or false.
* The hitcount condition, which is specified `cond <breakpoint> -hitcount <operator> <number>` (or by setting the HitCond field when amending a breakpoint via the API), is a constraint on the number of times the breakpoint has been hit.

When a breakpoint location is encountered during the execution of the program, the debugger will:

* Evaluate the normal condition
* Stop if there is an error while evaluating the normal condition
* If the normal condition evaluates to true the hit count is incremented
* Evaluate the hitcount condition
* If the hitcount condition is also satisfied stop the execution at the breakpoint

1 change: 1 addition & 0 deletions Documentation/cli/expr.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ Delve defines two special variables:

* `runtime.curg` evaluates to the 'g' struct for the current goroutine, in particular `runtime.curg.goid` is the goroutine id of the current goroutine.
* `runtime.frameoff` is the offset of the frame's base address from the bottom of the stack.
* `runtime.bphitcount[X]` is the total hitcount for breakpoint X, which can be either an ID or the breakpoint name as a string.

## Access to variables from previous frames

Expand Down
22 changes: 22 additions & 0 deletions Documentation/cli/starlark.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,25 @@ var = eval(
{"FollowPointers":True, "MaxVariableRecurse":2, "MaxStringLen":100, "MaxArrayValues":10, "MaxStructFields":100}
)
```

## Chain breakpoints

Chain a number of breakpoints such that breakpoint n+1 is only hit after breakpoint n is hit:

```python
def command_breakchain(*args):
v = args.split(" ")

bp = get_breakpoint(int(v[0]), "").Breakpoint
bp.HitCond = "== 1"
amend_breakpoint(bp)

for i in range(1, len(v)):
bp = get_breakpoint(int(v[i]), "").Breakpoint
if i != len(v)-1:
bp.HitCond = "== 1"
bp.Cond = "runtime.bphitcount[" + v[i-1] + "] > 0"
amend_breakpoint(bp)
```

To be used as `chain 1 2 3` where `1`, `2`, and `3` are IDs of breakpoints to chain together.
31 changes: 31 additions & 0 deletions _fixtures/bphitcountchain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package main

import "fmt"

func breakfunc1() {
fmt.Println("breakfunc1")
}

func breakfunc2() {
fmt.Println("breakfunc2")
}

func breakfunc3() {
fmt.Println("breakfunc3")
}

func main() {
breakfunc2()
breakfunc3()

breakfunc1() // hit
breakfunc3()
breakfunc1()

breakfunc2() // hit
breakfunc1()

breakfunc3() // hit
breakfunc1()
breakfunc2()
}
13 changes: 13 additions & 0 deletions _fixtures/chain_breakpoints.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def command_chain(args):
v = args.split(" ")

bp = get_breakpoint(int(v[0]), "").Breakpoint
bp.HitCond = "== 1"
amend_breakpoint(bp)

for i in range(1, len(v)):
bp = get_breakpoint(int(v[i]), "").Breakpoint
if i != len(v)-1:
bp.HitCond = "== 1"
bp.Cond = "runtime.bphitcount[" + v[i-1] + "] > 0"
amend_breakpoint(bp)
158 changes: 150 additions & 8 deletions pkg/proc/breakpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import (
"go/printer"
"go/token"
"reflect"
"strconv"

"github.com/go-delve/delve/pkg/dwarf/godwarf"
"github.com/go-delve/delve/pkg/dwarf/op"
"github.com/go-delve/delve/pkg/dwarf/reader"
"github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/proc/evalop"
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
)

Expand Down Expand Up @@ -920,6 +922,24 @@ func (bpmap *BreakpointMap) HasHWBreakpoints() bool {
return false
}

func totalHitCountByName(lbpmap map[int]*LogicalBreakpoint, s string) (uint64, error) {
for _, bp := range lbpmap {
if bp.Name == s {
return bp.TotalHitCount, nil
}
}
return 0, fmt.Errorf("could not find breakpoint named %q", s)
}

func totalHitCountByID(lbpmap map[int]*LogicalBreakpoint, id int) (uint64, error) {
for _, bp := range lbpmap {
if bp.LogicalID == int(id) {
return bp.TotalHitCount, nil
}
}
return 0, fmt.Errorf("could not find breakpoint with ID = %d", id)
}

// BreakpointState describes the state of a breakpoint in a thread.
type BreakpointState struct {
*Breakpoint
Expand Down Expand Up @@ -1063,6 +1083,8 @@ type LogicalBreakpoint struct {

// condSatisfiable is true when 'cond && hitCond' can potentially be true.
condSatisfiable bool
// condUsesHitCounts is true when 'cond' uses breakpoint hitcounts
condUsesHitCounts bool

UserData interface{} // Any additional information about the breakpoint
// Name of root function from where tracing needs to be done
Expand Down Expand Up @@ -1105,15 +1127,135 @@ func (lbp *LogicalBreakpoint) Cond() string {
return buf.String()
}

func breakpointConditionSatisfiable(lbp *LogicalBreakpoint) bool {
if lbp.hitCond == nil || lbp.HitCondPerG {
func breakpointConditionSatisfiable(lbpmap map[int]*LogicalBreakpoint, lbp *LogicalBreakpoint) bool {
if lbp.hitCond != nil && !lbp.HitCondPerG {
switch lbp.hitCond.Op {
case token.EQL, token.LEQ:
if int(lbp.TotalHitCount) >= lbp.hitCond.Val {
return false
}
case token.LSS:
if int(lbp.TotalHitCount) >= lbp.hitCond.Val-1 {
return false
}
}
}
if !lbp.condUsesHitCounts {
return true
}
switch lbp.hitCond.Op {
case token.EQL, token.LEQ:
return int(lbp.TotalHitCount) < lbp.hitCond.Val
case token.LSS:
return int(lbp.TotalHitCount) < lbp.hitCond.Val-1

toint := func(x ast.Expr) (uint64, bool) {
lit, ok := x.(*ast.BasicLit)
if !ok || lit.Kind != token.INT {
return 0, false
}
n, err := strconv.Atoi(lit.Value)
return uint64(n), err == nil && n >= 0
}
return true

hitcountexpr := func(x ast.Expr) (uint64, bool) {
idx, ok := x.(*ast.IndexExpr)
if !ok {
return 0, false
}
selx, ok := idx.X.(*ast.SelectorExpr)
if !ok {
return 0, false
}
ident, ok := selx.X.(*ast.Ident)
if !ok || ident.Name != "runtime" || selx.Sel.Name != evalop.BreakpointHitCountVarName {
return 0, false
}
lit, ok := idx.Index.(*ast.BasicLit)
if !ok {
return 0, false
}
switch lit.Kind {
case token.INT:
n, _ := strconv.Atoi(lit.Value)
thc, err := totalHitCountByID(lbpmap, n)
return thc, err == nil
case token.STRING:
v, _ := strconv.Unquote(lit.Value)
thc, err := totalHitCountByName(lbpmap, v)
return thc, err == nil
default:
return 0, false
}
}

var satisf func(n ast.Node) bool
satisf = func(n ast.Node) bool {
parexpr, ok := n.(*ast.ParenExpr)
if ok {
return satisf(parexpr.X)
}
binexpr, ok := n.(*ast.BinaryExpr)
if !ok {
return true
}
switch binexpr.Op {
case token.AND:
return satisf(binexpr.X) && satisf(binexpr.Y)
case token.OR:
if !satisf(binexpr.X) {
return false
}
if !satisf(binexpr.Y) {
return false
}
return true
case token.EQL, token.LEQ, token.LSS, token.NEQ, token.GTR, token.GEQ:
default:
return true
}

hitcount, ok1 := hitcountexpr(binexpr.X)
val, ok2 := toint(binexpr.Y)
if !ok1 || !ok2 {
return true
}

switch binexpr.Op {
case token.EQL:
return hitcount == val
case token.LEQ:
return hitcount <= val
case token.LSS:
return hitcount < val
case token.NEQ:
return hitcount != val
case token.GTR:
return hitcount > val
case token.GEQ:
return hitcount >= val
}
return true
}

return satisf(lbp.cond)
}

func breakpointConditionUsesHitCounts(lbp *LogicalBreakpoint) bool {
if lbp.cond == nil {
return false
}
r := false
ast.Inspect(lbp.cond, func(n ast.Node) bool {
if r {
return false
}
seln, ok := n.(*ast.SelectorExpr)
if ok {
ident, ok := seln.X.(*ast.Ident)
if ok {
if ident.Name == "runtime" && seln.Sel.Name == evalop.BreakpointHitCountVarName {
r = true
return false
}
}
}
return true
})
return r
}
30 changes: 30 additions & 0 deletions pkg/proc/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,9 @@ func (stack *evalStack) executeOp() {
case *evalop.PushDebugPinner:
stack.push(stack.debugPinner)

case *evalop.PushBreakpointHitCount:
stack.push(newVariable("runtime."+evalop.BreakpointHitCountVarName, fakeAddressUnresolv, godwarf.FakeSliceType(godwarf.FakeBasicType("uint", 64)), scope.BinInfo, scope.Mem))

default:
stack.err = fmt.Errorf("internal debugger error: unknown eval opcode: %#v", op)
}
Expand Down Expand Up @@ -2064,6 +2067,33 @@ func (scope *EvalScope) evalIndex(op *evalop.Index, stack *evalStack) {
return
}

if xev.Name == "runtime."+evalop.BreakpointHitCountVarName {
if idxev.Kind == reflect.String {
s := constant.StringVal(idxev.Value)
thc, err := totalHitCountByName(scope.target.Breakpoints().Logical, s)
if err == nil {
stack.push(newConstant(constant.MakeUint64(thc), scope.Mem))
}
stack.err = err
return
}
n, err := idxev.asInt()
if err != nil {
n2, err := idxev.asUint()
if err != nil {
stack.err = fmt.Errorf("can not index %s with %s", xev.Name, exprToString(op.Node.Index))
return
}
n = int64(n2)
}
thc, err := totalHitCountByID(scope.target.Breakpoints().Logical, int(n))
if err == nil {
stack.push(newConstant(constant.MakeUint64(thc), scope.Mem))
}
stack.err = err
return
}

if xev.Flags&VariableCPtr == 0 {
xev = xev.maybeDereference()
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/proc/evalop/evalcompile.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import (
)

var (
ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
DebugPinnerFunctionName = "runtime.debugPinnerV1"
ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
)

const (
BreakpointHitCountVarName = "bphitcount"
DebugPinnerFunctionName = "runtime.debugPinnerV1"
)

type compileCtx struct {
Expand Down Expand Up @@ -276,6 +280,9 @@ func (ctx *compileCtx) compileAST(t ast.Expr) error {
case x.Name == "runtime" && node.Sel.Name == "rangeParentOffset":
ctx.pushOp(&PushRangeParentOffset{})

case x.Name == "runtime" && node.Sel.Name == BreakpointHitCountVarName:
ctx.pushOp(&PushBreakpointHitCount{})

default:
ctx.pushOp(&PushPackageVarOrSelect{Name: x.Name, Sel: node.Sel.Name})
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/proc/evalop/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,10 @@ type PushPinAddress struct {
}

func (*PushPinAddress) depthCheck() (npop, npush int) { return 0, 1 }

// PushBreakpointHitCount pushes a special array containing the hit counts
// of breakpoints.
type PushBreakpointHitCount struct {
}

func (*PushBreakpointHitCount) depthCheck() (npop, npush int) { return 0, 1 }
Loading

0 comments on commit 33165b0

Please sign in to comment.