Skip to content

Commit

Permalink
pkg/proc/gdbserial: optimize gdbwire backend (#3715)
Browse files Browse the repository at this point in the history
This change optimizes the gdbwire backend by reducing the number of
round trips we have to make to debugserver. It does this by using the
jstopinfo packet to only query threads which we know to have a stop
reason, and it also uses the registers returned by the 'T' packet
to avoid issuing a bunch of 'p' packets to get the register values.
  • Loading branch information
derekparker authored May 15, 2024
1 parent b4fc206 commit 4f28742
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 46 deletions.
72 changes: 49 additions & 23 deletions pkg/proc/gdbserial/gdbserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ func (p *gdbProcess) initialize(path, cmdline string, debugInfoDirs []string, st
}
}

err = p.updateThreadList(&threadUpdater{p: p})
err = p.updateThreadList(&threadUpdater{p: p}, nil)
if err != nil {
p.conn.conn.Close()
p.bi.Close()
Expand Down Expand Up @@ -877,6 +877,7 @@ func (p *gdbProcess) ContinueOnce(cctx *proc.ContinueOnceContext) (proc.Thread,
var trapthread *gdbThread
var tu = threadUpdater{p: p}
var atstart bool
var trapThreadRegs map[uint64]uint64
continueLoop:
for {
tu.Reset()
Expand All @@ -895,7 +896,7 @@ continueLoop:
// NOTE: because debugserver will sometimes send two stop packets after a
// continue it is important that this is the very first thing we do after
// resume(). See comment in threadStopInfo for an explanation.
p.updateThreadList(&tu)
p.updateThreadList(&tu, sp.jstopInfo)

trapthread = p.findThreadByStrID(threadID)
if trapthread != nil && !p.threadStopInfo {
Expand All @@ -913,11 +914,16 @@ continueLoop:
return nil, proc.StopExited, proc.ErrProcessExited{Pid: p.conn.pid}
}
if shouldStop {
trapThreadRegs = sp.regs
break continueLoop
}
}

p.clearThreadRegisters()
// TODO(deparker): Can we support this optimization for the RR backend?
if p.conn.isDebugserver {
trapthread.reloadRegisters(trapThreadRegs)
}

stopReason := proc.StopUnknown
if atstart {
Expand Down Expand Up @@ -1000,7 +1006,6 @@ func (p *gdbProcess) handleThreadSignals(cctx *proc.ContinueOnceContext, trapthr
// Unfortunately debugserver can not convert them into signals for the
// process so we must stop here.
case debugServerTargetExcBadAccess, debugServerTargetExcBadInstruction, debugServerTargetExcArithmetic, debugServerTargetExcEmulation, debugServerTargetExcSoftware, debugServerTargetExcBreakpoint:

trapthreadCandidate = th
shouldStop = true

Expand Down Expand Up @@ -1127,7 +1132,7 @@ func (p *gdbProcess) Restart(cctx *proc.ContinueOnceContext, pos string) (proc.T
return nil, err
}

err = p.updateThreadList(&threadUpdater{p: p})
err = p.updateThreadList(&threadUpdater{p: p}, nil)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1405,7 +1410,7 @@ func (tu *threadUpdater) Finish() {
// Some stubs will return the list of running threads in the stop packet, if
// this happens the threadUpdater will know that we have already updated the
// thread list and the first step of updateThreadList will be skipped.
func (p *gdbProcess) updateThreadList(tu *threadUpdater) error {
func (p *gdbProcess) updateThreadList(tu *threadUpdater, jstopInfo map[int]stopPacket) error {
if !tu.done {
first := true
for {
Expand All @@ -1426,7 +1431,13 @@ func (p *gdbProcess) updateThreadList(tu *threadUpdater) error {
}

for _, th := range p.threads {
if p.threadStopInfo {
queryThreadInfo := true
if jstopInfo != nil {
// TODO(derekparker): Use jstopInfo directly, if present, instead of
// issuing another stop info request.
_, queryThreadInfo = jstopInfo[th.ID]
}
if p.threadStopInfo && queryThreadInfo {
sp, err := p.conn.threadStopInfo(th.strID)
if err != nil {
if isProtocolErrorUnsupported(err) {
Expand All @@ -1440,9 +1451,7 @@ func (p *gdbProcess) updateThreadList(tu *threadUpdater) error {
th.watchAddr = sp.watchAddr
th.watchReg = sp.watchReg
} else {
th.sig = 0
th.watchAddr = 0
th.watchReg = -1
th.clearBreakpointState()
}
}

Expand Down Expand Up @@ -1537,7 +1546,7 @@ func (t *gdbThread) ThreadID() int {
// Registers returns the CPU registers for this thread.
func (t *gdbThread) Registers() (proc.Registers, error) {
if t.regs.regs == nil {
if err := t.reloadRegisters(); err != nil {
if err := t.reloadRegisters(nil); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -1689,25 +1698,42 @@ func (regs *gdbRegisters) gdbRegisterNew(reginfo *gdbRegisterInfo) gdbRegister {
// It will also load the address of the thread's G.
// Loading the address of G can be done in one of two ways reloadGAlloc, if
// the stub can allocate memory, or reloadGAtPC, if the stub can't.
func (t *gdbThread) reloadRegisters() error {
func (t *gdbThread) reloadRegisters(regs map[uint64]uint64) error {
if t.regs.regs == nil {
t.regs.init(t.p.conn.regsInfo, t.p.bi.Arch, t.p.regnames)
}

if t.p.gcmdok {
if err := t.p.conn.readRegisters(t.strID, t.regs.buf); err != nil {
gdberr, isProt := err.(*GdbProtocolError)
if isProtocolErrorUnsupported(err) || (t.p.conn.isDebugserver && isProt && gdberr.code == "E74") {
t.p.gcmdok = false
} else {
return err
if regs == nil {
if t.p.gcmdok {
if err := t.p.conn.readRegisters(t.strID, t.regs.buf); err != nil {
gdberr, isProt := err.(*GdbProtocolError)
if isProtocolErrorUnsupported(err) || (t.p.conn.isDebugserver && isProt && gdberr.code == "E74") {
t.p.gcmdok = false
} else {
return err
}
}
}
}
if !t.p.gcmdok {
for _, reginfo := range t.p.conn.regsInfo {
if err := t.p.conn.readRegister(t.strID, reginfo.Regnum, t.regs.regs[reginfo.Name].value); err != nil {
return err
if !t.p.gcmdok {
for _, reginfo := range t.p.conn.regsInfo {
if err := t.p.conn.readRegister(t.strID, reginfo.Regnum, t.regs.regs[reginfo.Name].value); err != nil {
return err
}
}
}
} else {
for _, r := range t.p.conn.regsInfo {
if val, ok := regs[uint64(r.Regnum)]; ok {
switch r.Bitsize / 8 {
case 8:
binary.BigEndian.PutUint64(t.regs.regs[r.Name].value, val)
case 4:
binary.BigEndian.PutUint32(t.regs.regs[r.Name].value, uint32(val))
}
} else {
if err := t.p.conn.readRegister(t.strID, r.Regnum, t.regs.regs[r.Name].value); err != nil {
return err
}
}
}
}
Expand Down
91 changes: 68 additions & 23 deletions pkg/proc/gdbserial/gdbserver_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"debug/macho"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
Expand Down Expand Up @@ -724,6 +725,16 @@ type stopPacket struct {
reason string
watchAddr uint64
watchReg int
jstopInfo map[int]stopPacket
regs map[uint64]uint64
}

type jsonStopPacket struct {
Tid int `json:"tid"`
Signal int `json:"signal"`
Metype int `json:"metype"`
Medata []uint64 `json:"medata"`
Reason string `json:"reason"`
}

// Mach exception codes used to decode metype/medata keys in stop packets (necessary to support watchpoints with debugserver).
Expand Down Expand Up @@ -753,6 +764,7 @@ func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpd
}
sp.sig = uint8(sig)
sp.watchReg = -1
sp.regs = make(map[uint64]uint64)

if logflags.GdbWire() && gdbWireFullStopPacket {
conn.log.Debugf("full stop packet: %s", string(resp))
Expand All @@ -761,10 +773,45 @@ func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpd
var metype int
medata := make([]uint64, 0, 10)

parseMachException := func(sp *stopPacket, metype int, medata []uint64) {
// 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 || medata[0] == _EXC_ARM_BREAKPOINT) {
// The arm64 specification allows for up to 16 debug registers.
// The registers are zero indexed, thus a value less than 16 will
// be a hardware breakpoint register index.
// See: https://developer.arm.com/documentation/102120/0101/Debug-exceptions
// TODO(deparker): we can ask debugserver for the number of hardware breakpoints
// directly.
if medata[1] < 16 {
sp.watchReg = int(medata[1])
} else {
sp.watchAddr = medata[1]
}
}
}
}

csp := colonSemicolonParser{buf: resp[3:]}
for csp.next() {
key, value := csp.key, csp.value

if reg, err := strconv.ParseUint(string(key), 16, 64); err == nil {
// This is a register.
v, err := strconv.ParseUint(string(value), 16, 64)
if err != nil {
return false, stopPacket{}, fmt.Errorf("malformed stop packet: %s", string(resp))
}
sp.regs[reg] = v
}

switch string(key) {
case "thread":
sp.threadID = string(value)
Expand All @@ -787,33 +834,31 @@ func (conn *gdbConn) parseStopPacket(resp []byte, threadID string, tu *threadUpd
// 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 || medata[0] == _EXC_ARM_BREAKPOINT) {
// The arm64 specification allows for up to 16 debug registers.
// The registers are zero indexed, thus a value less than 16 will
// be a hardware breakpoint register index.
// See: https://developer.arm.com/documentation/102120/0101/Debug-exceptions
// TODO(deparker): we can ask debugserver for the number of hardware breakpoints
// directly.
if medata[1] < 16 {
sp.watchReg = int(medata[1])
} else {
sp.watchAddr = medata[1]
case "jstopinfo":
jstopinfo, err := hex.DecodeString(string(value))
if err != nil {
return false, stopPacket{}, fmt.Errorf("malformed stop packet: %s (wrong jstopinfo)", string(resp))
}
parsedJstopInfo := []jsonStopPacket{}
if err := json.Unmarshal(jstopinfo, &parsedJstopInfo); err != nil {
return false, stopPacket{}, fmt.Errorf("malformed stop packet: %s (wrong jstopinfo)", string(resp))
}
sp.jstopInfo = make(map[int]stopPacket)
for _, jsp := range parsedJstopInfo {
threadID := fmt.Sprintf("%d", jsp.Tid)
newStopPacket := stopPacket{
threadID: threadID,
reason: jsp.Reason,
sig: uint8(jsp.Signal),
}
parseMachException(&newStopPacket, jsp.Metype, jsp.Medata)
sp.jstopInfo[jsp.Tid] = newStopPacket
}
}
}

parseMachException(&sp, metype, medata)

return false, sp, nil

case 'W', 'X':
Expand Down

0 comments on commit 4f28742

Please sign in to comment.