From 55e44c9dc09328d56a5a48a83fe9360558526a65 Mon Sep 17 00:00:00 2001 From: aarzilli Date: Mon, 14 Aug 2017 20:20:34 +0200 Subject: [PATCH] proc/gdbserial: automatically retrieve exe path on attach on macOS debugserver doesn't support qXfer:exec-file:read, and it doesn't return the executable path in the response to qProcessInfoPID, however we can find out the executable path by using jGetLoadedDynamicLibrariesInfos. --- pkg/proc/gdbserial/gdbserver.go | 13 +++++ pkg/proc/gdbserial/gdbserver_conn.go | 80 +++++++++++++++++++++++++--- service/debugger/debugger.go | 6 --- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index 17a6d2fc..6cda348e 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -63,6 +63,7 @@ package gdbserial import ( "bytes" + "debug/macho" "encoding/binary" "errors" "fmt" @@ -261,6 +262,18 @@ func (p *Process) Connect(conn net.Conn, path string, pid int) error { } } + if path == "" { + // try using jGetLoadedDynamicLibrariesInfos which is the only way to do + // this supported on debugserver (but only on macOS >= 12.10) + images, _ := p.conn.getLoadedDynamicLibraries() + for _, image := range images { + if image.MachHeader.FileType == macho.TypeExec { + path = image.Pathname + break + } + } + } + var wg sync.WaitGroup err = p.bi.LoadBinaryInfo(path, &wg) wg.Wait() diff --git a/pkg/proc/gdbserial/gdbserver_conn.go b/pkg/proc/gdbserial/gdbserver_conn.go index 1c4e84a5..c88573d6 100644 --- a/pkg/proc/gdbserial/gdbserver_conn.go +++ b/pkg/proc/gdbserial/gdbserver_conn.go @@ -3,6 +3,8 @@ package gdbserial import ( "bufio" "bytes" + "debug/macho" + "encoding/json" "encoding/xml" "errors" "fmt" @@ -582,7 +584,7 @@ func (conn *gdbConn) waitForvContStop(context string, threadID string, tu *threa failed := false for { conn.conn.SetReadDeadline(time.Now().Add(heartbeatInterval)) - resp, err := conn.recv(nil, context) + 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, @@ -935,6 +937,35 @@ func (conn *gdbConn) qRRCmd(args ...string) (string, error) { return string(data), nil } +type imageList struct { + Images []imageDescription `json:"images"` +} + +type imageDescription struct { + 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 +} + // 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 @@ -942,8 +973,7 @@ 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) - + return conn.recv(cmd, context, false) } var hexdigit = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'} @@ -988,7 +1018,7 @@ func (conn *gdbConn) send(cmd []byte) error { return nil } -func (conn *gdbConn) recv(cmd []byte, context string) (resp []byte, err error) { +func (conn *gdbConn) recv(cmd []byte, context string, binary bool) (resp []byte, err error) { attempt := 0 for { var err error @@ -1044,7 +1074,11 @@ func (conn *gdbConn) recv(cmd []byte, context string) (resp []byte, err error) { conn.sendack('-') } - conn.inbuf, resp = wiredecode(resp, conn.inbuf) + if binary { + conn.inbuf, resp = binarywiredecode(resp, conn.inbuf) + } else { + conn.inbuf, resp = wiredecode(resp, conn.inbuf) + } if len(resp) == 0 || resp[0] == 'E' { cmdstr := "" @@ -1080,6 +1114,9 @@ func (conn *gdbConn) sendack(c byte) { } } +// 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. @@ -1100,7 +1137,8 @@ func wiredecode(in, buf []byte) (newbuf, msg []byte) { if i+1 >= len(in) { buf = append(buf, ch) } else { - buf = append(buf, in[i+1]^0x20) + buf = append(buf, in[i+1]^escapeXor) + i++ } case ':': buf = append(buf, ch) @@ -1128,6 +1166,36 @@ func wiredecode(in, buf []byte) (newbuf, msg []byte) { 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] != '$' { diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 289e4182..c2c30206 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -136,15 +136,9 @@ func (d *Debugger) Attach(pid int, path string) (proc.Process, error) { case "native": return native.Attach(pid) case "lldb": - if runtime.GOOS == "darwin" && path == "" { - return nil, ErrNoAttachPath - } return gdbserial.LLDBAttach(pid, path) case "default": if runtime.GOOS == "darwin" { - if path == "" { - return nil, ErrNoAttachPath - } return gdbserial.LLDBAttach(pid, path) } return native.Attach(pid)