delve/pkg/proc/gdbserial/gdbserver_conn.go
Alessandro Arzilli 721a0d7c9c
proc/gdbserial: refactor parsing of key-value pairs from gdb protocol (#3574)
The gdb remote serial protocol returns in several places a list of key
value pair in the form key1:value1;key2:value2;...;keyN:valueN.
We had ad-hoc parsers for this in three places, this commit
consolidates the parser in a single utility object.
2023-11-20 10:50:37 -08:00

1540 lines
40 KiB
Go

package gdbserial
import (
"bufio"
"bytes"
"debug/macho"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc"
)
type gdbConn struct {
conn net.Conn
rdr *bufio.Reader
inbuf []byte
outbuf bytes.Buffer
running bool
direction proc.Direction // direction of execution
packetSize int // maximum packet size supported by stub
regsInfo []gdbRegisterInfo // list of registers
workaroundReg *gdbRegisterInfo // used to work-around a register setting bug in debugserver, see use in gdbserver.go
pid int // cache process id
ack bool // when ack is true acknowledgment packets are enabled
multiprocess bool // multiprocess extensions are active
maxTransmitAttempts int // maximum number of transmit or receive attempts when bad checksums are read
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
goos string
useXcmd bool // forces writeMemory to use the 'X' command
log logflags.Logger
}
var ErrTooManyAttempts = errors.New("too many transmit attempts")
// GdbProtocolError is an error response (Exx) of Gdb Remote Serial Protocol
// or an "unsupported command" response (empty packet).
type GdbProtocolError struct {
context string
cmd string
code string
}
func (err *GdbProtocolError) Error() string {
cmd := err.cmd
if len(cmd) > 20 {
cmd = cmd[:20] + "..."
}
if err.code == "" {
return fmt.Sprintf("unsupported packet %s during %s", cmd, err.context)
}
return fmt.Sprintf("protocol error %s during %s for packet %s", err.code, err.context, cmd)
}
func isProtocolErrorUnsupported(err error) bool {
gdberr, ok := err.(*GdbProtocolError)
if !ok {
return false
}
return gdberr.code == ""
}
// GdbMalformedThreadIDError is returned when the stub responds with a
// thread ID that does not conform with the Gdb Remote Serial Protocol
// specification.
type GdbMalformedThreadIDError struct {
tid string
}
func (err *GdbMalformedThreadIDError) Error() string {
return fmt.Sprintf("malformed thread ID %q", err.tid)
}
const (
qSupportedSimple = "$qSupported:swbreak+;hwbreak+;no-resumed+;xmlRegisters=i386"
qSupportedMultiprocess = "$qSupported:multiprocess+;swbreak+;hwbreak+;no-resumed+;xmlRegisters=i386"
)
func (conn *gdbConn) handshake(regnames *gdbRegnames) error {
conn.ack = true
conn.packetSize = 256
conn.rdr = bufio.NewReader(conn.conn)
// This first ack packet is needed to start up the connection
conn.sendack('+')
conn.disableAck()
// Try to enable thread suffixes for the command 'g' and 'p'
if _, err := conn.exec([]byte("$QThreadSuffixSupported"), "init"); err != nil {
if isProtocolErrorUnsupported(err) {
conn.threadSuffixSupported = false
} else {
return err
}
} else {
conn.threadSuffixSupported = true
}
if !conn.threadSuffixSupported {
features, err := conn.qSupported(true)
if err != nil {
return err
}
conn.multiprocess = features["multiprocess"]
// for some reason gdbserver won't let us read target.xml unless first we
// select a thread.
if conn.multiprocess {
conn.exec([]byte("$Hgp0.0"), "init")
} else {
conn.exec([]byte("$Hgp0"), "init")
}
} else {
// execute qSupported with the multiprocess feature disabled (the
// interaction of thread suffixes and multiprocess is not documented), we
// only need this call to configure conn.packetSize.
if _, err := conn.qSupported(false); err != nil {
return err
}
}
// Attempt to figure out the name of the processor register.
// We either need qXfer:features:read (gdbserver/rr) or qRegisterInfo (lldb)
regFound := map[string]bool{
regnames.PC: false,
regnames.SP: false,
regnames.BP: false,
regnames.CX: false,
}
if err := conn.readRegisterInfo(regFound); err != nil {
if isProtocolErrorUnsupported(err) {
if err := conn.readTargetXml(regFound); err != nil {
return err
}
} else {
return err
}
}
for n := range regFound {
if n == "" {
continue
}
if !regFound[n] {
return fmt.Errorf("could not find %s register", n)
}
}
// We either need:
// * QListThreadsInStopReply + qThreadStopInfo (i.e. lldb-server/debugserver),
// * or a stub that runs the inferior in single threaded mode (i.e. rr).
// Otherwise we'll have problems handling breakpoints in multithreaded programs.
if _, err := conn.exec([]byte("$QListThreadsInStopReply"), "init"); err != nil {
gdberr, ok := err.(*GdbProtocolError)
if !ok {
return err
}
if gdberr.code != "" {
return err
}
}
if resp, err := conn.exec([]byte("$x0,0"), "init"); err == nil && string(resp) == "OK" {
conn.xcmdok = true
}
return nil
}
// qSupported interprets qSupported responses.
func (conn *gdbConn) qSupported(multiprocess bool) (features map[string]bool, err error) {
q := qSupportedSimple
if multiprocess {
q = qSupportedMultiprocess
}
respBuf, err := conn.exec([]byte(q), "init/qSupported")
if err != nil {
return nil, err
}
resp := strings.Split(string(respBuf), ";")
features = make(map[string]bool)
for _, stubfeature := range resp {
if len(stubfeature) == 0 {
continue
} else if equal := strings.Index(stubfeature, "="); equal >= 0 {
if stubfeature[:equal] == "PacketSize" {
if n, err := strconv.ParseInt(stubfeature[equal+1:], 16, 64); err == nil {
conn.packetSize = int(n)
}
}
} else if stubfeature[len(stubfeature)-1] == '+' {
features[stubfeature[:len(stubfeature)-1]] = true
}
}
return features, nil
}
// disableAck disables protocol acks.
func (conn *gdbConn) disableAck() error {
_, err := conn.exec([]byte("$QStartNoAckMode"), "init/disableAck")
if err == nil {
conn.ack = false
}
return err
}
// gdbTarget is a struct type used to parse target.xml
type gdbTarget struct {
Includes []gdbTargetInclude `xml:"xi include"`
Registers []gdbRegisterInfo `xml:"reg"`
}
type gdbTargetInclude struct {
Href string `xml:"href,attr"`
}
type gdbRegisterInfo struct {
Name string `xml:"name,attr"`
Bitsize int `xml:"bitsize,attr"`
Offset int
Regnum int `xml:"regnum,attr"`
Group string `xml:"group,attr"`
ignoreOnWrite bool
}
func setRegFound(regFound map[string]bool, name string) {
for n := range regFound {
if name == n {
regFound[n] = true
}
}
}
// readTargetXml reads target.xml file from stub using qXfer:features:read,
// then parses it requesting any additional files.
// The schema of target.xml is described by:
//
// https://github.com/bminor/binutils-gdb/blob/61baf725eca99af2569262d10aca03dcde2698f6/gdb/features/gdb-target.dtd
func (conn *gdbConn) readTargetXml(regFound map[string]bool) (err error) {
conn.regsInfo, err = conn.readAnnex("target.xml")
if err != nil {
return err
}
var offset int
regnum := 0
for i := range conn.regsInfo {
if conn.regsInfo[i].Regnum == 0 {
conn.regsInfo[i].Regnum = regnum
} else {
regnum = conn.regsInfo[i].Regnum
}
conn.regsInfo[i].Offset = offset
offset += conn.regsInfo[i].Bitsize / 8
setRegFound(regFound, conn.regsInfo[i].Name)
regnum++
}
return nil
}
// readRegisterInfo uses qRegisterInfo to read register information (used
// when qXfer:feature:read is not supported).
func (conn *gdbConn) readRegisterInfo(regFound map[string]bool) (err error) {
regnum := 0
for {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$qRegisterInfo%x", regnum)
respbytes, err := conn.exec(conn.outbuf.Bytes(), "register info")
if err != nil {
if regnum == 0 {
return err
}
break
}
var regname string
var offset int
var bitsize int
var contained bool
var ignoreOnWrite bool
resp := string(respbytes)
for {
semicolon := strings.Index(resp, ";")
keyval := resp
if semicolon >= 0 {
keyval = resp[:semicolon]
}
colon := strings.Index(keyval, ":")
if colon >= 0 {
name := keyval[:colon]
value := keyval[colon+1:]
switch name {
case "name":
regname = value
case "offset":
offset, _ = strconv.Atoi(value)
case "bitsize":
bitsize, _ = strconv.Atoi(value)
case "container-regs":
contained = true
case "set":
if value == "Exception State Registers" || value == "AMX Registers" {
// debugserver doesn't like it if we try to write these
ignoreOnWrite = true
}
}
}
if semicolon < 0 {
break
}
resp = resp[semicolon+1:]
}
if contained {
if regname == "xmm0" {
conn.workaroundReg = &gdbRegisterInfo{Regnum: regnum, Name: regname, Bitsize: bitsize, Offset: offset, ignoreOnWrite: ignoreOnWrite}
}
regnum++
continue
}
setRegFound(regFound, regname)
conn.regsInfo = append(conn.regsInfo, gdbRegisterInfo{Regnum: regnum, Name: regname, Bitsize: bitsize, Offset: offset, ignoreOnWrite: ignoreOnWrite})
regnum++
}
return nil
}
func (conn *gdbConn) readAnnex(annex string) ([]gdbRegisterInfo, error) {
tgtbuf, err := conn.qXfer("features", annex, false)
if err != nil {
return nil, err
}
var tgt gdbTarget
if err := xml.Unmarshal(tgtbuf, &tgt); err != nil {
return nil, err
}
for _, incl := range tgt.Includes {
regs, err := conn.readAnnex(incl.Href)
if err != nil {
return nil, err
}
tgt.Registers = append(tgt.Registers, regs...)
}
return tgt.Registers, nil
}
func (conn *gdbConn) readExecFile() (string, error) {
outbuf, err := conn.qXfer("exec-file", "", true)
if err != nil {
return "", err
}
return string(outbuf), nil
}
func (conn *gdbConn) readAuxv() ([]byte, error) {
return conn.qXfer("auxv", "", true)
}
// qXfer executes a 'qXfer' read with the specified kind (i.e. feature,
// exec-file, etc...) and annex.
func (conn *gdbConn) qXfer(kind, annex string, binary bool) ([]byte, error) {
out := []byte{}
for {
cmd := []byte(fmt.Sprintf("$qXfer:%s:read:%s:%x,fff", kind, annex, len(out)))
err := conn.send(cmd)
if err != nil {
return nil, err
}
buf, err := conn.recv(cmd, "target features transfer", binary)
if err != nil {
return nil, err
}
out = append(out, buf[1:]...)
if buf[0] == 'l' {
break
}
}
return out, nil
}
// qXferWrite executes a 'qXfer' write with the specified kind and annex.
func (conn *gdbConn) qXferWrite(kind, annex string) error {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$qXfer:%s:write:%s:0:", kind, annex)
//TODO(aarzilli): if we ever actually need to write something with qXfer,
//this will need to be implemented properly. At the moment it is only used
//for a fake write to the siginfo kind, to end a diversion in 'rr'.
_, err := conn.exec(conn.outbuf.Bytes(), "qXfer")
return err
}
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, typ breakpointType, kind int) error {
conn.outbuf.Reset()
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, typ breakpointType, kind int) error {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$z%d,%x,%d", typ, addr, kind)
_, err := conn.exec(conn.outbuf.Bytes(), "clear breakpoint")
return err
}
// kill executes a 'k' (kill) command.
func (conn *gdbConn) kill() error {
resp, err := conn.exec([]byte{'$', 'k'}, "kill")
if err == io.EOF {
// The stub is allowed to shut the connection on us immediately after a
// kill. This is not an error.
conn.conn.Close()
conn.conn = nil
return proc.ErrProcessExited{Pid: conn.pid}
}
if err != nil {
return err
}
_, _, err = conn.parseStopPacket(resp, "", nil)
return err
}
// detach executes a 'D' (detach) command.
func (conn *gdbConn) detach() error {
if conn.conn == nil {
// Already detached
return nil
}
_, err := conn.exec([]byte{'$', 'D'}, "detach")
conn.conn.Close()
conn.conn = nil
return err
}
// readRegisters executes a 'g' (read registers) command.
func (conn *gdbConn) readRegisters(threadID string, data []byte) error {
if !conn.threadSuffixSupported {
if err := conn.selectThread('g', threadID, "registers read"); err != nil {
return err
}
}
conn.outbuf.Reset()
conn.outbuf.WriteString("$g")
conn.appendThreadSelector(threadID)
resp, err := conn.exec(conn.outbuf.Bytes(), "registers read")
if err != nil {
return err
}
for i := 0; i < len(resp); i += 2 {
n, _ := strconv.ParseUint(string(resp[i:i+2]), 16, 8)
data[i/2] = uint8(n)
}
return nil
}
// writeRegisters executes a 'G' (write registers) command.
func (conn *gdbConn) writeRegisters(threadID string, data []byte) error {
if !conn.threadSuffixSupported {
if err := conn.selectThread('g', threadID, "registers write"); err != nil {
return err
}
}
conn.outbuf.Reset()
conn.outbuf.WriteString("$G")
for _, b := range data {
fmt.Fprintf(&conn.outbuf, "%02x", b)
}
conn.appendThreadSelector(threadID)
_, err := conn.exec(conn.outbuf.Bytes(), "registers write")
return err
}
// readRegister executes 'p' (read register) command.
func (conn *gdbConn) readRegister(threadID string, regnum int, data []byte) error {
if !conn.threadSuffixSupported {
if err := conn.selectThread('g', threadID, "registers write"); err != nil {
return err
}
}
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$p%x", regnum)
conn.appendThreadSelector(threadID)
resp, err := conn.exec(conn.outbuf.Bytes(), "register read")
if err != nil {
return err
}
for i := 0; i < len(resp); i += 2 {
n, _ := strconv.ParseUint(string(resp[i:i+2]), 16, 8)
data[i/2] = uint8(n)
}
return nil
}
// writeRegister executes 'P' (write register) command.
func (conn *gdbConn) writeRegister(threadID string, regnum int, data []byte) error {
if !conn.threadSuffixSupported {
if err := conn.selectThread('g', threadID, "registers write"); err != nil {
return err
}
}
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$P%x=", regnum)
for _, b := range data {
fmt.Fprintf(&conn.outbuf, "%02x", b)
}
conn.appendThreadSelector(threadID)
_, err := conn.exec(conn.outbuf.Bytes(), "register write")
return err
}
// resume execution of the target process.
// If the current direction is proc.Backward this is done with the 'bc' command.
// If the current direction is proc.Forward this is done with the vCont command.
// The threads argument will be used to determine which signal to use to
// 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(cctx *proc.ContinueOnceContext, threads map[int]*gdbThread, tu *threadUpdater) (stopPacket, error) {
if conn.direction == proc.Forward {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$vCont")
for _, th := range threads {
if th.sig != 0 {
fmt.Fprintf(&conn.outbuf, ";C%02x:%s", th.sig, th.strID)
}
}
fmt.Fprintf(&conn.outbuf, ";c")
} else {
if err := conn.selectThread('c', "p-1.-1", "resume"); err != nil {
return stopPacket{}, err
}
conn.outbuf.Reset()
fmt.Fprint(&conn.outbuf, "$bc")
}
cctx.StopMu.Lock()
if err := conn.send(conn.outbuf.Bytes()); err != nil {
cctx.StopMu.Unlock()
return stopPacket{}, err
}
conn.running = true
cctx.StopMu.Unlock()
defer func() {
cctx.StopMu.Lock()
conn.running = false
cctx.StopMu.Unlock()
}()
if cctx.ResumeChan != nil {
close(cctx.ResumeChan)
cctx.ResumeChan = nil
}
return conn.waitForvContStop("resume", "-1", tu)
}
// step executes a 'vCont' command on the specified thread with 's' action.
func (conn *gdbConn) step(th *gdbThread, tu *threadUpdater, ignoreFaultSignal bool) error {
threadID := th.strID
if conn.direction != proc.Forward {
if err := conn.selectThread('c', threadID, "step"); err != nil {
return err
}
conn.outbuf.Reset()
fmt.Fprint(&conn.outbuf, "$bs")
if err := conn.send(conn.outbuf.Bytes()); err != nil {
return err
}
_, err := conn.waitForvContStop("singlestep", threadID, tu)
return err
}
var _SIGBUS uint8
switch conn.goos {
case "linux":
_SIGBUS = 0x7
case "darwin":
_SIGBUS = 0xa
default:
panic(fmt.Errorf("unknown GOOS %s", conn.goos))
}
var sig uint8 = 0
for {
conn.outbuf.Reset()
if sig == 0 {
fmt.Fprintf(&conn.outbuf, "$vCont;s:%s", threadID)
} else {
fmt.Fprintf(&conn.outbuf, "$vCont;S%02x:%s", sig, threadID)
}
if err := conn.send(conn.outbuf.Bytes()); err != nil {
return err
}
if tu != nil {
tu.Reset()
}
sp, err := conn.waitForvContStop("singlestep", threadID, tu)
sig = sp.sig
if err != nil {
return err
}
switch sig {
case faultSignal:
if ignoreFaultSignal { // we attempting to read the TLS, a fault here should be ignored
return nil
}
if conn.isDebugserver {
// For some reason trying to deliver a signal in vCont step makes
// debugserver lockup (no errors, it just gets stuck), store the signal
// to deliver it later with the vCont;c
th.sig = sig
return nil
}
case _SIGILL, _SIGBUS, _SIGFPE:
if conn.isDebugserver {
// See comment above
th.sig = sig
return nil
}
// otherwise propagate these signals to inferior immediately
case interruptSignal, breakpointSignal, stopSignal:
return nil
case childSignal: // stop on debugserver but SIGCHLD on lldb-server/linux
if conn.isDebugserver {
return nil
}
case debugServerTargetExcBadAccess, debugServerTargetExcBadInstruction, debugServerTargetExcArithmetic, debugServerTargetExcEmulation, debugServerTargetExcSoftware, debugServerTargetExcBreakpoint:
if ignoreFaultSignal {
return nil
}
return machTargetExcToError(sig)
default:
// delay propagation of any other signal to until after the stepping is done
th.sig = sig
sig = 0
}
}
}
var errThreadBlocked = errors.New("thread blocked")
func (conn *gdbConn) waitForvContStop(context, threadID string, tu *threadUpdater) (stopPacket, error) {
count := 0
failed := false
for {
conn.conn.SetReadDeadline(time.Now().Add(heartbeatInterval))
resp, err := conn.recv(nil, context, false)
conn.conn.SetReadDeadline(time.Time{})
if neterr, isneterr := err.(net.Error); isneterr && neterr.Timeout() {
// Debugserver sometimes forgets to inform us that inferior stopped,
// sending this status request after a timeout helps us get unstuck.
// Debugserver will not respond to this request unless inferior is
// already stopped.
if conn.isDebugserver {
conn.send([]byte("$?"))
}
if count > 1 && context == "singlestep" {
failed = true
conn.sendCtrlC()
}
count++
} else if failed {
return stopPacket{}, errThreadBlocked
} else if err != nil {
return stopPacket{}, err
} else {
repeat, sp, err := conn.parseStopPacket(resp, threadID, tu)
if !repeat {
return sp, err
}
}
}
}
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] {
case 'T':
if len(resp) < 3 {
return false, stopPacket{}, fmt.Errorf("malformed response for vCont %s", string(resp))
}
sig, err := strconv.ParseUint(string(resp[1:3]), 16, 8)
if err != nil {
return false, stopPacket{}, fmt.Errorf("malformed stop packet: %s", string(resp))
}
sp.sig = uint8(sig)
if logflags.GdbWire() && gdbWireFullStopPacket {
conn.log.Debugf("full stop packet: %s", string(resp))
}
var metype int
var medata = make([]uint64, 0, 10)
csp := colonSemicolonParser{buf: resp[3:]}
for csp.next() {
key, value := csp.key, csp.value
switch string(key) {
case "thread":
sp.threadID = string(value)
case "threads":
if tu != nil {
tu.Add(strings.Split(string(value), ","))
tu.Finish()
}
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]
}
}
return false, sp, nil
case 'W', 'X':
// process exited, next two character are exit code
semicolon := bytes.Index(resp, []byte{';'})
if semicolon < 0 {
semicolon = len(resp)
}
status, _ := strconv.ParseUint(string(resp[1:semicolon]), 16, 8)
return false, stopPacket{}, proc.ErrProcessExited{Pid: conn.pid, Status: int(status)}
case 'N':
// we were singlestepping the thread and the thread exited
sp.threadID = threadID
return false, sp, nil
case 'O':
data := make([]byte, 0, len(resp[1:])/2)
for i := 1; i < len(resp); i += 2 {
n, _ := strconv.ParseUint(string(resp[i:i+2]), 16, 8)
data = append(data, uint8(n))
}
os.Stdout.Write(data)
return true, sp, nil
default:
return false, sp, fmt.Errorf("unexpected response for vCont %c", resp[0])
}
}
const ctrlC = 0x03 // the ASCII character for ^C
// executes a ctrl-C on the line
func (conn *gdbConn) sendCtrlC() error {
conn.log.Debug("<- interrupt")
_, err := conn.conn.Write([]byte{ctrlC})
return err
}
// queryProcessInfo executes a qProcessInfoPID (if pid != 0) or a qProcessInfo (if pid == 0)
func (conn *gdbConn) queryProcessInfo(pid int) (map[string]string, error) {
conn.outbuf.Reset()
if pid != 0 {
fmt.Fprintf(&conn.outbuf, "$qProcessInfoPID:%d", pid)
} else {
fmt.Fprint(&conn.outbuf, "$qProcessInfo")
}
resp, err := conn.exec(conn.outbuf.Bytes(), "process info for pid")
if err != nil {
return nil, err
}
pi := make(map[string]string)
csp := colonSemicolonParser{buf: resp}
for csp.next() {
key, value := string(csp.key), string(csp.value)
switch key {
case "name":
name := make([]byte, len(value)/2)
for i := 0; i < len(value); i += 2 {
n, _ := strconv.ParseUint(value[i:i+2], 16, 8)
name[i/2] = byte(n)
}
pi[key] = string(name)
default:
pi[key] = value
}
}
return pi, nil
}
// executes qfThreadInfo/qsThreadInfo commands
func (conn *gdbConn) queryThreads(first bool) (threads []string, err error) {
// https://sourceware.org/gdb/onlinedocs/gdb/General-Query-Packets.html
conn.outbuf.Reset()
if first {
conn.outbuf.WriteString("$qfThreadInfo")
} else {
conn.outbuf.WriteString("$qsThreadInfo")
}
resp, err := conn.exec(conn.outbuf.Bytes(), "thread info")
if err != nil {
return nil, err
}
switch resp[0] {
case 'l':
return nil, nil
case 'm':
// parse list...
default:
return nil, errors.New("malformed qfThreadInfo response")
}
var pid int
resp = resp[1:]
for {
tidbuf := resp
comma := bytes.Index(tidbuf, []byte{','})
if comma >= 0 {
tidbuf = tidbuf[:comma]
}
if conn.multiprocess && pid == 0 {
dot := bytes.Index(tidbuf, []byte{'.'})
if dot >= 0 {
pid, _ = strconv.Atoi(string(tidbuf[1:dot]))
}
}
threads = append(threads, string(tidbuf))
if comma < 0 {
break
}
resp = resp[comma+1:]
}
if conn.multiprocess && pid > 0 {
conn.pid = pid
}
return threads, nil
}
func (conn *gdbConn) selectThread(kind byte, threadID string, context string) error {
if conn.threadSuffixSupported {
panic("selectThread when thread suffix is supported")
}
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$H%c%s", kind, threadID)
_, err := conn.exec(conn.outbuf.Bytes(), context)
return err
}
func (conn *gdbConn) appendThreadSelector(threadID string) {
if !conn.threadSuffixSupported {
return
}
fmt.Fprintf(&conn.outbuf, ";thread:%s;", threadID)
}
func (conn *gdbConn) readMemory(data []byte, addr uint64) error {
if conn.xcmdok && len(data) > conn.packetSize {
return conn.readMemoryBinary(data, addr)
}
return conn.readMemoryHex(data, addr)
}
// executes 'm' (read memory) command
func (conn *gdbConn) readMemoryHex(data []byte, addr uint64) error {
size := len(data)
data = data[:0]
for size > 0 {
conn.outbuf.Reset()
// gdbserver will crash if we ask too many bytes... not return an error, actually crash
sz := size
if dataSize := (conn.packetSize - 4) / 2; sz > dataSize {
sz = dataSize
}
size = size - sz
fmt.Fprintf(&conn.outbuf, "$m%x,%x", addr+uint64(len(data)), sz)
resp, err := conn.exec(conn.outbuf.Bytes(), "memory read")
if err != nil {
return err
}
for i := 0; i < len(resp); i += 2 {
n, _ := strconv.ParseUint(string(resp[i:i+2]), 16, 8)
data = append(data, uint8(n))
}
}
return nil
}
// executes 'x' (binary read memory) command
func (conn *gdbConn) readMemoryBinary(data []byte, addr uint64) error {
size := len(data)
data = data[:0]
for len(data) < size {
conn.outbuf.Reset()
sz := size - len(data)
fmt.Fprintf(&conn.outbuf, "$x%x,%x", addr+uint64(len(data)), sz)
if err := conn.send(conn.outbuf.Bytes()); err != nil {
return err
}
resp, err := conn.recv(conn.outbuf.Bytes(), "binary memory read", true)
if err != nil {
return err
}
data = append(data, resp...)
}
return nil
}
func writeAsciiBytes(w io.Writer, data []byte) {
for _, b := range data {
fmt.Fprintf(w, "%02x", b)
}
}
// writeMemory writes memory using either 'M' or 'X'
func (conn *gdbConn) writeMemory(addr uint64, data []byte) (written int, err error) {
if conn.useXcmd {
return conn.writeMemoryBinary(addr, data)
}
return conn.writeMemoryHex(addr, data)
}
// executes 'M' (write memory) command
func (conn *gdbConn) writeMemoryHex(addr uint64, data []byte) (written int, err error) {
if len(data) == 0 {
// LLDB can't parse requests for 0-length writes and hangs if we emit them
return 0, nil
}
conn.outbuf.Reset()
//TODO(aarzilli): do not send packets larger than conn.PacketSize
fmt.Fprintf(&conn.outbuf, "$M%x,%x:", addr, len(data))
writeAsciiBytes(&conn.outbuf, data)
_, err = conn.exec(conn.outbuf.Bytes(), "memory write")
if err != nil {
return 0, err
}
return len(data), nil
}
func (conn *gdbConn) writeMemoryBinary(addr uint64, data []byte) (written int, err error) {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$X%x,%x:", addr, len(data))
for _, b := range data {
switch b {
case '#', '$', '}':
conn.outbuf.WriteByte('}')
conn.outbuf.WriteByte(b ^ escapeXor)
default:
conn.outbuf.WriteByte(b)
}
}
_, err = conn.exec(conn.outbuf.Bytes(), "memory write")
if err != nil {
return 0, err
}
return len(data), nil
}
func (conn *gdbConn) allocMemory(sz uint64) (uint64, error) {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$_M%x,rwx", sz)
resp, err := conn.exec(conn.outbuf.Bytes(), "memory allocation")
if err != nil {
return 0, err
}
return strconv.ParseUint(string(resp), 16, 64)
}
// threadStopInfo executes a 'qThreadStopInfo' and returns the reason the
// thread stopped.
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 stopPacket{}, err
}
_, sp, err = conn.parseStopPacket(resp, "", nil)
if err != nil {
return stopPacket{}, err
}
if sp.threadID != threadID {
// When we send a ^C (manual stop request) and the process is close to
// stopping anyway, sometimes, debugserver will send back two stop
// packets. We need to ignore this spurious stop packet. Because the first
// thing we do after the stop is updateThreadList, which calls this
// function, this is relatively painless. We simply need to check that the
// stop packet we receive is for the thread we requested, if it isn't we
// can assume it is the spurious extra stop packet and simply ignore it.
// An example of a problematic interaction is in the commit message for
// this change.
// See https://github.com/go-delve/delve/issues/3013.
conn.conn.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
resp, err = conn.recv(conn.outbuf.Bytes(), "thread stop info", false)
conn.conn.SetReadDeadline(time.Time{})
if err != nil {
if neterr, isneterr := err.(net.Error); isneterr && neterr.Timeout() {
return stopPacket{}, fmt.Errorf("qThreadStopInfo mismatch, requested %s got %s", sp.threadID, threadID)
}
return stopPacket{}, err
}
_, sp, err = conn.parseStopPacket(resp, "", nil)
if err != nil {
return stopPacket{}, err
}
if sp.threadID != threadID {
return stopPacket{}, fmt.Errorf("qThreadStopInfo mismatch, requested %s got %s", sp.threadID, threadID)
}
}
return sp, nil
}
// restart executes a 'vRun' command.
func (conn *gdbConn) restart(pos string) error {
conn.outbuf.Reset()
fmt.Fprint(&conn.outbuf, "$vRun;")
if pos != "" {
fmt.Fprint(&conn.outbuf, ";")
writeAsciiBytes(&conn.outbuf, []byte(pos))
}
_, err := conn.exec(conn.outbuf.Bytes(), "restart")
return err
}
// qRRCmd executes a qRRCmd command
func (conn *gdbConn) qRRCmd(args ...string) (string, error) {
if len(args) == 0 {
panic("must specify at least one argument for qRRCmd")
}
conn.outbuf.Reset()
fmt.Fprint(&conn.outbuf, "$qRRCmd")
for _, arg := range args {
fmt.Fprint(&conn.outbuf, ":")
writeAsciiBytes(&conn.outbuf, []byte(arg))
}
resp, err := conn.exec(conn.outbuf.Bytes(), "qRRCmd")
if err != nil {
return "", err
}
data := make([]byte, 0, len(resp)/2)
for i := 0; i < len(resp); i += 2 {
n, _ := strconv.ParseUint(string(resp[i:i+2]), 16, 8)
data = append(data, uint8(n))
}
return string(data), nil
}
type imageList struct {
Images []imageDescription `json:"images"`
}
type imageDescription struct {
LoadAddress uint64 `json:"load_address"`
Pathname string `json:"pathname"`
MachHeader machHeader `json:"mach_header"`
}
type machHeader struct {
FileType macho.Type `json:"filetype"`
}
// getLoadedDynamicLibraries executes jGetLoadedDynamicLibrariesInfos which
// returns the list of loaded dynamic libraries
func (conn *gdbConn) getLoadedDynamicLibraries() ([]imageDescription, error) {
cmd := []byte("$jGetLoadedDynamicLibrariesInfos:{\"fetch_all_solibs\":true}")
if err := conn.send(cmd); err != nil {
return nil, err
}
resp, err := conn.recv(cmd, "get dynamic libraries", true)
if err != nil {
return nil, err
}
var images imageList
err = json.Unmarshal(resp, &images)
return images.Images, err
}
type memoryRegionInfo struct {
start uint64
size uint64
permissions string
name string
}
func decodeHexString(in []byte) (string, bool) {
out := make([]byte, 0, len(in)/2)
for i := 0; i < len(in); i += 2 {
v, err := strconv.ParseUint(string(in[i:i+2]), 16, 8)
if err != nil {
return "", false
}
out = append(out, byte(v))
}
return string(out), true
}
func (conn *gdbConn) memoryRegionInfo(addr uint64) (*memoryRegionInfo, error) {
conn.outbuf.Reset()
fmt.Fprintf(&conn.outbuf, "$qMemoryRegionInfo:%x", addr)
resp, err := conn.exec(conn.outbuf.Bytes(), "qMemoryRegionInfo")
if err != nil {
return nil, err
}
mri := &memoryRegionInfo{}
csp := colonSemicolonParser{buf: resp}
for csp.next() {
key, value := csp.key, csp.value
switch string(key) {
case "start":
start, err := strconv.ParseUint(string(value), 16, 64)
if err != nil {
return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (start): %v in %s", err, string(resp))
}
mri.start = start
case "size":
size, err := strconv.ParseUint(string(value), 16, 64)
if err != nil {
return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (size): %v in %s", err, string(resp))
}
mri.size = size
case "permissions":
mri.permissions = string(value)
case "name":
namestr, ok := decodeHexString(value)
if !ok {
return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (name): %s", string(resp))
}
mri.name = namestr
case "error":
errstr, ok := decodeHexString(value)
if !ok {
return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (error): %s", string(resp))
}
return nil, fmt.Errorf("qMemoryRegionInfo error: %s", errstr)
}
}
return mri, nil
}
// exec executes a message to the stub and reads a response.
// The details of the wire protocol are described here:
//
// https://sourceware.org/gdb/onlinedocs/gdb/Overview.html#Overview
func (conn *gdbConn) exec(cmd []byte, context string) ([]byte, error) {
if err := conn.send(cmd); err != nil {
return nil, err
}
return conn.recv(cmd, context, false)
}
const hexdigit = "0123456789abcdef"
func (conn *gdbConn) send(cmd []byte) error {
if len(cmd) == 0 || cmd[0] != '$' {
panic("gdb protocol error: command doesn't start with '$'")
}
// append checksum to packet
cmd = append(cmd, '#')
sum := checksum(cmd)
cmd = append(cmd, hexdigit[sum>>4], hexdigit[sum&0xf])
attempt := 0
for {
if logflags.GdbWire() {
if len(cmd) > gdbWireMaxLen {
conn.log.Debugf("<- %s...", string(cmd[:gdbWireMaxLen]))
} else {
conn.log.Debugf("<- %s", string(cmd))
}
}
_, err := conn.conn.Write(cmd)
if err != nil {
return err
}
if !conn.ack {
break
}
if conn.readack() {
break
}
if attempt > conn.maxTransmitAttempts {
return ErrTooManyAttempts
}
attempt++
}
return nil
}
func (conn *gdbConn) recv(cmd []byte, context string, binary bool) (resp []byte, err error) {
attempt := 0
for {
var err error
resp, err = conn.rdr.ReadBytes('#')
if err != nil {
return nil, err
}
// read checksum
_, err = io.ReadFull(conn.rdr, conn.inbuf[:2])
if err != nil {
return nil, err
}
if logflags.GdbWire() {
out := resp
partial := false
if !binary {
if idx := bytes.Index(out, []byte{'\n'}); idx >= 0 {
out = resp[:idx]
partial = true
}
}
if len(out) > gdbWireMaxLen {
out = out[:gdbWireMaxLen]
partial = true
}
if !partial {
if binary {
conn.log.Debugf("-> %q%s", string(resp), string(conn.inbuf[:2]))
} else {
conn.log.Debugf("-> %s%s", string(resp), string(conn.inbuf[:2]))
}
} else {
if binary {
conn.log.Debugf("-> %q...", string(out))
} else {
conn.log.Debugf("-> %s...", string(out))
}
}
}
if !conn.ack {
break
}
if resp[0] == '%' {
// If the first character is a % (instead of $) the stub sent us a
// notification packet, this is weird since we specifically claimed that
// we don't support notifications of any kind, but it should be safe to
// ignore regardless.
continue
}
if checksumok(resp, conn.inbuf[:2]) {
conn.sendack('+')
break
}
if attempt > conn.maxTransmitAttempts {
conn.sendack('+')
return nil, ErrTooManyAttempts
}
attempt++
conn.sendack('-')
}
if binary {
conn.inbuf, resp = binarywiredecode(resp, conn.inbuf)
} else {
conn.inbuf, resp = wiredecode(resp, conn.inbuf)
}
if len(resp) == 0 || (resp[0] == 'E' && !binary) || (resp[0] == 'E' && len(resp) == 3) {
cmdstr := ""
if cmd != nil {
cmdstr = string(cmd)
}
return nil, &GdbProtocolError{context, cmdstr, string(resp)}
}
return resp, nil
}
// Readack reads one byte from stub, returns true if the byte is '+'
func (conn *gdbConn) readack() bool {
b, err := conn.rdr.ReadByte()
if err != nil {
return false
}
conn.log.Debugf("-> %s", string(b))
return b == '+'
}
// Sendack executes an ack character, c must be either '+' or '-'
func (conn *gdbConn) sendack(c byte) {
if c != '+' && c != '-' {
panic(fmt.Errorf("sendack(%c)", c))
}
conn.conn.Write([]byte{c})
conn.log.Debugf("<- %s", string(c))
}
// escapeXor is the value mandated by the specification to escape characters
const escapeXor byte = 0x20
// wiredecode decodes the contents of in into buf.
// If buf is nil it will be allocated ex-novo, if the size of buf is not
// enough to hold the decoded contents it will be grown.
// Returns the newly allocated buffer as newbuf and the message contents as
// msg.
func wiredecode(in, buf []byte) (newbuf, msg []byte) {
if buf != nil {
buf = buf[:0]
} else {
buf = make([]byte, 0, 256)
}
start := 1
for i := 0; i < len(in); i++ {
switch ch := in[i]; ch {
case '{': // escape
if i+1 >= len(in) {
buf = append(buf, ch)
} else {
buf = append(buf, in[i+1]^escapeXor)
i++
}
case ':':
buf = append(buf, ch)
if i == 3 {
// we just read the sequence identifier
start = i + 1
}
case '#': // end of packet
return buf, buf[start:]
case '*': // runlength encoding marker
if i+1 >= len(in) || i == 0 {
buf = append(buf, ch)
} else {
n := in[i+1] - 29
r := buf[len(buf)-1]
for j := uint8(0); j < n; j++ {
buf = append(buf, r)
}
i++
}
default:
buf = append(buf, ch)
}
}
return buf, buf[start:]
}
// binarywiredecode is like wiredecode but decodes the wire encoding for
// binary packets, such as the 'x' and 'X' packets as well as all the json
// packets used by lldb/debugserver.
func binarywiredecode(in, buf []byte) (newbuf, msg []byte) {
if buf != nil {
buf = buf[:0]
} else {
buf = make([]byte, 0, 256)
}
start := 1
for i := 0; i < len(in); i++ {
switch ch := in[i]; ch {
case '}': // escape
if i+1 >= len(in) {
buf = append(buf, ch)
} else {
buf = append(buf, in[i+1]^escapeXor)
i++
}
case '#': // end of packet
return buf, buf[start:]
default:
buf = append(buf, ch)
}
}
return buf, buf[start:]
}
// checksumok checks that checksum is a valid checksum for packet.
func checksumok(packet, checksumBuf []byte) bool {
if packet[0] != '$' {
return false
}
sum := checksum(packet)
tgt, err := strconv.ParseUint(string(checksumBuf), 16, 8)
if err != nil {
return false
}
tgt8 := uint8(tgt)
return sum == tgt8
}
func checksum(packet []byte) (sum uint8) {
for i := 1; i < len(packet); i++ {
if packet[i] == '#' {
return sum
}
sum += packet[i]
}
return sum
}
// colonSemicolonParser parses a string in the form:
//
// key1:value1;key2:value2;...;keyN:valueN
type colonSemicolonParser struct {
buf []byte
key, value []byte
}
func (csp *colonSemicolonParser) next() bool {
if len(csp.buf) == 0 {
return false
}
colon := bytes.IndexByte(csp.buf, ':')
if colon < 0 {
return false
}
csp.key = csp.buf[:colon]
csp.buf = csp.buf[colon+1:]
semicolon := bytes.IndexByte(csp.buf, ';')
if semicolon < 0 {
csp.value = csp.buf
csp.buf = nil
} else {
csp.value = csp.buf[:semicolon]
csp.buf = csp.buf[semicolon+1:]
}
return true
}