proc/gdbserver: support watchpoints (#2659)

Adds watchpoint support to gdbserver backend for rr debugger and
debugserver on macOS/amd64 and macOS/arm64.

Also changes stack watchpoints to support reverse execution.
This commit is contained in:
Alessandro Arzilli 2021-10-04 23:45:05 +02:00 committed by GitHub
parent dc2f615c9a
commit 348c722981
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 257 additions and 74 deletions

@ -4,12 +4,9 @@ Tests skipped by each supported backend:
* 1 broken
* 3 broken - cgo stacktraces
* 3 not implemented
* arm64 skipped = 5
* arm64 skipped = 2
* 1 broken
* 1 broken - global variable symbolication
* 3 not implemented
* darwin skipped = 3
* 3 not implemented
* darwin/arm64 skipped = 1
* 1 broken - cgo stacktraces
* darwin/lldb skipped = 1
@ -19,12 +16,11 @@ Tests skipped by each supported backend:
* 4 not implemented
* linux/386/pie skipped = 1
* 1 broken
* linux/arm64 skipped = 1
* linux/arm64 skipped = 4
* 1 broken - cgo stacktraces
* 3 not implemented
* pie skipped = 2
* 2 upstream issue - https://github.com/golang/go/issues/29322
* rr skipped = 3
* 3 not implemented
* windows skipped = 2
* 1 broken
* 1 upstream issue

@ -669,6 +669,8 @@ The memory location is specified with the same expression language used by 'prin
will watch the address of variable 'v'.
Note that writes that do not change the value of the watched memory address might not be reported.
See also: "help print".

@ -15,13 +15,13 @@ func main() { // Position 0
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
@ -29,6 +29,6 @@ func main() { // Position 0
func f(done chan struct{}) {
runtime.LockOSThread()
globalvar1 = globalvar2 + 1
globalvar1 = globalvar2 + 2
close(done) // Position 5
}

@ -2,12 +2,12 @@ package main
import (
"fmt"
"runtime"
//
)
func f() {
w := 0
runtime.Breakpoint()
g(1000, &w) // Position 0
}

@ -176,6 +176,7 @@ type gdbThread struct {
p *gdbProcess
sig uint8 // signal received by thread after last stop
setbp bool // thread was stopped because of a breakpoint
watchAddr uint64 // if > 0 this is the watchpoint address
common proc.CommonThread
}
@ -225,6 +226,7 @@ func newProcess(process *os.Process) *gdbProcess {
inbuf: make([]byte, 0, initialInputBufferSize),
direction: proc.Forward,
log: logger,
goarch: runtime.GOARCH,
},
threads: make(map[int]*gdbThread),
bi: proc.NewBinaryInfo(runtime.GOOS, runtime.GOARCH),
@ -822,10 +824,9 @@ func (p *gdbProcess) ContinueOnce() (proc.Thread, proc.StopReason, error) {
var atstart bool
continueLoop:
for {
var err error
var sig uint8
tu.Reset()
threadID, sig, err = p.conn.resume(p.threads, &tu)
sp, err := p.conn.resume(p.threads, &tu)
threadID = sp.threadID
if err != nil {
if _, exited := err.(proc.ErrProcessExited); exited {
p.exited = true
@ -842,7 +843,8 @@ continueLoop:
if trapthread != nil && !p.threadStopInfo {
// For stubs that do not support qThreadStopInfo we manually set the
// reason the thread returned by resume() stopped.
trapthread.sig = sig
trapthread.sig = sp.sig
trapthread.watchAddr = sp.watchAddr
}
var shouldStop bool
@ -1075,7 +1077,7 @@ func (p *gdbProcess) Restart(pos string) (proc.Thread, error) {
// for some reason we have to send a vCont;c after a vRun to make rr behave
// properly, because that's what gdb does.
_, _, err = p.conn.resume(nil, nil)
_, err = p.conn.resume(nil, nil)
if err != nil {
return nil, err
}
@ -1087,8 +1089,8 @@ func (p *gdbProcess) Restart(pos string) (proc.Thread, error) {
p.clearThreadSignals()
p.clearThreadRegisters()
for addr := range p.breakpoints.M {
p.conn.setBreakpoint(addr, p.breakpointKind)
for _, bp := range p.breakpoints.M {
p.WriteBreakpoint(bp)
}
return p.currentThread, p.setCurrentBreakpoints()
@ -1224,15 +1226,33 @@ func (p *gdbProcess) FindBreakpoint(pc uint64) (*proc.Breakpoint, bool) {
return nil, false
}
func (p *gdbProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
if bp.WatchType != 0 {
return errors.New("hardware breakpoints not supported")
func watchTypeToBreakpointType(wtype proc.WatchType) breakpointType {
switch {
case wtype.Read() && wtype.Write():
return accessWatchpoint
case wtype.Write():
return writeWatchpoint
case wtype.Read():
return readWatchpoint
default:
return swBreakpoint
}
return p.conn.setBreakpoint(bp.Addr, p.breakpointKind)
}
func (p *gdbProcess) WriteBreakpoint(bp *proc.Breakpoint) error {
kind := p.breakpointKind
if bp.WatchType != 0 {
kind = bp.WatchType.Size()
}
return p.conn.setBreakpoint(bp.Addr, watchTypeToBreakpointType(bp.WatchType), kind)
}
func (p *gdbProcess) EraseBreakpoint(bp *proc.Breakpoint) error {
return p.conn.clearBreakpoint(bp.Addr, p.breakpointKind)
kind := p.breakpointKind
if bp.WatchType != 0 {
kind = bp.WatchType.Size()
}
return p.conn.clearBreakpoint(bp.Addr, watchTypeToBreakpointType(bp.WatchType), kind)
}
type threadUpdater struct {
@ -1324,7 +1344,7 @@ func (p *gdbProcess) updateThreadList(tu *threadUpdater) error {
for _, th := range p.threads {
if p.threadStopInfo {
sig, reason, err := p.conn.threadStopInfo(th.strID)
sp, err := p.conn.threadStopInfo(th.strID)
if err != nil {
if isProtocolErrorUnsupported(err) {
p.threadStopInfo = false
@ -1332,10 +1352,12 @@ func (p *gdbProcess) updateThreadList(tu *threadUpdater) error {
}
return err
}
th.setbp = (reason == "breakpoint" || (reason == "" && sig == breakpointSignal))
th.sig = sig
th.setbp = (sp.reason == "breakpoint" || (sp.reason == "" && sp.sig == breakpointSignal) || (sp.watchAddr > 0))
th.sig = sp.sig
th.watchAddr = sp.watchAddr
} else {
th.sig = 0
th.watchAddr = 0
}
}
@ -1452,12 +1474,12 @@ func (t *gdbThread) Common() *proc.CommonThread {
// StepInstruction will step exactly 1 CPU instruction.
func (t *gdbThread) StepInstruction() error {
pc := t.regs.PC()
if _, atbp := t.p.breakpoints.M[pc]; atbp {
err := t.p.conn.clearBreakpoint(pc, t.p.breakpointKind)
if bp, atbp := t.p.breakpoints.M[pc]; atbp && bp.WatchType == 0 {
err := t.p.conn.clearBreakpoint(pc, swBreakpoint, t.p.breakpointKind)
if err != nil {
return err
}
defer t.p.conn.setBreakpoint(pc, t.p.breakpointKind)
defer t.p.conn.setBreakpoint(pc, swBreakpoint, t.p.breakpointKind)
}
// Reset thread registers so the next call to
// Thread.Registers will not be cached.
@ -1677,13 +1699,16 @@ func (t *gdbThread) reloadGAtPC() error {
// around by clearing and re-setting the breakpoint in a specific sequence
// with the memory writes.
// Additionally all breakpoints in [pc, pc+len(movinstr)] need to be removed
for addr := range t.p.breakpoints.M {
for addr, bp := range t.p.breakpoints.M {
if bp.WatchType != 0 {
continue
}
if addr >= pc && addr <= pc+uint64(len(movinstr)) {
err := t.p.conn.clearBreakpoint(addr, t.p.breakpointKind)
err := t.p.conn.clearBreakpoint(addr, swBreakpoint, t.p.breakpointKind)
if err != nil {
return err
}
defer t.p.conn.setBreakpoint(addr, t.p.breakpointKind)
defer t.p.conn.setBreakpoint(addr, swBreakpoint, t.p.breakpointKind)
}
}
@ -1795,6 +1820,13 @@ func (t *gdbThread) SetCurrentBreakpoint(adjustPC bool) error {
// adjustPC is ignored, it is the stub's responsibiility to set the PC
// address correctly after hitting a breakpoint.
t.clearBreakpointState()
if t.watchAddr > 0 {
t.CurrentBreakpoint.Breakpoint = t.p.Breakpoints().M[t.watchAddr]
if t.CurrentBreakpoint.Breakpoint == nil {
return fmt.Errorf("could not find watchpoint at address %#x", t.watchAddr)
}
return nil
}
regs, err := t.Registers()
if err != nil {
return err

@ -45,6 +45,7 @@ type gdbConn struct {
threadSuffixSupported bool // thread suffix supported by stub
isDebugserver bool // true if the stub is debugserver
xcmdok bool // x command can be used to transfer memory
goarch string
log *logrus.Entry
}
@ -404,18 +405,28 @@ func (conn *gdbConn) qXfer(kind, annex string, binary bool) ([]byte, error) {
return out, nil
}
type breakpointType uint8
const (
swBreakpoint breakpointType = 0
hwBreakpoint breakpointType = 1
writeWatchpoint breakpointType = 2
readWatchpoint breakpointType = 3
accessWatchpoint breakpointType = 4
)
// setBreakpoint executes a 'Z' (insert breakpoint) command of type '0' and kind '1' or '4'
func (conn *gdbConn) setBreakpoint(addr uint64, kind int) error {
func (conn *gdbConn) setBreakpoint(addr uint64, typ breakpointType, kind int) error {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$Z0,%x,%d", addr, kind)
fmt.Fprintf(&conn.outbuf, "$Z%d,%x,%d", typ, addr, kind)
_, err := conn.exec(conn.outbuf.Bytes(), "set breakpoint")
return err
}
// clearBreakpoint executes a 'z' (remove breakpoint) command of type '0' and kind '1' or '4'
func (conn *gdbConn) clearBreakpoint(addr uint64, kind int) error {
func (conn *gdbConn) clearBreakpoint(addr uint64, typ breakpointType, kind int) error {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$z0,%x,%d", addr, kind)
fmt.Fprintf(&conn.outbuf, "$z%d,%x,%d", typ, addr, kind)
_, err := conn.exec(conn.outbuf.Bytes(), "clear breakpoint")
return err
}
@ -537,7 +548,7 @@ func (conn *gdbConn) writeRegister(threadID string, regnum int, data []byte) err
// resume each thread. If a thread has sig == 0 the 'c' action will be used,
// otherwise the 'C' action will be used and the value of sig will be passed
// to it.
func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (string, uint8, error) {
func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stopPacket, error) {
if conn.direction == proc.Forward {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$vCont")
@ -549,7 +560,7 @@ func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stri
fmt.Fprintf(&conn.outbuf, ";c")
} else {
if err := conn.selectThread('c', "p-1.-1", "resume"); err != nil {
return "", 0, err
return stopPacket{}, err
}
conn.outbuf.Reset()
fmt.Fprint(&conn.outbuf, "$bc")
@ -557,7 +568,7 @@ func (conn *gdbConn) resume(threads map[int]*gdbThread, tu *threadUpdater) (stri
conn.manualStopMutex.Lock()
if err := conn.send(conn.outbuf.Bytes()); err != nil {
conn.manualStopMutex.Unlock()
return "", 0, err
return stopPacket{}, err
}
conn.running = true
conn.manualStopMutex.Unlock()
@ -584,7 +595,7 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
if err := conn.send(conn.outbuf.Bytes()); err != nil {
return err
}
_, _, err := conn.waitForvContStop("singlestep", threadID, tu)
_, err := conn.waitForvContStop("singlestep", threadID, tu)
return err
}
var sig uint8 = 0
@ -601,8 +612,8 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
if tu != nil {
tu.Reset()
}
var err error
_, sig, err = conn.waitForvContStop("singlestep", threadID, tu)
sp, err := conn.waitForvContStop("singlestep", threadID, tu)
sig = sp.sig
if err != nil {
return err
}
@ -626,7 +637,7 @@ func (conn *gdbConn) step(threadID string, tu *threadUpdater, ignoreFaultSignal
var errThreadBlocked = errors.New("thread blocked")
func (conn *gdbConn) waitForvContStop(context string, threadID string, tu *threadUpdater) (string, uint8, error) {
func (conn *gdbConn) waitForvContStop(context, threadID string, tu *threadUpdater) (stopPacket, error) {
count := 0
failed := false
for {
@ -647,13 +658,13 @@ func (conn *gdbConn) waitForvContStop(context string, threadID string, tu *threa
}
count++
} else if failed {
return "", 0, errThreadBlocked
return stopPacket{}, errThreadBlocked
} else if err != nil {
return "", 0, err
return stopPacket{}, err
} else {
repeat, sp, err := conn.parseStopPacket(resp, threadID, tu)
if !repeat {
return sp.threadID, sp.sig, err
return sp, err
}
}
}
@ -663,8 +674,20 @@ type stopPacket struct {
threadID string
sig uint8
reason string
watchAddr uint64
}
// Mach exception codes used to decode metype/medata keys in stop packets (necessary to support watchpoints with debugserver).
// See:
// https://opensource.apple.com/source/xnu/xnu-4570.1.46/osfmk/mach/exception_types.h.auto.html
// https://opensource.apple.com/source/xnu/xnu-4570.1.46/osfmk/mach/i386/exception.h.auto.html
// https://opensource.apple.com/source/xnu/xnu-4570.1.46/osfmk/mach/arm/exception.h.auto.html
const (
_EXC_BREAKPOINT = 6 // mach exception type for hardware breakpoints
_EXC_I386_SGL = 1 // mach exception code for single step on x86, for some reason this is also used for watchpoints
_EXC_ARM_DA_DEBUG = 0x102 // mach exception code for debug fault on arm/arm64
)
// executes 'vCont' (continue/step) command
func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpdater) (repeat bool, sp stopPacket, err error) {
switch resp[0] {
@ -683,6 +706,9 @@ func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpd
conn.log.Debugf("full stop packet: %s", string(resp))
}
var metype int
var medata = make([]uint64, 0, 10)
buf := resp[3:]
for buf != nil {
colon := bytes.Index(buf, []byte{':'})
@ -712,6 +738,32 @@ func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpd
}
case "reason":
sp.reason = string(value)
case "watch", "awatch", "rwatch":
sp.watchAddr, err = strconv.ParseUint(string(value), 16, 64)
if err != nil {
return false, stopPacket{}, fmt.Errorf("malformed stop packet: %s (wrong watch address)", string(resp))
}
case "metype":
// mach exception type (debugserver extension)
metype, _ = strconv.Atoi(string(value))
case "medata":
// mach exception data (debugserver extension)
d, _ := strconv.ParseUint(string(value), 16, 64)
medata = append(medata, d)
}
}
// Debugserver does not report watchpoint stops in the standard way preferring
// instead the semi-undocumented metype/medata keys.
// These values also have different meanings depending on the CPU architecture.
switch conn.goarch {
case "amd64":
if metype == _EXC_BREAKPOINT && len(medata) >= 2 && medata[0] == _EXC_I386_SGL {
sp.watchAddr = medata[1] // this should be zero if this is really a single step stop and non-zero for watchpoints
}
case "arm64":
if metype == _EXC_BREAKPOINT && len(medata) >= 2 && medata[0] == _EXC_ARM_DA_DEBUG {
sp.watchAddr = medata[1]
}
}
@ -967,18 +1019,18 @@ func (conn *gdbConn) allocMemory(sz uint64) (uint64, error) {
// threadStopInfo executes a 'qThreadStopInfo' and returns the reason the
// thread stopped.
func (conn *gdbConn) threadStopInfo(threadID string) (sig uint8, reason string, err error) {
func (conn *gdbConn) threadStopInfo(threadID string) (sp stopPacket, err error) {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$qThreadStopInfo%s", threadID)
resp, err := conn.exec(conn.outbuf.Bytes(), "thread stop info")
if err != nil {
return 0, "", err
return stopPacket{}, err
}
_, sp, err := conn.parseStopPacket(resp, "", nil)
_, sp, err = conn.parseStopPacket(resp, "", nil)
if err != nil {
return 0, "", err
return stopPacket{}, err
}
return sp.sig, sp.reason, nil
return sp, nil
}
// restart executes a 'vRun' command.

@ -5364,13 +5364,22 @@ func TestVariablesWithExternalLinking(t *testing.T) {
func TestWatchpointsBasic(t *testing.T) {
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")
skipOn(t, "not implemented", "linux", "arm64")
protest.AllowRecording(t)
position1 := 17
position5 := 33
if runtime.GOARCH == "arm64" {
position1 = 16
position5 = 32
}
withTestProcess("databpeasy", t, func(p *proc.Target, fixture protest.Fixture) {
setFunctionBreakpoint(p, t, "main.main")
setFileBreakpoint(p, t, fixture.Source, 19) // Position 2 breakpoint
setFileBreakpoint(p, t, fixture.Source, 25) // Position 4 breakpoint
assertNoError(p.Continue(), t, "Continue 0")
assertLineNumber(p, t, 11, "Continue 0") // Position 0
@ -5381,7 +5390,11 @@ func TestWatchpointsBasic(t *testing.T) {
assertNoError(err, t, "SetDataBreakpoint(write-only)")
assertNoError(p.Continue(), t, "Continue 1")
assertLineNumber(p, t, 17, "Continue 1") // Position 1
assertLineNumber(p, t, position1, "Continue 1") // Position 1
if curbp := p.CurrentThread().Breakpoint().Breakpoint; curbp == nil || (curbp.LogicalID() != bp.LogicalID()) {
t.Fatal("breakpoint not set")
}
p.ClearBreakpoint(bp.Addr)
@ -5399,22 +5412,21 @@ func TestWatchpointsBasic(t *testing.T) {
assertNoError(p.Continue(), t, "Continue 4")
assertLineNumber(p, t, 25, "Continue 4") // Position 4
t.Logf("setting final breakpoint")
_, 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
assertLineNumber(p, t, position5, "Continue 5") // Position 5
})
}
func TestWatchpointCounts(t *testing.T) {
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")
skipOn(t, "not implemented", "linux", "arm64")
protest.AllowRecording(t)
withTestProcess("databpcountstest", t, func(p *proc.Target, fixture protest.Fixture) {
setFunctionBreakpoint(p, t, "main.main")
assertNoError(p.Continue(), t, "Continue 0")
@ -5526,12 +5538,18 @@ func TestDwrapStartLocation(t *testing.T) {
func TestWatchpointStack(t *testing.T) {
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")
skipOn(t, "not implemented", "linux", "arm64")
protest.AllowRecording(t)
position1 := 17
if runtime.GOARCH == "arm64" {
position1 = 16
}
withTestProcess("databpstack", t, func(p *proc.Target, fixture protest.Fixture) {
setFileBreakpoint(p, t, fixture.Source, 11) // Position 0 breakpoint
clearlen := len(p.Breakpoints().M)
assertNoError(p.Continue(), t, "Continue 0")
@ -5543,8 +5561,13 @@ func TestWatchpointStack(t *testing.T) {
_, err = p.SetWatchpoint(scope, "w", proc.WatchWrite, nil)
assertNoError(err, t, "SetDataBreakpoint(write-only)")
if len(p.Breakpoints().M) != clearlen+3 {
// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel
watchbpnum := 3
if recorded, _ := p.Recorded(); recorded {
watchbpnum = 4
}
if len(p.Breakpoints().M) != clearlen+watchbpnum {
// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel (2 if recorded)
t.Errorf("wrong number of breakpoints after setting watchpoint: %d", len(p.Breakpoints().M)-clearlen)
}
@ -5558,16 +5581,21 @@ func TestWatchpointStack(t *testing.T) {
}
}
// Note: for recorded processes retaddr will not always be the return
// address, ~50% of the times it will be the address of the CALL
// instruction preceding the return address, this does not matter for this
// test.
_, err = p.SetBreakpoint(retaddr, proc.UserBreakpoint, nil)
assertNoError(err, t, "SetBreakpoint")
if len(p.Breakpoints().M) != clearlen+3 {
// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel (which is also a user breakpoint)
if len(p.Breakpoints().M) != clearlen+watchbpnum {
// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel (which is also a user breakpoint) (and another out of scope sentinel if recorded)
t.Errorf("wrong number of breakpoints after setting watchpoint: %d", len(p.Breakpoints().M)-clearlen)
}
assertNoError(p.Continue(), t, "Continue 1")
assertLineNumber(p, t, 17, "Continue 1") // Position 1
assertLineNumber(p, t, position1, "Continue 1") // Position 1
assertNoError(p.Continue(), t, "Continue 2")
t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
@ -5591,3 +5619,52 @@ func TestWatchpointStack(t *testing.T) {
}
})
}
func TestWatchpointStackBackwardsOutOfScope(t *testing.T) {
skipUnlessOn(t, "only for recorded targets", "rr")
protest.AllowRecording(t)
withTestProcess("databpstack", t, func(p *proc.Target, fixture protest.Fixture) {
setFileBreakpoint(p, t, fixture.Source, 11) // Position 0 breakpoint
clearlen := len(p.Breakpoints().M)
assertNoError(p.Continue(), t, "Continue 0")
assertLineNumber(p, t, 11, "Continue 0") // Position 0
scope, err := proc.GoroutineScope(p, p.CurrentThread())
assertNoError(err, t, "GoroutineScope")
_, err = p.SetWatchpoint(scope, "w", proc.WatchWrite, nil)
assertNoError(err, t, "SetDataBreakpoint(write-only)")
assertNoError(p.Continue(), t, "Continue 1")
assertLineNumber(p, t, 17, "Continue 1") // Position 1
p.ChangeDirection(proc.Backward)
assertNoError(p.Continue(), t, "Continue 2")
t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
assertLineNumber(p, t, 16, "Continue 2") // Position 1 again (because of inverted movement)
assertNoError(p.Continue(), t, "Continue 3")
t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
assertLineNumber(p, t, 11, "Continue 3") // Position 0 (breakpoint 1 hit)
assertNoError(p.Continue(), t, "Continue 4")
t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
assertLineNumber(p, t, 23, "Continue 4") // Position 2 (watchpoint gone out of scope)
if len(p.Breakpoints().M) != clearlen {
t.Errorf("wrong number of breakpoints after watchpoint goes out of scope: %d", len(p.Breakpoints().M)-clearlen)
}
if len(p.Breakpoints().WatchOutOfScope) != 1 {
t.Errorf("wrong number of out-of-scope watchpoints after watchpoint goes out of scope: %d", len(p.Breakpoints().WatchOutOfScope))
}
if len(p.Breakpoints().M) != clearlen {
// want 1 user breakpoint set at retaddr
t.Errorf("wrong number of breakpoints after removing user breakpoint: %d", len(p.Breakpoints().M)-clearlen)
}
})
}

@ -72,6 +72,28 @@ func (t *Target) setStackWatchBreakpoints(scope *EvalScope, watchpoint *Breakpoi
retbreaklet.watchpoint = watchpoint
retbreaklet.callback = woos
if recorded, _ := t.Recorded(); recorded && retframe.Current.Fn != nil {
// Must also set a breakpoint on the call instruction immediately
// preceding retframe.Current.PC, because the watchpoint could also go out
// of scope while we are running backwards.
callerText, err := disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), retframe.Current.Fn.Entry, retframe.Current.Fn.End, false)
if err != nil {
return err
}
for i, instr := range callerText {
if instr.Loc.PC == retframe.Current.PC && i > 0 {
retbp2, err := t.SetBreakpoint(callerText[i-1].Loc.PC, WatchOutOfScopeBreakpoint, retFrameCond)
if err != nil {
return err
}
retbreaklet2 := retbp2.Breaklets[len(retbp.Breaklets)-1]
retbreaklet2.watchpoint = watchpoint
retbreaklet2.callback = woos
break
}
}
}
// Stack Resize Sentinel
fn := t.BinInfo().LookupFunc["runtime.copystack"]

@ -147,6 +147,8 @@ The memory location is specified with the same expression language used by 'prin
will watch the address of variable 'v'.
Note that writes that do not change the value of the watched memory address might not be reported.
See also: "help print".`},
{aliases: []string{"restart", "r"}, group: runCmds, cmdFn: restart, helpMsg: `Restart process.