proc/native: low level support for watchpoints in linux/amd64 (#2301)

Adds the low-level support for watchpoints (aka data breakpoints) to
the native linux/amd64 backend.

Does not add user interface or functioning support for watchpoints
on stack variables.

Updates #279
This commit is contained in:
Alessandro Arzilli 2021-05-06 19:33:56 +02:00 committed by GitHub
parent 1962c3a627
commit 58762685e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 636 additions and 26 deletions

@ -1,23 +1,30 @@
Tests skipped by each supported backend:
* 386 skipped = 2.7% (4/147)
* 386 skipped = 4% (6/149)
* 1 broken
* 3 broken - cgo stacktraces
* arm64 skipped = 2% (3/147)
* 2 not implemented
* arm64 skipped = 3.4% (5/149)
* 1 broken
* 1 broken - cgo stacktraces
* 1 broken - global variable symbolication
* darwin/arm64 skipped = 0.68% (1/147)
* 2 not implemented
* darwin skipped = 1.3% (2/149)
* 2 not implemented
* darwin/arm64 skipped = 0.67% (1/149)
* 1 broken - cgo stacktraces
* darwin/lldb skipped = 0.68% (1/147)
* darwin/lldb skipped = 0.67% (1/149)
* 1 upstream issue
* freebsd skipped = 8.2% (12/147)
* freebsd skipped = 9.4% (14/149)
* 11 broken
* 1 not implemented
* linux/386/pie skipped = 0.68% (1/147)
* 3 not implemented
* linux/386/pie skipped = 0.67% (1/149)
* 1 broken
* pie skipped = 0.68% (1/147)
* pie skipped = 0.67% (1/149)
* 1 upstream issue - https://github.com/golang/go/issues/29322
* windows skipped = 1.4% (2/147)
* rr skipped = 1.3% (2/149)
* 2 not implemented
* windows skipped = 2.7% (4/149)
* 1 broken
* 2 not implemented
* 1 upstream issue

@ -0,0 +1,31 @@
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var globalvar1 int
func demo(id int, wait *sync.WaitGroup) {
for i := 0; i < 100; i++ {
sleep := rand.Intn(10) + 1
fmt.Printf("id: %d step: %d sleeping %d\n", id, i, sleep)
globalvar1 = globalvar1 + 1
time.Sleep(time.Duration(sleep) * time.Millisecond)
}
wait.Done()
}
func main() {
wait := new(sync.WaitGroup)
wait.Add(1)
wait.Add(1)
go demo(1, wait)
go demo(2, wait)
wait.Wait()
}

34
_fixtures/databpeasy.go Normal file

@ -0,0 +1,34 @@
package main
import (
"fmt"
"runtime"
)
var globalvar1 = 0
var globalvar2 = 0
func main() { // Position 0
runtime.LockOSThread()
globalvar2 = 1
fmt.Printf("%d %d\n", globalvar1, globalvar2)
globalvar2 = globalvar1 + 1
globalvar1 = globalvar2 + 1
fmt.Printf("%d %d\n", globalvar1, globalvar2) // Position 1
runtime.Breakpoint()
globalvar2 = globalvar2 + 1 // Position 2
globalvar2 = globalvar1 + globalvar2 // Position 3
fmt.Printf("%d %d\n", globalvar1, globalvar2)
globalvar1 = globalvar2 + 1
fmt.Printf("%d %d\n", globalvar1, globalvar2)
runtime.Breakpoint()
done := make(chan struct{}) // Position 4
go f(done)
<-done
}
func f(done chan struct{}) {
runtime.LockOSThread()
globalvar1 = globalvar2 + 1
close(done) // Position 5
}

@ -0,0 +1,130 @@
package amd64util
import (
"errors"
"fmt"
)
// DebugRegisters represents x86 debug registers described in the Intel 64
// and IA-32 Architectures Software Developer's Manual, Vol. 3B, section
// 17.2
type DebugRegisters struct {
pAddrs [4]*uint64
pDR6, pDR7 *uint64
Dirty bool
}
func NewDebugRegisters(pDR0, pDR1, pDR2, pDR3, pDR6, pDR7 *uint64) *DebugRegisters {
return &DebugRegisters{
pAddrs: [4]*uint64{pDR0, pDR1, pDR2, pDR3},
pDR6: pDR6,
pDR7: pDR7,
Dirty: false,
}
}
func lenrwBitsOffset(idx uint8) uint8 {
return 16 + idx*4
}
func enableBitOffset(idx uint8) uint8 {
return idx * 2
}
func (drs *DebugRegisters) breakpoint(idx uint8) (addr uint64, read, write bool, sz int) {
enable := *(drs.pDR7) & (1 << enableBitOffset(idx))
if enable == 0 {
return 0, false, false, 0
}
addr = *(drs.pAddrs[idx])
lenrw := (*(drs.pDR7) >> lenrwBitsOffset(idx)) & 0xf
write = (lenrw & 0x1) != 0
read = (lenrw & 0x2) != 0
switch lenrw >> 2 {
case 0x0:
sz = 1
case 0x1:
sz = 2
case 0x2:
sz = 8 // sic
case 0x3:
sz = 4
}
return addr, read, write, sz
}
// SetBreakpoint sets hardware breakpoint at index 'idx' to the specified
// address, read/write flags and size.
// If the breakpoint is already in use but the parameters match it does
// nothing.
func (drs *DebugRegisters) SetBreakpoint(idx uint8, addr uint64, read, write bool, sz int) error {
if int(idx) >= len(drs.pAddrs) {
return fmt.Errorf("hardware breakpoints exhausted")
}
curaddr, curread, curwrite, cursz := drs.breakpoint(idx)
if curaddr != 0 {
if (curaddr != addr) || (curread != read) || (curwrite != write) || (cursz != sz) {
return fmt.Errorf("hardware breakpoint %d already in use (address %#x)", idx, curaddr)
}
// hardware breakpoint already set
return nil
}
if read && !write {
return errors.New("break on read only not supported")
}
*(drs.pAddrs[idx]) = addr
var lenrw uint64
if write {
lenrw |= 0x1
}
if read {
lenrw |= 0x2
}
switch sz {
case 1:
// already ok
case 2:
lenrw |= 0x1 << 2
case 4:
lenrw |= 0x3 << 2
case 8:
lenrw |= 0x2 << 2
default:
return fmt.Errorf("data breakpoint of size %d not supported", sz)
}
*(drs.pDR7) &^= (0xf << lenrwBitsOffset(idx)) // clear old settings
*(drs.pDR7) |= lenrw << lenrwBitsOffset(idx)
*(drs.pDR7) |= 1 << enableBitOffset(idx) // enable
drs.Dirty = true
return nil
}
// ClearBreakpoint disables the hardware breakpoint at index 'idx'. If the
// breakpoint was already disabled it does nothing.
func (drs *DebugRegisters) ClearBreakpoint(idx uint8) {
if *(drs.pDR7)&(1<<enableBitOffset(idx)) == 0 {
return
}
*(drs.pDR7) &^= (1 << enableBitOffset(idx))
drs.Dirty = true
}
// GetActiveBreakpoint returns the active hardware breakpoint and resets the
// condition flags.
func (drs *DebugRegisters) GetActiveBreakpoint() (ok bool, idx uint8) {
for idx := uint8(0); idx < 3; idx++ {
enable := *(drs.pDR7) & (1 << enableBitOffset(idx))
if enable == 0 {
continue
}
if *(drs.pDR6)&(1<<idx) != 0 {
*drs.pDR6 &^= 0xf // it is our responsibility to clear the condition bits
drs.Dirty = true
return true, idx
}
}
return false, 0
}

@ -5,6 +5,7 @@ import (
"fmt"
"go/ast"
"go/constant"
"go/parser"
"reflect"
)
@ -34,6 +35,9 @@ type Breakpoint struct {
Name string // User defined name of the breakpoint
LogicalID int // ID of the logical breakpoint that owns this physical breakpoint
WatchType WatchType
HWBreakIndex uint8 // hardware breakpoint index
// Kind describes whether this is an internal breakpoint (for next'ing or
// stepping).
// A single breakpoint can be both a UserBreakpoint and some kind of
@ -93,6 +97,36 @@ const (
StepBreakpoint
)
// WatchType is the watchpoint type
type WatchType uint8
const (
WatchRead WatchType = 1 << iota
WatchWrite
)
// Read returns true if the hardware breakpoint should trigger on memory reads.
func (wtype WatchType) Read() bool {
return wtype&WatchRead != 0
}
// Write returns true if the hardware breakpoint should trigger on memory writes.
func (wtype WatchType) Write() bool {
return wtype&WatchWrite != 0
}
// Size returns the size in bytes of the hardware breakpoint.
func (wtype WatchType) Size() int {
return int(wtype >> 4)
}
// withSize returns a new HWBreakType with the size set to the specified value
func (wtype WatchType) withSize(sz uint8) WatchType {
return WatchType((sz << 4) | uint8(wtype&0xf))
}
var ErrHWBreakUnsupported = errors.New("hardware breakpoints not implemented")
func (bp *Breakpoint) String() string {
return fmt.Sprintf("Breakpoint %d at %#v %s:%d (%d)", bp.LogicalID, bp.Addr, bp.File, bp.Line, bp.TotalHitCount)
}
@ -243,6 +277,49 @@ func NewBreakpointMap() BreakpointMap {
// SetBreakpoint sets a breakpoint at addr, and stores it in the process wide
// break point table.
func (t *Target) SetBreakpoint(addr uint64, kind BreakpointKind, cond ast.Expr) (*Breakpoint, error) {
return t.setBreakpointInternal(addr, kind, 0, cond)
}
// SetWatchpoint sets a data breakpoint at addr and stores it in the
// process wide break point table.
func (t *Target) SetWatchpoint(scope *EvalScope, expr string, wtype WatchType, cond ast.Expr) (*Breakpoint, error) {
if (wtype&WatchWrite == 0) && (wtype&WatchRead == 0) {
return nil, errors.New("at least one of read and write must be set for watchpoint")
}
n, err := parser.ParseExpr(expr)
if err != nil {
return nil, err
}
xv, err := scope.evalAST(n)
if err != nil {
return nil, err
}
if xv.Addr == 0 || xv.Flags&VariableFakeAddress != 0 || xv.DwarfType == nil {
return nil, fmt.Errorf("can not watch %q", expr)
}
if xv.Unreadable != nil {
return nil, fmt.Errorf("expression %q is unreadable: %v", expr, xv.Unreadable)
}
if xv.Kind == reflect.UnsafePointer || xv.Kind == reflect.Invalid {
return nil, fmt.Errorf("can not watch variable of type %s", xv.Kind.String())
}
sz := xv.DwarfType.Size()
if sz <= 0 || sz > int64(t.BinInfo().Arch.PtrSize()) {
//TODO(aarzilli): it is reasonable to expect to be able to watch string
//and interface variables and we could support it by watching certain
//member fields here.
return nil, fmt.Errorf("can not watch variable of type %s", xv.DwarfType.String())
}
if xv.Addr >= scope.g.stack.lo && xv.Addr < scope.g.stack.hi {
//TODO(aarzilli): support watching stack variables
return nil, errors.New("can not watch stack allocated variable")
}
return t.setBreakpointInternal(xv.Addr, UserBreakpoint, wtype.withSize(uint8(sz)), cond)
}
func (t *Target) setBreakpointInternal(addr uint64, kind BreakpointKind, wtype WatchType, cond ast.Expr) (*Breakpoint, error) {
if valid, err := t.Valid(); !valid {
return nil, err
}
@ -270,8 +347,25 @@ func (t *Target) SetBreakpoint(addr uint64, kind BreakpointKind, cond ast.Expr)
fnName = fn.Name
}
hwidx := uint8(0)
if wtype != 0 {
m := make(map[uint8]bool)
for _, bp := range bpmap.M {
if bp.WatchType != 0 {
m[bp.HWBreakIndex] = true
}
}
for hwidx = 0; true; hwidx++ {
if !m[hwidx] {
break
}
}
}
newBreakpoint := &Breakpoint{
FunctionName: fnName,
WatchType: wtype,
HWBreakIndex: hwidx,
File: f,
Line: l,
Addr: addr,
@ -372,6 +466,16 @@ func (bpmap *BreakpointMap) HasInternalBreakpoints() bool {
return false
}
// HasHWBreakpoints returns true if there are hardware breakpoints.
func (bpmap *BreakpointMap) HasHWBreakpoints() bool {
for _, bp := range bpmap.M {
if bp.WatchType != 0 {
return true
}
}
return false
}
// BreakpointState describes the state of a breakpoint in a thread.
type BreakpointState struct {
*Breakpoint

@ -1209,6 +1209,9 @@ func (p *gdbProcess) FindBreakpoint(pc uint64) (*proc.Breakpoint, bool) {
}
func (p *gdbProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
if bp.WatchType != 0 {
return errors.New("hardware breakpoints not supported")
}
return p.conn.setBreakpoint(bp.Addr, p.breakpointKind)
}

@ -114,6 +114,18 @@ func (t *nativeThread) restoreRegisters(sr proc.Registers) error {
panic(ErrNativeBackendDisabled)
}
func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
panic(ErrNativeBackendDisabled)
}
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
panic(ErrNativeBackendDisabled)
}
func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
panic(ErrNativeBackendDisabled)
}
// Stopped returns whether the thread is stopped at
// the operating system level.
func (t *nativeThread) Stopped() bool {

@ -206,6 +206,16 @@ func (dbp *nativeProcess) CheckAndClearManualStopRequest() bool {
}
func (dbp *nativeProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
if bp.WatchType != 0 {
for _, thread := range dbp.threads {
err := thread.writeHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
if err != nil {
return err
}
}
return nil
}
bp.OriginalData = make([]byte, dbp.bi.Arch.BreakpointSize())
_, err := dbp.memthread.ReadMemory(bp.OriginalData, bp.Addr)
if err != nil {
@ -215,7 +225,17 @@ func (dbp *nativeProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
}
func (dbp *nativeProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
return dbp.memthread.ClearBreakpoint(bp)
if bp.WatchType != 0 {
for _, thread := range dbp.threads {
err := thread.clearHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
if err != nil {
return err
}
}
return nil
}
return dbp.memthread.clearSoftwareBreakpoint(bp)
}
// ContinueOnce will continue the target until it stops.

@ -268,6 +268,14 @@ func (dbp *nativeProcess) addThread(tid int, attach bool) (*nativeThread, error)
if dbp.memthread == nil {
dbp.memthread = dbp.threads[tid]
}
for _, bp := range dbp.Breakpoints().M {
if bp.WatchType != 0 {
err := dbp.threads[tid].writeHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
if err != nil {
return nil, err
}
}
}
return dbp.threads[tid], nil
}

@ -53,6 +53,17 @@ func (t *nativeThread) StepInstruction() (err error) {
defer func() {
t.singleStepping = false
}()
if bp := t.CurrentBreakpoint.Breakpoint; bp != nil && bp.WatchType != 0 && t.dbp.Breakpoints().M[bp.Addr] == bp {
err = t.clearHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
if err != nil {
return err
}
defer func() {
err = t.writeHardwareBreakpoint(bp.Addr, bp.WatchType, bp.HWBreakIndex)
}()
}
pc, err := t.PC()
if err != nil {
return err
@ -61,7 +72,7 @@ func (t *nativeThread) StepInstruction() (err error) {
bp, ok := t.dbp.FindBreakpoint(pc, false)
if ok {
// Clear the breakpoint so that we can continue execution.
err = t.ClearBreakpoint(bp)
err = t.clearSoftwareBreakpoint(bp)
if err != nil {
return err
}
@ -109,23 +120,40 @@ func (t *nativeThread) Common() *proc.CommonThread {
// thread is stopped at as CurrentBreakpoint on the thread struct.
func (t *nativeThread) SetCurrentBreakpoint(adjustPC bool) error {
t.CurrentBreakpoint.Clear()
pc, err := t.PC()
if err != nil {
return err
var bp *proc.Breakpoint
if t.dbp.Breakpoints().HasHWBreakpoints() {
var err error
bp, err = t.findHardwareBreakpoint()
if err != nil {
return err
}
}
if bp == nil {
pc, err := t.PC()
if err != nil {
return err
}
// If the breakpoint instruction does not change the value
// of PC after being executed we should look for breakpoints
// with bp.Addr == PC and there is no need to call SetPC
// after finding one.
adjustPC = adjustPC && t.BinInfo().Arch.BreakInstrMovesPC()
// If the breakpoint instruction does not change the value
// of PC after being executed we should look for breakpoints
// with bp.Addr == PC and there is no need to call SetPC
// after finding one.
adjustPC = adjustPC && t.BinInfo().Arch.BreakInstrMovesPC()
if bp, ok := t.dbp.FindBreakpoint(pc, adjustPC); ok {
if adjustPC {
if err = t.setPC(bp.Addr); err != nil {
return err
var ok bool
bp, ok = t.dbp.FindBreakpoint(pc, adjustPC)
if ok {
if adjustPC {
if err = t.setPC(bp.Addr); err != nil {
return err
}
}
}
}
if bp != nil {
t.CurrentBreakpoint = bp.CheckCondition(t)
if t.CurrentBreakpoint.Breakpoint != nil && t.CurrentBreakpoint.Active {
if g, err := proc.GetG(t); err == nil {
@ -148,8 +176,8 @@ func (t *nativeThread) ThreadID() int {
return t.ID
}
// ClearBreakpoint clears the specified breakpoint.
func (t *nativeThread) ClearBreakpoint(bp *proc.Breakpoint) error {
// clearSoftwareBreakpoint clears the specified breakpoint.
func (t *nativeThread) clearSoftwareBreakpoint(bp *proc.Breakpoint) error {
if _, err := t.WriteMemory(bp.Addr, bp.OriginalData); err != nil {
return fmt.Errorf("could not clear breakpoint %s", err)
}

@ -132,3 +132,15 @@ func (t *nativeThread) ReadMemory(buf []byte, addr uint64) (int, error) {
func (t *nativeThread) restoreRegisters(sr proc.Registers) error {
return errors.New("not implemented")
}
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
return nil, nil
}

@ -120,3 +120,15 @@ func (t *nativeThread) ReadMemory(data []byte, addr uint64) (n int, err error) {
t.dbp.execPtraceFunc(func() { n, err = ptraceReadData(t.ID, uintptr(addr), data) })
return n, err
}
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
return nil, nil
}

@ -8,3 +8,15 @@ import (
func (t *nativeThread) restoreRegisters(savedRegs proc.Registers) error {
return fmt.Errorf("restore regs not supported on i386")
}
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
return nil, nil
}

@ -7,6 +7,7 @@ import (
sys "golang.org/x/sys/unix"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/amd64util"
"github.com/go-delve/delve/pkg/proc/linutil"
)
@ -45,3 +46,76 @@ func (t *nativeThread) restoreRegisters(savedRegs proc.Registers) error {
}
return restoreRegistersErr
}
const debugRegUserOffset = 848 // offset of debug registers in the user struct, see source/arch/x86/kernel/ptrace.c
func (t *nativeThread) withDebugRegisters(f func(*amd64util.DebugRegisters) error) error {
var err error
t.dbp.execPtraceFunc(func() {
debugregs := make([]uint64, 8)
for i := range debugregs {
if i == 4 || i == 5 {
continue
}
_, _, err = sys.Syscall6(sys.SYS_PTRACE, sys.PTRACE_PEEKUSR, uintptr(t.ID), uintptr(debugRegUserOffset+uintptr(i)*unsafe.Sizeof(debugregs[0])), uintptr(unsafe.Pointer(&debugregs[i])), 0, 0)
if err != nil && err != syscall.Errno(0) {
return
}
}
drs := amd64util.NewDebugRegisters(&debugregs[0], &debugregs[1], &debugregs[2], &debugregs[3], &debugregs[6], &debugregs[7])
err = f(drs)
if drs.Dirty {
for i := range debugregs {
if i == 4 || i == 5 {
// Linux will return EIO for DR4 and DR5
continue
}
_, _, err = sys.Syscall6(sys.SYS_PTRACE, sys.PTRACE_POKEUSR, uintptr(t.ID), uintptr(debugRegUserOffset+uintptr(i)*unsafe.Sizeof(debugregs[0])), uintptr(debugregs[i]), 0, 0)
if err != nil && err != syscall.Errno(0) {
return
}
}
}
})
if err == syscall.Errno(0) || err == sys.ESRCH {
err = nil
}
return err
}
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return t.withDebugRegisters(func(drs *amd64util.DebugRegisters) error {
return drs.SetBreakpoint(idx, addr, wtype.Read(), wtype.Write(), wtype.Size())
})
}
func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return t.withDebugRegisters(func(drs *amd64util.DebugRegisters) error {
drs.ClearBreakpoint(idx)
return nil
})
}
func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
var retbp *proc.Breakpoint
err := t.withDebugRegisters(func(drs *amd64util.DebugRegisters) error {
ok, idx := drs.GetActiveBreakpoint()
if ok {
for _, bp := range t.dbp.Breakpoints().M {
if bp.WatchType != 0 && bp.HWBreakIndex == idx {
retbp = bp
break
}
}
}
return nil
})
if err != nil {
return nil, err
}
return retbp, nil
}

@ -42,3 +42,15 @@ func (t *nativeThread) restoreRegisters(savedRegs proc.Registers) error {
}
return restoreRegistersErr
}
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
return nil, nil
}

@ -155,3 +155,15 @@ func (t *nativeThread) ReadMemory(buf []byte, addr uint64) (int, error) {
func (t *nativeThread) restoreRegisters(savedRegs proc.Registers) error {
return _SetThreadContext(t.os.hThread, savedRegs.(*winutil.AMD64Registers).Context)
}
func (t *nativeThread) writeHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) clearHardwareBreakpoint(addr uint64, wtype proc.WatchType, idx uint8) error {
return proc.ErrHWBreakUnsupported
}
func (t *nativeThread) findHardwareBreakpoint() (*proc.Breakpoint, error) {
return nil, nil
}

@ -5216,3 +5216,94 @@ func TestVariablesWithExternalLinking(t *testing.T) {
}
})
}
func TestWatchpointsBasic(t *testing.T) {
skipOn(t, "not implemented", "windows")
skipOn(t, "not implemented", "freebsd")
skipOn(t, "not implemented", "darwin")
skipOn(t, "not implemented", "386")
skipOn(t, "not implemented", "arm64")
skipOn(t, "not implemented", "rr")
withTestProcess("databpeasy", t, func(p *proc.Target, fixture protest.Fixture) {
setFunctionBreakpoint(p, t, "main.main")
assertNoError(p.Continue(), t, "Continue 0")
assertLineNumber(p, t, 11, "Continue 0") // Position 0
scope, err := proc.GoroutineScope(p.CurrentThread())
assertNoError(err, t, "GoroutineScope")
bp, err := p.SetWatchpoint(scope, "globalvar1", proc.WatchWrite, nil)
assertNoError(err, t, "SetDataBreakpoint(write-only)")
assertNoError(p.Continue(), t, "Continue 1")
assertLineNumber(p, t, 17, "Continue 1") // Position 1
p.ClearBreakpoint(bp.Addr)
assertNoError(p.Continue(), t, "Continue 2")
assertLineNumber(p, t, 19, "Continue 2") // Position 2
_, err = p.SetWatchpoint(scope, "globalvar1", proc.WatchWrite|proc.WatchRead, nil)
assertNoError(err, t, "SetDataBreakpoint(read-write)")
assertNoError(p.Continue(), t, "Continue 3")
assertLineNumber(p, t, 20, "Continue 3") // Position 3
p.ClearBreakpoint(bp.Addr)
assertNoError(p.Continue(), t, "Continue 4")
assertLineNumber(p, t, 25, "Continue 4") // Position 4
_, err = p.SetWatchpoint(scope, "globalvar1", proc.WatchWrite, nil)
assertNoError(err, t, "SetDataBreakpoint(write-only, again)")
assertNoError(p.Continue(), t, "Continue 5")
assertLineNumber(p, t, 33, "Continue 5") // Position 5
})
}
func TestWatchpointCounts(t *testing.T) {
skipOn(t, "not implemented", "windows")
skipOn(t, "not implemented", "freebsd")
skipOn(t, "not implemented", "darwin")
skipOn(t, "not implemented", "386")
skipOn(t, "not implemented", "arm64")
skipOn(t, "not implemented", "rr")
protest.AllowRecording(t)
withTestProcess("databpcountstest", t, func(p *proc.Target, fixture protest.Fixture) {
setFunctionBreakpoint(p, t, "main.main")
assertNoError(p.Continue(), t, "Continue 0")
scope, err := proc.GoroutineScope(p.CurrentThread())
assertNoError(err, t, "GoroutineScope")
bp, err := p.SetWatchpoint(scope, "globalvar1", proc.WatchWrite, nil)
assertNoError(err, t, "SetWatchpoint(write-only)")
for {
if err := p.Continue(); err != nil {
if _, exited := err.(proc.ErrProcessExited); exited {
break
}
assertNoError(err, t, "Continue()")
}
}
t.Logf("TotalHitCount: %d", bp.TotalHitCount)
if bp.TotalHitCount != 200 {
t.Fatalf("Wrong TotalHitCount for the breakpoint (%d)", bp.TotalHitCount)
}
if len(bp.HitCount) != 2 {
t.Fatalf("Wrong number of goroutines for breakpoint (%d)", len(bp.HitCount))
}
for _, v := range bp.HitCount {
if v != 100 {
t.Fatalf("Wrong HitCount for breakpoint (%v)", bp.HitCount)
}
}
})
}

@ -101,6 +101,8 @@ func (sr StopReason) String() string {
return "next finished"
case StopCallReturned:
return "call returned"
case StopWatchpoint:
return "watchpoint"
default:
return ""
}
@ -116,6 +118,7 @@ const (
StopManual // A manual stop was requested
StopNextFinished // The next/step/stepout command terminated
StopCallReturned // An injected call completed
StopWatchpoint // The target process hit one or more watchpoints
)
// NewTargetConfig contains the configuration for a new Target object,

@ -201,6 +201,9 @@ func (dbp *Target) Continue() error {
dbp.ClearInternalBreakpoints()
}
dbp.StopReason = StopBreakpoint
if curbp.Breakpoint.WatchType != 0 {
dbp.StopReason = StopWatchpoint
}
return conditionErrors(threads)
default:
// not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat
@ -413,11 +416,11 @@ func (dbp *Target) StepInstruction() (err error) {
if ok, err := dbp.Valid(); !ok {
return err
}
thread.Breakpoint().Clear()
err = thread.StepInstruction()
if err != nil {
return err
}
thread.Breakpoint().Clear()
err = thread.SetCurrentBreakpoint(true)
if err != nil {
return err

@ -1938,6 +1938,8 @@ func (s *Server) doCommand(command string, asyncSetupDone chan struct{}) {
stopped.Body.Reason = "pause"
case proc.StopUnknown: // can happen while stopping
stopped.Body.Reason = "unknown"
case proc.StopWatchpoint:
stopped.Body.Reason = "data breakpoint"
default:
stopped.Body.Reason = "breakpoint"
}