Skip to content

Commit ebb6951

Browse files
committed
proc: expose breakpoint hitcounts in expressions
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.
1 parent 5d9a7df commit ebb6951

15 files changed

+459
-29
lines changed

Documentation/cli/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ Set breakpoint condition.
211211

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

214-
See [Documentation/cli/expr.md](//github.com/go-delve/delve/tree/master/Documentation/cli/expr.md) for a description of supported expressions.
214+
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.
215215

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

Documentation/cli/cond.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Breakpoint conditions
2+
3+
Breakpoints have two conditions:
4+
5+
* 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.
6+
* 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.
7+
8+
When a breakpoint location is encountered during the execution of the program, the debugger will:
9+
10+
* Evaluate the normal condition
11+
* Stop if there is an error while evaluating the normal condition
12+
* If the normal condition evaluates to true the hit count is incremented
13+
* Evaluate the hitcount condition
14+
* If the hitcount condition is also satisfied stop the execution at the breakpoint
15+

Documentation/cli/expr.md

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ Delve defines two special variables:
119119

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

123124
## Access to variables from previous frames
124125

Documentation/cli/starlark.md

+22
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,25 @@ var = eval(
340340
{"FollowPointers":True, "MaxVariableRecurse":2, "MaxStringLen":100, "MaxArrayValues":10, "MaxStructFields":100}
341341
)
342342
```
343+
344+
## Chain breakpoints
345+
346+
Chain a number of breakpoints such that breakpoint n+1 is only hit after breakpoint n is hit:
347+
348+
```python
349+
def command_breakchain(*args):
350+
v = args.split(" ")
351+
352+
bp = get_breakpoint(int(v[0]), "").Breakpoint
353+
bp.HitCond = "== 1"
354+
amend_breakpoint(bp)
355+
356+
for i in range(1, len(v)):
357+
bp = get_breakpoint(int(v[i]), "").Breakpoint
358+
if i != len(v)-1:
359+
bp.HitCond = "== 1"
360+
bp.Cond = "runtime.bphitcount[" + v[i-1] + "] > 0"
361+
amend_breakpoint(bp)
362+
```
363+
364+
To be used as `chain 1 2 3` where `1`, `2`, and `3` are IDs of breakpoints to chain together.

_fixtures/bphitcountchain.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import "fmt"
4+
5+
func breakfunc1() {
6+
fmt.Println("breakfunc1")
7+
}
8+
9+
func breakfunc2() {
10+
fmt.Println("breakfunc2")
11+
}
12+
13+
func breakfunc3() {
14+
fmt.Println("breakfunc3")
15+
}
16+
17+
func main() {
18+
breakfunc2()
19+
breakfunc3()
20+
21+
breakfunc1() // hit
22+
breakfunc3()
23+
breakfunc1()
24+
25+
breakfunc2() // hit
26+
breakfunc1()
27+
28+
breakfunc3() // hit
29+
breakfunc1()
30+
breakfunc2()
31+
}

_fixtures/chain_breakpoints.star

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
def command_chain(args):
2+
v = args.split(" ")
3+
4+
bp = get_breakpoint(int(v[0]), "").Breakpoint
5+
bp.HitCond = "== 1"
6+
amend_breakpoint(bp)
7+
8+
for i in range(1, len(v)):
9+
bp = get_breakpoint(int(v[i]), "").Breakpoint
10+
if i != len(v)-1:
11+
bp.HitCond = "== 1"
12+
bp.Cond = "runtime.bphitcount[" + v[i-1] + "] > 0"
13+
amend_breakpoint(bp)

pkg/proc/breakpoints.go

+150-8
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import (
1111
"go/printer"
1212
"go/token"
1313
"reflect"
14+
"strconv"
1415

1516
"github.com/go-delve/delve/pkg/dwarf/godwarf"
1617
"github.com/go-delve/delve/pkg/dwarf/op"
1718
"github.com/go-delve/delve/pkg/dwarf/reader"
1819
"github.com/go-delve/delve/pkg/goversion"
20+
"github.com/go-delve/delve/pkg/proc/evalop"
1921
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
2022
)
2123

@@ -920,6 +922,24 @@ func (bpmap *BreakpointMap) HasHWBreakpoints() bool {
920922
return false
921923
}
922924

925+
func totalHitCountByName(lbpmap map[int]*LogicalBreakpoint, s string) (uint64, error) {
926+
for _, bp := range lbpmap {
927+
if bp.Name == s {
928+
return bp.TotalHitCount, nil
929+
}
930+
}
931+
return 0, fmt.Errorf("could not find breakpoint named %q", s)
932+
}
933+
934+
func totalHitCountByID(lbpmap map[int]*LogicalBreakpoint, id int) (uint64, error) {
935+
for _, bp := range lbpmap {
936+
if bp.LogicalID == int(id) {
937+
return bp.TotalHitCount, nil
938+
}
939+
}
940+
return 0, fmt.Errorf("could not find breakpoint with ID = %d", id)
941+
}
942+
923943
// BreakpointState describes the state of a breakpoint in a thread.
924944
type BreakpointState struct {
925945
*Breakpoint
@@ -1063,6 +1083,8 @@ type LogicalBreakpoint struct {
10631083

10641084
// condSatisfiable is true when 'cond && hitCond' can potentially be true.
10651085
condSatisfiable bool
1086+
// condUsesHitCounts is true when 'cond' uses breakpoint hitcounts
1087+
condUsesHitCounts bool
10661088

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

1108-
func breakpointConditionSatisfiable(lbp *LogicalBreakpoint) bool {
1109-
if lbp.hitCond == nil || lbp.HitCondPerG {
1130+
func breakpointConditionSatisfiable(lbpmap map[int]*LogicalBreakpoint, lbp *LogicalBreakpoint) bool {
1131+
if lbp.hitCond != nil && !lbp.HitCondPerG {
1132+
switch lbp.hitCond.Op {
1133+
case token.EQL, token.LEQ:
1134+
if int(lbp.TotalHitCount) >= lbp.hitCond.Val {
1135+
return false
1136+
}
1137+
case token.LSS:
1138+
if int(lbp.TotalHitCount) >= lbp.hitCond.Val-1 {
1139+
return false
1140+
}
1141+
}
1142+
}
1143+
if !lbp.condUsesHitCounts {
11101144
return true
11111145
}
1112-
switch lbp.hitCond.Op {
1113-
case token.EQL, token.LEQ:
1114-
return int(lbp.TotalHitCount) < lbp.hitCond.Val
1115-
case token.LSS:
1116-
return int(lbp.TotalHitCount) < lbp.hitCond.Val-1
1146+
1147+
toint := func(x ast.Expr) (uint64, bool) {
1148+
lit, ok := x.(*ast.BasicLit)
1149+
if !ok || lit.Kind != token.INT {
1150+
return 0, false
1151+
}
1152+
n, err := strconv.Atoi(lit.Value)
1153+
return uint64(n), err == nil && n >= 0
11171154
}
1118-
return true
1155+
1156+
hitcountexpr := func(x ast.Expr) (uint64, bool) {
1157+
idx, ok := x.(*ast.IndexExpr)
1158+
if !ok {
1159+
return 0, false
1160+
}
1161+
selx, ok := idx.X.(*ast.SelectorExpr)
1162+
if !ok {
1163+
return 0, false
1164+
}
1165+
ident, ok := selx.X.(*ast.Ident)
1166+
if !ok || ident.Name != "runtime" || selx.Sel.Name != evalop.BreakpointHitCountVarName {
1167+
return 0, false
1168+
}
1169+
lit, ok := idx.Index.(*ast.BasicLit)
1170+
if !ok {
1171+
return 0, false
1172+
}
1173+
switch lit.Kind {
1174+
case token.INT:
1175+
n, _ := strconv.Atoi(lit.Value)
1176+
thc, err := totalHitCountByID(lbpmap, n)
1177+
return thc, err == nil
1178+
case token.STRING:
1179+
v, _ := strconv.Unquote(lit.Value)
1180+
thc, err := totalHitCountByName(lbpmap, v)
1181+
return thc, err == nil
1182+
default:
1183+
return 0, false
1184+
}
1185+
}
1186+
1187+
var satisf func(n ast.Node) bool
1188+
satisf = func(n ast.Node) bool {
1189+
parexpr, ok := n.(*ast.ParenExpr)
1190+
if ok {
1191+
return satisf(parexpr.X)
1192+
}
1193+
binexpr, ok := n.(*ast.BinaryExpr)
1194+
if !ok {
1195+
return true
1196+
}
1197+
switch binexpr.Op {
1198+
case token.AND:
1199+
return satisf(binexpr.X) && satisf(binexpr.Y)
1200+
case token.OR:
1201+
if !satisf(binexpr.X) {
1202+
return false
1203+
}
1204+
if !satisf(binexpr.Y) {
1205+
return false
1206+
}
1207+
return true
1208+
case token.EQL, token.LEQ, token.LSS, token.NEQ, token.GTR, token.GEQ:
1209+
default:
1210+
return true
1211+
}
1212+
1213+
hitcount, ok1 := hitcountexpr(binexpr.X)
1214+
val, ok2 := toint(binexpr.Y)
1215+
if !ok1 || !ok2 {
1216+
return true
1217+
}
1218+
1219+
switch binexpr.Op {
1220+
case token.EQL:
1221+
return hitcount == val
1222+
case token.LEQ:
1223+
return hitcount <= val
1224+
case token.LSS:
1225+
return hitcount < val
1226+
case token.NEQ:
1227+
return hitcount != val
1228+
case token.GTR:
1229+
return hitcount > val
1230+
case token.GEQ:
1231+
return hitcount >= val
1232+
}
1233+
return true
1234+
}
1235+
1236+
return satisf(lbp.cond)
1237+
}
1238+
1239+
func breakpointConditionUsesHitCounts(lbp *LogicalBreakpoint) bool {
1240+
if lbp.cond == nil {
1241+
return false
1242+
}
1243+
r := false
1244+
ast.Inspect(lbp.cond, func(n ast.Node) bool {
1245+
if r {
1246+
return false
1247+
}
1248+
seln, ok := n.(*ast.SelectorExpr)
1249+
if ok {
1250+
ident, ok := seln.X.(*ast.Ident)
1251+
if ok {
1252+
if ident.Name == "runtime" && seln.Sel.Name == evalop.BreakpointHitCountVarName {
1253+
r = true
1254+
return false
1255+
}
1256+
}
1257+
}
1258+
return true
1259+
})
1260+
return r
11191261
}

pkg/proc/eval.go

+30
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,9 @@ func (stack *evalStack) executeOp() {
12401240
case *evalop.PushDebugPinner:
12411241
stack.push(stack.debugPinner)
12421242

1243+
case *evalop.PushBreakpointHitCount:
1244+
stack.push(newVariable("runtime."+evalop.BreakpointHitCountVarName, fakeAddressUnresolv, godwarf.FakeSliceType(godwarf.FakeBasicType("uint", 64)), scope.BinInfo, scope.Mem))
1245+
12431246
default:
12441247
stack.err = fmt.Errorf("internal debugger error: unknown eval opcode: %#v", op)
12451248
}
@@ -2064,6 +2067,33 @@ func (scope *EvalScope) evalIndex(op *evalop.Index, stack *evalStack) {
20642067
return
20652068
}
20662069

2070+
if xev.Name == "runtime."+evalop.BreakpointHitCountVarName {
2071+
if idxev.Kind == reflect.String {
2072+
s := constant.StringVal(idxev.Value)
2073+
thc, err := totalHitCountByName(scope.target.Breakpoints().Logical, s)
2074+
if err == nil {
2075+
stack.push(newConstant(constant.MakeUint64(thc), scope.Mem))
2076+
}
2077+
stack.err = err
2078+
return
2079+
}
2080+
n, err := idxev.asInt()
2081+
if err != nil {
2082+
n2, err := idxev.asUint()
2083+
if err != nil {
2084+
stack.err = fmt.Errorf("can not index %s with %s", xev.Name, exprToString(op.Node.Index))
2085+
return
2086+
}
2087+
n = int64(n2)
2088+
}
2089+
thc, err := totalHitCountByID(scope.target.Breakpoints().Logical, int(n))
2090+
if err == nil {
2091+
stack.push(newConstant(constant.MakeUint64(thc), scope.Mem))
2092+
}
2093+
stack.err = err
2094+
return
2095+
}
2096+
20672097
if xev.Flags&VariableCPtr == 0 {
20682098
xev = xev.maybeDereference()
20692099
}

pkg/proc/evalop/evalcompile.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ import (
1818
)
1919

2020
var (
21-
ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
22-
DebugPinnerFunctionName = "runtime.debugPinnerV1"
21+
ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
22+
)
23+
24+
const (
25+
BreakpointHitCountVarName = "bphitcount"
26+
DebugPinnerFunctionName = "runtime.debugPinnerV1"
2327
)
2428

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

283+
case x.Name == "runtime" && node.Sel.Name == BreakpointHitCountVarName:
284+
ctx.pushOp(&PushBreakpointHitCount{})
285+
279286
default:
280287
ctx.pushOp(&PushPackageVarOrSelect{Name: x.Name, Sel: node.Sel.Name})
281288
}

pkg/proc/evalop/ops.go

+7
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,10 @@ type PushPinAddress struct {
324324
}
325325

326326
func (*PushPinAddress) depthCheck() (npop, npush int) { return 0, 1 }
327+
328+
// PushBreakpointHitCount pushes a special array containing the hit counts
329+
// of breakpoints.
330+
type PushBreakpointHitCount struct {
331+
}
332+
333+
func (*PushBreakpointHitCount) depthCheck() (npop, npush int) { return 0, 1 }

0 commit comments

Comments
 (0)