proc: implement conditional breakpoints

Backend only, no UI

Implements #120 (partial)
This commit is contained in:
aarzilli 2016-01-12 09:01:42 +01:00
parent 092571591f
commit d9a31dd598
5 changed files with 152 additions and 24 deletions

@ -1,6 +1,12 @@
package proc
import "fmt"
import (
"errors"
"fmt"
"go/ast"
"go/constant"
"reflect"
)
// Breakpoint represents a breakpoint. Stores information on the break
// point including the byte of data that originally was stored at that
@ -24,7 +30,7 @@ type Breakpoint struct {
HitCount map[int]uint64 // Number of times a breakpoint has been reached in a certain goroutine
TotalHitCount uint64 // Number of times a breakpoint has been reached
Cond int // When Cond is greater than zero this breakpoint will trigger only when the current goroutine id is equal to it
Cond ast.Expr // When Cond is not nil the breakpoint will be triggered only if evaluating Cond returns true
}
func (bp *Breakpoint) String() string {
@ -78,7 +84,7 @@ func (dbp *Process) setBreakpoint(tid int, addr uint64, temp bool) (*Breakpoint,
Line: l,
Addr: addr,
Temp: temp,
Cond: -1,
Cond: nil,
HitCount: map[int]uint64{},
}
@ -109,15 +115,25 @@ func (dbp *Process) writeSoftwareBreakpoint(thread *Thread, addr uint64) error {
return err
}
func (bp *Breakpoint) checkCondition(thread *Thread) bool {
if bp.Cond < 0 {
return true
func (bp *Breakpoint) checkCondition(thread *Thread) (bool, error) {
if bp.Cond == nil {
return true, nil
}
g, err := thread.GetG()
scope, err := thread.Scope()
if err != nil {
return false
return true, err
}
return g.ID == bp.Cond
v, err := scope.evalAST(bp.Cond)
if err != nil {
return true, fmt.Errorf("error evaluating expression: %v", err)
}
if v.Unreadable != nil {
return true, fmt.Errorf("condition expression unreadable: %v", v.Unreadable)
}
if v.Kind != reflect.Bool {
return true, errors.New("condition expression not boolean")
}
return constant.BoolVal(v.Value), nil
}
// NoBreakpointError is returned when trying to

@ -6,10 +6,13 @@ import (
"encoding/binary"
"errors"
"fmt"
"go/ast"
"go/constant"
"go/token"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
@ -297,7 +300,17 @@ func (dbp *Process) Next() (err error) {
if !goroutineExiting {
for i := range dbp.Breakpoints {
if dbp.Breakpoints[i].Temp {
dbp.Breakpoints[i].Cond = g.ID
dbp.Breakpoints[i].Cond = &ast.BinaryExpr{
Op: token.EQL,
X: &ast.SelectorExpr{
X: &ast.SelectorExpr{
X: &ast.Ident{Name: "runtime"},
Sel: &ast.Ident{Name: "curg"},
},
Sel: &ast.Ident{Name: "goid"},
},
Y: &ast.BasicLit{Kind: token.INT, Value: strconv.Itoa(g.ID)},
}
}
}
}
@ -374,20 +387,45 @@ func (dbp *Process) Continue() error {
}
}
}
return nil
return dbp.conditionErrors()
case dbp.CurrentThread.onTriggeredTempBreakpoint():
return dbp.clearTempBreakpoints()
case dbp.CurrentThread.onTriggeredBreakpoint():
if dbp.CurrentThread.onNextGoroutine() {
return dbp.clearTempBreakpoints()
err := dbp.clearTempBreakpoints()
if err != nil {
return err
}
return nil
return dbp.conditionErrors()
case dbp.CurrentThread.onTriggeredBreakpoint():
onNextGoroutine, err := dbp.CurrentThread.onNextGoroutine()
if err != nil {
return err
}
if onNextGoroutine {
err := dbp.clearTempBreakpoints()
if err != nil {
return err
}
}
return dbp.conditionErrors()
default:
// not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat
}
}
}
func (dbp *Process) conditionErrors() error {
var condErr error
for _, th := range dbp.Threads {
if th.CurrentBreakpoint != nil && th.BreakpointConditionError != nil {
if condErr == nil {
condErr = th.BreakpointConditionError
} else {
return fmt.Errorf("multiple errors evaluating conditions")
}
}
}
return condErr
}
// pick a new dbp.CurrentThread, with the following priority:
// - a thread with onTriggeredTempBreakpoint() == true
// - a thread with onTriggeredBreakpoint() == true (prioritizing trapthread)
@ -665,6 +703,8 @@ func (dbp *Process) run(fn func() error) error {
}
for _, th := range dbp.Threads {
th.CurrentBreakpoint = nil
th.BreakpointConditionMet = false
th.BreakpointConditionError = nil
}
if err := fn(); err != nil {
return err

@ -3,7 +3,9 @@ package proc
import (
"bytes"
"fmt"
"go/ast"
"go/constant"
"go/token"
"net"
"net/http"
"os"
@ -1356,3 +1358,71 @@ func BenchmarkLocalVariables(b *testing.B) {
}
})
}
func TestCondBreakpoint(t *testing.T) {
withTestProcess("parallel_next", t, func(p *Process, fixture protest.Fixture) {
addr, _, err := p.goSymTable.LineToPC(fixture.Source, 9)
assertNoError(err, t, "LineToPC")
bp, err := p.SetBreakpoint(addr)
assertNoError(err, t, "SetBreakpoint()")
bp.Cond = &ast.BinaryExpr{
Op: token.EQL,
X: &ast.Ident{Name: "n"},
Y: &ast.BasicLit{Kind: token.INT, Value: "7"},
}
assertNoError(p.Continue(), t, "Continue()")
nvar, err := evalVariable(p, "n")
assertNoError(err, t, "EvalVariable()")
n, _ := constant.Int64Val(nvar.Value)
if n != 7 {
t.Fatalf("Stoppend on wrong goroutine %d\n", n)
}
})
}
func TestCondBreakpointError(t *testing.T) {
withTestProcess("parallel_next", t, func(p *Process, fixture protest.Fixture) {
addr, _, err := p.goSymTable.LineToPC(fixture.Source, 9)
assertNoError(err, t, "LineToPC")
bp, err := p.SetBreakpoint(addr)
assertNoError(err, t, "SetBreakpoint()")
bp.Cond = &ast.BinaryExpr{
Op: token.EQL,
X: &ast.Ident{Name: "nonexistentvariable"},
Y: &ast.BasicLit{Kind: token.INT, Value: "7"},
}
err = p.Continue()
if err == nil {
t.Fatalf("No error on first Continue()")
}
if err.Error() != "error evaluating expression: could not find symbol value for nonexistentvariable" && err.Error() != "multiple errors evaluating conditions" {
t.Fatalf("Unexpected error on first Continue(): %v", err)
}
bp.Cond = &ast.BinaryExpr{
Op: token.EQL,
X: &ast.Ident{Name: "n"},
Y: &ast.BasicLit{Kind: token.INT, Value: "7"},
}
err = p.Continue()
if err != nil {
if _, exited := err.(ProcessExitedError); !exited {
t.Fatalf("Unexpected error on second Continue(): %v", err)
}
} else {
nvar, err := evalVariable(p, "n")
assertNoError(err, t, "EvalVariable()")
n, _ := constant.Int64Val(nvar.Value)
if n != 7 {
t.Fatalf("Stoppend on wrong goroutine %d\n", n)
}
}
})
}

@ -17,10 +17,12 @@ import (
// a whole, and Status represents the last result of a `wait` call
// on this thread.
type Thread struct {
ID int // Thread ID or mach port
Status *WaitStatus // Status returned from last wait call
CurrentBreakpoint *Breakpoint // Breakpoint thread is currently stopped at
BreakpointConditionMet bool // Output of evaluating the breakpoint's condition
ID int // Thread ID or mach port
Status *WaitStatus // Status returned from last wait call
CurrentBreakpoint *Breakpoint // Breakpoint thread is currently stopped at
BreakpointConditionMet bool // Output of evaluating the breakpoint's condition
BreakpointConditionError error // Error evaluating the breakpoint's condition
dbp *Process
singleStepping bool
running bool
@ -362,7 +364,7 @@ func (thread *Thread) SetCurrentBreakpoint() error {
if err = thread.SetPC(bp.Addr); err != nil {
return err
}
thread.BreakpointConditionMet = bp.checkCondition(thread)
thread.BreakpointConditionMet, thread.BreakpointConditionError = bp.checkCondition(thread)
if thread.onTriggeredBreakpoint() {
if g, err := thread.GetG(); err == nil {
thread.CurrentBreakpoint.HitCount[g.ID]++
@ -390,7 +392,7 @@ func (thread *Thread) onRuntimeBreakpoint() bool {
}
// Returns true if this thread is on the goroutine requested by the current 'next' command
func (th *Thread) onNextGoroutine() bool {
func (th *Thread) onNextGoroutine() (bool, error) {
var bp *Breakpoint
for i := range th.dbp.Breakpoints {
if th.dbp.Breakpoints[i].Temp {
@ -398,7 +400,7 @@ func (th *Thread) onNextGoroutine() bool {
}
}
if bp == nil {
return false
return false, nil
}
return bp.checkCondition(th)
}

@ -172,7 +172,7 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
bp.Goroutine = requestedBp.Goroutine
bp.Stacktrace = requestedBp.Stacktrace
bp.Variables = requestedBp.Variables
bp.Cond = -1
bp.Cond = nil
createdBp = api.ConvertBreakpoint(bp)
log.Printf("created breakpoint: %#v", createdBp)
return createdBp, nil