diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index b1825f8e..a57a26ac 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -563,15 +563,16 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile switch APIVersion { case 1, 2: server = rpccommon.NewServer(&service.Config{ - Listener: listener, - ProcessArgs: processArgs, - AttachPid: attachPid, - AcceptMulti: AcceptMulti, - APIVersion: APIVersion, - WorkingDir: WorkingDir, - Backend: Backend, - CoreFile: coreFile, - Foreground: Headless, + Listener: listener, + ProcessArgs: processArgs, + AttachPid: attachPid, + AcceptMulti: AcceptMulti, + APIVersion: APIVersion, + WorkingDir: WorkingDir, + Backend: Backend, + CoreFile: coreFile, + Foreground: Headless, + DebugInfoDirectories: conf.DebugInfoDirectories, DisconnectChan: disconnectChan, }) diff --git a/pkg/config/config.go b/pkg/config/config.go index 3e51e25e..9f9becde 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -43,10 +43,14 @@ type Config struct { // If ShowLocationExpr is true whatis will print the DWARF location // expression for its argument. ShowLocationExpr bool `yaml:"show-location-expr"` - + // Source list line-number color (3/4 bit color codes as defined // here: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) SourceListLineColor int `yaml:"source-list-line-color"` + + // DebugFileDirectories is the list of directories Delve will use + // in order to resolve external debug info files. + DebugInfoDirectories []string `yaml:"debug-info-directories"` } // LoadConfig attempts to populate a Config object from the config.yml file. @@ -160,6 +164,9 @@ substitute-path: # Uncomment the following line to make the whatis command also print the DWARF location expression of its argument. # show-location-expr: true + +# List of directories to use when searching for separate debug info files. +debug-info-directories: ["/usr/lib/debug/.build-id"] `) return err } diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 7d2fbf29..9b8dea1d 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "os" + "path/filepath" "sort" "strings" "sync" @@ -27,6 +28,8 @@ import ( // BinaryInfo holds information on the binary being executed. type BinaryInfo struct { + // Path on disk of the binary being executed. + Path string // Architecture of this binary. Arch Arch @@ -91,6 +94,9 @@ var ErrUnsupportedDarwinArch = errors.New("unsupported architecture - only darwi // position independant executable. var ErrCouldNotDetermineRelocation = errors.New("could not determine the base address of a PIE") +// ErrNoDebugInfoFound is returned when Delve cannot find the external debug information file. +var ErrNoDebugInfoFound = errors.New("could not find external debug info file") + const dwarfGoLanguage = 22 // DW_LANG_Go (from DWARF v5, section 7.12, page 231) type compileUnit struct { @@ -298,22 +304,22 @@ func NewBinaryInfo(goos, goarch string) *BinaryInfo { // LoadBinaryInfo will load and store the information from the binary at 'path'. // It is expected this will be called in parallel with other initialization steps // so a sync.WaitGroup must be provided. -func (bi *BinaryInfo) LoadBinaryInfo(path string, entryPoint uint64, wg *sync.WaitGroup) error { +func (bi *BinaryInfo) LoadBinaryInfo(path string, entryPoint uint64, debugInfoDirs []string, wg *sync.WaitGroup) error { fi, err := os.Stat(path) if err == nil { bi.lastModified = fi.ModTime() } + bi.Path = path switch bi.GOOS { case "linux": - return bi.LoadBinaryInfoElf(path, entryPoint, wg) + return bi.LoadBinaryInfoElf(path, entryPoint, debugInfoDirs, wg) case "windows": return bi.LoadBinaryInfoPE(path, entryPoint, wg) case "darwin": return bi.LoadBinaryInfoMacho(path, entryPoint, wg) } return errors.New("unsupported operating system") - return nil } // GStructOffset returns the offset of the G @@ -563,35 +569,32 @@ func (e *ErrNoBuildIDNote) Error() string { // in GDB's documentation [1], and if found returns two handles, one // for the bare file, and another for its corresponding elf.File. // [1] https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html -func (bi *BinaryInfo) openSeparateDebugInfo(exe *elf.File) (*os.File, *elf.File, error) { - buildid := exe.Section(".note.gnu.build-id") - if buildid == nil { - return nil, nil, &ErrNoBuildIDNote{} +// +// Alternatively, if the debug file cannot be found be the build-id, Delve +// will look in directories specified by the debug-info-directories config value. +func (bi *BinaryInfo) openSeparateDebugInfo(exe *elf.File, debugInfoDirectories []string) (*os.File, *elf.File, error) { + var debugFilePath string + for _, dir := range debugInfoDirectories { + var potentialDebugFilePath string + if strings.Contains(dir, "build-id") { + desc1, desc2, err := parseBuildID(exe) + if err != nil { + continue + } + potentialDebugFilePath = fmt.Sprintf("%s/%s/%s.debug", dir, desc1, desc2) + } else { + potentialDebugFilePath = fmt.Sprintf("%s/%s.debug", dir, filepath.Base(bi.Path)) + } + _, err := os.Stat(potentialDebugFilePath) + if err == nil { + debugFilePath = potentialDebugFilePath + break + } } - - br := buildid.Open() - bh := new(buildIDHeader) - if err := binary.Read(br, binary.LittleEndian, bh); err != nil { - return nil, nil, errors.New("can't read build-id header: " + err.Error()) + if debugFilePath == "" { + return nil, nil, ErrNoDebugInfoFound } - - name := make([]byte, bh.Namesz) - if err := binary.Read(br, binary.LittleEndian, name); err != nil { - return nil, nil, errors.New("can't read build-id name: " + err.Error()) - } - - if strings.TrimSpace(string(name)) != "GNU\x00" { - return nil, nil, errors.New("invalid build-id signature") - } - - descBinary := make([]byte, bh.Descsz) - if err := binary.Read(br, binary.LittleEndian, descBinary); err != nil { - return nil, nil, errors.New("can't read build-id desc: " + err.Error()) - } - desc := hex.EncodeToString(descBinary) - - debugPath := fmt.Sprintf("/usr/lib/debug/.build-id/%s/%s.debug", desc[:2], desc[2:]) - sepFile, err := os.OpenFile(debugPath, 0, os.ModePerm) + sepFile, err := os.OpenFile(debugFilePath, 0, os.ModePerm) if err != nil { return nil, nil, errors.New("can't open separate debug file: " + err.Error()) } @@ -599,19 +602,48 @@ func (bi *BinaryInfo) openSeparateDebugInfo(exe *elf.File) (*os.File, *elf.File, elfFile, err := elf.NewFile(sepFile) if err != nil { sepFile.Close() - return nil, nil, fmt.Errorf("can't open separate debug file %q: %v", debugPath, err.Error()) + return nil, nil, fmt.Errorf("can't open separate debug file %q: %v", debugFilePath, err.Error()) } if elfFile.Machine != elf.EM_X86_64 { sepFile.Close() - return nil, nil, fmt.Errorf("can't open separate debug file %q: %v", debugPath, ErrUnsupportedLinuxArch.Error()) + return nil, nil, fmt.Errorf("can't open separate debug file %q: %v", debugFilePath, ErrUnsupportedLinuxArch.Error()) } return sepFile, elfFile, nil } +func parseBuildID(exe *elf.File) (string, string, error) { + buildid := exe.Section(".note.gnu.build-id") + if buildid == nil { + return "", "", &ErrNoBuildIDNote{} + } + + br := buildid.Open() + bh := new(buildIDHeader) + if err := binary.Read(br, binary.LittleEndian, bh); err != nil { + return "", "", errors.New("can't read build-id header: " + err.Error()) + } + + name := make([]byte, bh.Namesz) + if err := binary.Read(br, binary.LittleEndian, name); err != nil { + return "", "", errors.New("can't read build-id name: " + err.Error()) + } + + if strings.TrimSpace(string(name)) != "GNU\x00" { + return "", "", errors.New("invalid build-id signature") + } + + descBinary := make([]byte, bh.Descsz) + if err := binary.Read(br, binary.LittleEndian, descBinary); err != nil { + return "", "", errors.New("can't read build-id desc: " + err.Error()) + } + desc := hex.EncodeToString(descBinary) + return desc[:2], desc[2:], nil +} + // LoadBinaryInfoElf specifically loads information from an ELF binary. -func (bi *BinaryInfo) LoadBinaryInfoElf(path string, entryPoint uint64, wg *sync.WaitGroup) error { +func (bi *BinaryInfo) LoadBinaryInfoElf(path string, entryPoint uint64, debugInfoDirectories []string, wg *sync.WaitGroup) error { exe, err := os.OpenFile(path, 0, os.ModePerm) if err != nil { return err @@ -639,7 +671,7 @@ func (bi *BinaryInfo) LoadBinaryInfoElf(path string, entryPoint uint64, wg *sync if err != nil { var sepFile *os.File var serr error - sepFile, dwarfFile, serr = bi.openSeparateDebugInfo(elfFile) + sepFile, dwarfFile, serr = bi.openSeparateDebugInfo(elfFile, debugInfoDirectories) if serr != nil { if _, ok := serr.(*ErrNoBuildIDNote); ok { return err diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go index 96227cf7..1446fe4c 100644 --- a/pkg/proc/core/core.go +++ b/pkg/proc/core/core.go @@ -185,14 +185,16 @@ var ( ) // OpenCore will open the core file and return a Process struct. -func OpenCore(corePath, exePath string) (*Process, error) { +// If the DWARF information cannot be found in the binary, Delve will look +// for external debug files in the directories passed in. +func OpenCore(corePath, exePath string, debugInfoDirs []string) (*Process, error) { p, err := readLinuxAMD64Core(corePath, exePath) if err != nil { return nil, err } var wg sync.WaitGroup - err = p.bi.LoadBinaryInfo(exePath, p.entryPoint, &wg) + err = p.bi.LoadBinaryInfo(exePath, p.entryPoint, debugInfoDirs, &wg) wg.Wait() if err == nil { err = p.bi.LoadError() diff --git a/pkg/proc/core/core_test.go b/pkg/proc/core/core_test.go index 5814085b..12c90867 100644 --- a/pkg/proc/core/core_test.go +++ b/pkg/proc/core/core_test.go @@ -172,7 +172,7 @@ func withCoreFile(t *testing.T, name, args string) *Process { } corePath := cores[0] - p, err := OpenCore(corePath, fix.Path) + p, err := OpenCore(corePath, fix.Path, []string{}) if err != nil { t.Errorf("ReadCore(%q) failed: %v", corePath, err) pat, err := ioutil.ReadFile("/proc/sys/kernel/core_pattern") diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index a8317a99..2bbd0f55 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -205,7 +205,7 @@ func New(process *os.Process) *Process { } // Listen waits for a connection from the stub. -func (p *Process) Listen(listener net.Listener, path string, pid int) error { +func (p *Process) Listen(listener net.Listener, path string, pid int, debugInfoDirs []string) error { acceptChan := make(chan net.Conn) go func() { @@ -219,7 +219,7 @@ func (p *Process) Listen(listener net.Listener, path string, pid int) error { if conn == nil { return errors.New("could not connect") } - return p.Connect(conn, path, pid) + return p.Connect(conn, path, pid, debugInfoDirs) case status := <-p.waitChan: listener.Close() return fmt.Errorf("stub exited while waiting for connection: %v", status) @@ -227,11 +227,11 @@ func (p *Process) Listen(listener net.Listener, path string, pid int) error { } // Dial attempts to connect to the stub. -func (p *Process) Dial(addr string, path string, pid int) error { +func (p *Process) Dial(addr string, path string, pid int, debugInfoDirs []string) error { for { conn, err := net.Dial("tcp", addr) if err == nil { - return p.Connect(conn, path, pid) + return p.Connect(conn, path, pid, debugInfoDirs) } select { case status := <-p.waitChan: @@ -248,7 +248,7 @@ func (p *Process) Dial(addr string, path string, pid int) error { // program and the PID of the target process, both are optional, however // some stubs do not provide ways to determine path and pid automatically // and Connect will be unable to function without knowing them. -func (p *Process) Connect(conn net.Conn, path string, pid int) error { +func (p *Process) Connect(conn net.Conn, path string, pid int, debugInfoDirs []string) error { p.conn.conn = conn p.conn.pid = pid @@ -312,7 +312,7 @@ func (p *Process) Connect(conn net.Conn, path string, pid int) error { } var wg sync.WaitGroup - err = p.bi.LoadBinaryInfo(path, entryPoint, &wg) + err = p.bi.LoadBinaryInfo(path, entryPoint, debugInfoDirs, &wg) wg.Wait() if err == nil { err = p.bi.LoadError() @@ -405,7 +405,7 @@ func getLdEnvVars() []string { // LLDBLaunch starts an instance of lldb-server and connects to it, asking // it to launch the specified target program with the specified arguments // (cmd) on the specified directory wd. -func LLDBLaunch(cmd []string, wd string, foreground bool) (*Process, error) { +func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string) (*Process, error) { switch runtime.GOOS { case "windows": return nil, ErrUnsupportedOS @@ -482,9 +482,9 @@ func LLDBLaunch(cmd []string, wd string, foreground bool) (*Process, error) { p.conn.isDebugserver = isDebugserver if listener != nil { - err = p.Listen(listener, cmd[0], 0) + err = p.Listen(listener, cmd[0], 0, debugInfoDirs) } else { - err = p.Dial(port, cmd[0], 0) + err = p.Dial(port, cmd[0], 0, debugInfoDirs) } if err != nil { return nil, err @@ -497,13 +497,13 @@ func LLDBLaunch(cmd []string, wd string, foreground bool) (*Process, error) { // Path is path to the target's executable, path only needs to be specified // for some stubs that do not provide an automated way of determining it // (for example debugserver). -func LLDBAttach(pid int, path string) (*Process, error) { +func LLDBAttach(pid int, path string, debugInfoDirs []string) (*Process, error) { if runtime.GOOS == "windows" { return nil, ErrUnsupportedOS } isDebugserver := false - var proc *exec.Cmd + var process *exec.Cmd var listener net.Listener var port string if _, err := os.Stat(debugserverExecutable); err == nil { @@ -512,32 +512,32 @@ func LLDBAttach(pid int, path string) (*Process, error) { if err != nil { return nil, err } - proc = exec.Command(debugserverExecutable, "-R", fmt.Sprintf("127.0.0.1:%d", listener.Addr().(*net.TCPAddr).Port), "--attach="+strconv.Itoa(pid)) + process = exec.Command(debugserverExecutable, "-R", fmt.Sprintf("127.0.0.1:%d", listener.Addr().(*net.TCPAddr).Port), "--attach="+strconv.Itoa(pid)) } else { if _, err := exec.LookPath("lldb-server"); err != nil { return nil, &ErrBackendUnavailable{} } port = unusedPort() - proc = exec.Command("lldb-server", "gdbserver", "--attach", strconv.Itoa(pid), port) + process = exec.Command("lldb-server", "gdbserver", "--attach", strconv.Itoa(pid), port) } - proc.Stdout = os.Stdout - proc.Stderr = os.Stderr + process.Stdout = os.Stdout + process.Stderr = os.Stderr - proc.SysProcAttr = sysProcAttr(false) + process.SysProcAttr = sysProcAttr(false) - err := proc.Start() + err := process.Start() if err != nil { return nil, err } - p := New(proc.Process) + p := New(process.Process) p.conn.isDebugserver = isDebugserver if listener != nil { - err = p.Listen(listener, path, pid) + err = p.Listen(listener, path, pid, debugInfoDirs) } else { - err = p.Dial(port, path, pid) + err = p.Dial(port, path, pid, debugInfoDirs) } if err != nil { return nil, err diff --git a/pkg/proc/gdbserial/rr.go b/pkg/proc/gdbserial/rr.go index 5ee41dca..a2f5decb 100644 --- a/pkg/proc/gdbserial/rr.go +++ b/pkg/proc/gdbserial/rr.go @@ -53,7 +53,7 @@ func Record(cmd []string, wd string, quiet bool) (tracedir string, err error) { // Replay starts an instance of rr in replay mode, with the specified trace // directory, and connects to it. -func Replay(tracedir string, quiet bool) (*Process, error) { +func Replay(tracedir string, quiet bool, debugInfoDirs []string) (*Process, error) { if err := checkRRAvailabe(); err != nil { return nil, err } @@ -82,7 +82,7 @@ func Replay(tracedir string, quiet bool) (*Process, error) { p := New(rrcmd.Process) p.tracedir = tracedir - err = p.Dial(init.port, init.exe, 0) + err = p.Dial(init.port, init.exe, 0, debugInfoDirs) if err != nil { rrcmd.Process.Kill() return nil, err @@ -257,11 +257,11 @@ func splitQuotedFields(in string) []string { } // RecordAndReplay acts like calling Record and then Replay. -func RecordAndReplay(cmd []string, wd string, quiet bool) (p *Process, tracedir string, err error) { +func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string) (p *Process, tracedir string, err error) { tracedir, err = Record(cmd, wd, quiet) if tracedir == "" { return nil, "", err } - p, err = Replay(tracedir, quiet) + p, err = Replay(tracedir, quiet, debugInfoDirs) return p, tracedir, err } diff --git a/pkg/proc/gdbserial/rr_test.go b/pkg/proc/gdbserial/rr_test.go index 3cd5f916..fa31ac94 100644 --- a/pkg/proc/gdbserial/rr_test.go +++ b/pkg/proc/gdbserial/rr_test.go @@ -30,7 +30,7 @@ func withTestRecording(name string, t testing.TB, fn func(p *gdbserial.Process, t.Skip("test skipped, rr not found") } t.Log("recording") - p, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true) + p, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{}) if err != nil { t.Fatal("Launch():", err) } diff --git a/pkg/proc/native/nonative_darwin.go b/pkg/proc/native/nonative_darwin.go index f5e5d96b..b01a2d93 100644 --- a/pkg/proc/native/nonative_darwin.go +++ b/pkg/proc/native/nonative_darwin.go @@ -12,12 +12,12 @@ import ( var ErrNativeBackendDisabled = errors.New("native backend disabled during compilation") // Launch returns ErrNativeBackendDisabled. -func Launch(cmd []string, wd string, foreground bool) (*Process, error) { +func Launch(cmd []string, wd string, foreground bool, _ []string) (*Process, error) { return nil, ErrNativeBackendDisabled } // Attach returns ErrNativeBackendDisabled. -func Attach(pid int) (*Process, error) { +func Attach(pid int, _ []string) (*Process, error) { return nil, ErrNativeBackendDisabled } diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go index 1d6d3a47..e56fd01b 100644 --- a/pkg/proc/native/proc.go +++ b/pkg/proc/native/proc.go @@ -189,7 +189,7 @@ func (dbp *Process) Breakpoints() *proc.BreakpointMap { // * Dwarf .debug_frame section // * Dwarf .debug_line section // * Go symbol table. -func (dbp *Process) LoadInformation(path string) error { +func (dbp *Process) LoadInformation(path string, debugInfoDirs []string) error { var wg sync.WaitGroup path = findExecutable(path, dbp.pid) @@ -201,7 +201,7 @@ func (dbp *Process) LoadInformation(path string) error { wg.Add(1) go dbp.loadProcessInformation(&wg) - err = dbp.bi.LoadBinaryInfo(path, entryPoint, &wg) + err = dbp.bi.LoadBinaryInfo(path, entryPoint, debugInfoDirs, &wg) wg.Wait() if err == nil { err = dbp.bi.LoadError() @@ -380,8 +380,8 @@ func (dbp *Process) FindBreakpoint(pc uint64) (*proc.Breakpoint, bool) { } // Returns a new Process struct. -func initializeDebugProcess(dbp *Process, path string) (*Process, error) { - err := dbp.LoadInformation(path) +func initializeDebugProcess(dbp *Process, path string, debugInfoDirs []string) (*Process, error) { + err := dbp.LoadInformation(path, debugInfoDirs) if err != nil { return dbp, err } diff --git a/pkg/proc/native/proc_darwin.go b/pkg/proc/native/proc_darwin.go index 84cf3642..d90b87b6 100644 --- a/pkg/proc/native/proc_darwin.go +++ b/pkg/proc/native/proc_darwin.go @@ -38,7 +38,7 @@ type OSProcessDetails struct { // custom fork/exec process in order to take advantage of // PT_SIGEXC on Darwin which will turn Unix signals into // Mach exceptions. -func Launch(cmd []string, wd string, foreground bool) (*Process, error) { +func Launch(cmd []string, wd string, foreground bool, _ []string) (*Process, error) { // check that the argument to Launch is an executable file if fi, staterr := os.Stat(cmd[0]); staterr == nil && (fi.Mode()&0111) == 0 { return nil, proc.ErrNotExecutable @@ -119,7 +119,7 @@ func Launch(cmd []string, wd string, foreground bool) (*Process, error) { } dbp.os.initialized = true - dbp, err = initializeDebugProcess(dbp, argv0Go) + dbp, err = initializeDebugProcess(dbp, argv0Go, []string{}) if err != nil { return nil, err } @@ -132,7 +132,7 @@ func Launch(cmd []string, wd string, foreground bool) (*Process, error) { } // Attach to an existing process with the given PID. -func Attach(pid int) (*Process, error) { +func Attach(pid int, _ []string) (*Process, error) { dbp := New(pid) kret := C.acquire_mach_task(C.int(pid), @@ -155,7 +155,7 @@ func Attach(pid int) (*Process, error) { return nil, err } - dbp, err = initializeDebugProcess(dbp, "") + dbp, err = initializeDebugProcess(dbp, "", []string{}) if err != nil { dbp.Detach(false) return nil, err diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go index 6527f3e2..93a874ed 100644 --- a/pkg/proc/native/proc_linux.go +++ b/pkg/proc/native/proc_linux.go @@ -46,7 +46,9 @@ type OSProcessDetails struct { // Launch creates and begins debugging a new process. First entry in // `cmd` is the program to run, and then rest are the arguments // to be supplied to that process. `wd` is working directory of the program. -func Launch(cmd []string, wd string, foreground bool) (*Process, error) { +// If the DWARF information cannot be found in the binary, Delve will look +// for external debug files in the directories passed in. +func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string) (*Process, error) { var ( process *exec.Cmd err error @@ -88,11 +90,13 @@ func Launch(cmd []string, wd string, foreground bool) (*Process, error) { if err != nil { return nil, fmt.Errorf("waiting for target execve failed: %s", err) } - return initializeDebugProcess(dbp, process.Path) + return initializeDebugProcess(dbp, process.Path, debugInfoDirs) } -// Attach to an existing process with the given PID. -func Attach(pid int) (*Process, error) { +// Attach to an existing process with the given PID. Once attached, if +// the DWARF information cannot be found in the binary, Delve will look +// for external debug files in the directories passed in. +func Attach(pid int, debugInfoDirs []string) (*Process, error) { dbp := New(pid) dbp.common = proc.NewCommonProcess(true) @@ -106,7 +110,7 @@ func Attach(pid int) (*Process, error) { return nil, err } - dbp, err = initializeDebugProcess(dbp, "") + dbp, err = initializeDebugProcess(dbp, "", debugInfoDirs) if err != nil { dbp.Detach(false) return nil, err diff --git a/pkg/proc/native/proc_windows.go b/pkg/proc/native/proc_windows.go index e56e4fa5..222bd307 100644 --- a/pkg/proc/native/proc_windows.go +++ b/pkg/proc/native/proc_windows.go @@ -37,7 +37,7 @@ func openExecutablePathPE(path string) (*pe.File, io.Closer, error) { } // Launch creates and begins debugging a new process. -func Launch(cmd []string, wd string, foreground bool) (*Process, error) { +func Launch(cmd []string, wd string, foreground bool, _ []string) (*Process, error) { argv0Go, err := filepath.Abs(cmd[0]) if err != nil { return nil, err @@ -115,7 +115,7 @@ func newDebugProcess(dbp *Process, exepath string) (*Process, error) { return nil, err } - return initializeDebugProcess(dbp, exepath) + return initializeDebugProcess(dbp, exepath, []string{}) } // findExePath searches for process pid, and returns its executable path. @@ -153,7 +153,7 @@ func findExePath(pid int) (string, error) { } // Attach to an existing process with the given PID. -func Attach(pid int) (*Process, error) { +func Attach(pid int, _ []string) (*Process, error) { // TODO: Probably should have SeDebugPrivilege before starting here. err := _DebugActiveProcess(uint32(pid)) if err != nil { diff --git a/pkg/proc/proc_linux_test.go b/pkg/proc/proc_linux_test.go new file mode 100644 index 00000000..ae018143 --- /dev/null +++ b/pkg/proc/proc_linux_test.go @@ -0,0 +1,39 @@ +package proc_test + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/derekparker/delve/pkg/proc/native" + protest "github.com/derekparker/delve/pkg/proc/test" +) + +func TestLoadingExternalDebugInfo(t *testing.T) { + fixture := protest.BuildFixture("locationsprog", 0) + defer os.Remove(fixture.Path) + stripAndCopyDebugInfo(fixture, t) + p, err := native.Launch(append([]string{fixture.Path}, ""), "", false, []string{filepath.Dir(fixture.Path)}) + if err != nil { + t.Fatal(err) + } + p.Detach(true) +} + +func stripAndCopyDebugInfo(f protest.Fixture, t *testing.T) { + name := filepath.Base(f.Path) + // Copy the debug information to an external file. + copyCmd := exec.Command("objcopy", "--only-keep-debug", name, name+".debug") + copyCmd.Dir = filepath.Dir(f.Path) + if err := copyCmd.Run(); err != nil { + t.Fatal(err) + } + + // Strip the original binary of the debug information. + stripCmd := exec.Command("strip", "--strip-debug", "--strip-unneeded", name) + stripCmd.Dir = filepath.Dir(f.Path) + if err := stripCmd.Run(); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 5865b7b1..2cccf463 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -66,13 +66,13 @@ func withTestProcessArgs(name string, t testing.TB, wd string, args []string, bu switch testBackend { case "native": - p, err = native.Launch(append([]string{fixture.Path}, args...), wd, false) + p, err = native.Launch(append([]string{fixture.Path}, args...), wd, false, []string{}) case "lldb": - p, err = gdbserial.LLDBLaunch(append([]string{fixture.Path}, args...), wd, false) + p, err = gdbserial.LLDBLaunch(append([]string{fixture.Path}, args...), wd, false, []string{}) case "rr": protest.MustHaveRecordingAllowed(t) t.Log("recording") - p, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true) + p, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true, []string{}) t.Logf("replaying %q", tracedir) default: t.Fatal("unknown backend") @@ -2048,9 +2048,9 @@ func TestIssue509(t *testing.T) { switch testBackend { case "native": - _, err = native.Launch([]string{exepath}, ".", false) + _, err = native.Launch([]string{exepath}, ".", false, []string{}) case "lldb": - _, err = gdbserial.LLDBLaunch([]string{exepath}, ".", false) + _, err = gdbserial.LLDBLaunch([]string{exepath}, ".", false, []string{}) default: t.Skip("test not valid for this backend") } @@ -2090,9 +2090,9 @@ func TestUnsupportedArch(t *testing.T) { switch testBackend { case "native": - p, err = native.Launch([]string{outfile}, ".", false) + p, err = native.Launch([]string{outfile}, ".", false, []string{}) case "lldb": - p, err = gdbserial.LLDBLaunch([]string{outfile}, ".", false) + p, err = gdbserial.LLDBLaunch([]string{outfile}, ".", false, []string{}) default: t.Skip("test not valid for this backend") } @@ -2839,13 +2839,13 @@ func TestAttachDetach(t *testing.T) { switch testBackend { case "native": - p, err = native.Attach(cmd.Process.Pid) + p, err = native.Attach(cmd.Process.Pid, []string{}) case "lldb": path := "" if runtime.GOOS == "darwin" { path = fixture.Path } - p, err = gdbserial.LLDBAttach(cmd.Process.Pid, path) + p, err = gdbserial.LLDBAttach(cmd.Process.Pid, path, []string{}) default: err = fmt.Errorf("unknown backend %q", testBackend) } @@ -3141,13 +3141,13 @@ func TestAttachStripped(t *testing.T) { switch testBackend { case "native": - p, err = native.Attach(cmd.Process.Pid) + p, err = native.Attach(cmd.Process.Pid, []string{}) case "lldb": path := "" if runtime.GOOS == "darwin" { path = fixture.Path } - p, err = gdbserial.LLDBAttach(cmd.Process.Pid, path) + p, err = gdbserial.LLDBAttach(cmd.Process.Pid, path, []string{}) default: t.Fatalf("unknown backend %q", testBackend) } diff --git a/service/config.go b/service/config.go index dcd128bb..837abcad 100644 --- a/service/config.go +++ b/service/config.go @@ -29,6 +29,10 @@ type Config struct { // CoreFile specifies the path to the core dump to open. CoreFile string + // DebugInfoDirectories is the list of directories to look for + // when resolving external debug info files. + DebugInfoDirectories []string + // Selects server backend. Backend string diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index b1a1642f..0fffa13e 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -65,6 +65,10 @@ type Config struct { // Foreground lets target process access stdin. Foreground bool + + // DebugInfoDirectories is the list of directories to look for + // when resolving external debug info files. + DebugInfoDirectories []string } // New creates a new Debugger. ProcessArgs specify the commandline arguments for the @@ -102,10 +106,10 @@ func New(config *Config, processArgs []string) (*Debugger, error) { switch d.config.Backend { case "rr": d.log.Infof("opening trace %s", d.config.CoreFile) - p, err = gdbserial.Replay(d.config.CoreFile, false) + p, err = gdbserial.Replay(d.config.CoreFile, false, d.config.DebugInfoDirectories) default: d.log.Infof("opening core file %s (executable %s)", d.config.CoreFile, d.processArgs[0]) - p, err = core.OpenCore(d.config.CoreFile, d.processArgs[0]) + p, err = core.OpenCore(d.config.CoreFile, d.processArgs[0], d.config.DebugInfoDirectories) } if err != nil { err = go11DecodeErrorCheck(err) @@ -132,17 +136,17 @@ func New(config *Config, processArgs []string) (*Debugger, error) { func (d *Debugger) Launch(processArgs []string, wd string) (proc.Process, error) { switch d.config.Backend { case "native": - return native.Launch(processArgs, wd, d.config.Foreground) + return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories) case "lldb": - return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground)) + return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories)) case "rr": - p, _, err := gdbserial.RecordAndReplay(processArgs, wd, false) + p, _, err := gdbserial.RecordAndReplay(processArgs, wd, false, d.config.DebugInfoDirectories) return p, err case "default": if runtime.GOOS == "darwin" { - return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground)) + return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories)) } - return native.Launch(processArgs, wd, d.config.Foreground) + return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories) default: return nil, fmt.Errorf("unknown backend %q", d.config.Backend) } @@ -157,14 +161,14 @@ var ErrNoAttachPath = errors.New("must specify executable path on macOS") func (d *Debugger) Attach(pid int, path string) (proc.Process, error) { switch d.config.Backend { case "native": - return native.Attach(pid) + return native.Attach(pid, d.config.DebugInfoDirectories) case "lldb": - return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path)) + return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path, d.config.DebugInfoDirectories)) case "default": if runtime.GOOS == "darwin" { - return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path)) + return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path, d.config.DebugInfoDirectories)) } - return native.Attach(pid) + return native.Attach(pid, d.config.DebugInfoDirectories) default: return nil, fmt.Errorf("unknown backend %q", d.config.Backend) } diff --git a/service/rpccommon/server.go b/service/rpccommon/server.go index a0cc4c3b..9e842bb2 100644 --- a/service/rpccommon/server.go +++ b/service/rpccommon/server.go @@ -121,11 +121,12 @@ func (s *ServerImpl) Run() error { // Create and start the debugger if s.debugger, err = debugger.New(&debugger.Config{ - AttachPid: s.config.AttachPid, - WorkingDir: s.config.WorkingDir, - CoreFile: s.config.CoreFile, - Backend: s.config.Backend, - Foreground: s.config.Foreground, + AttachPid: s.config.AttachPid, + WorkingDir: s.config.WorkingDir, + CoreFile: s.config.CoreFile, + Backend: s.config.Backend, + Foreground: s.config.Foreground, + DebugInfoDirectories: s.config.DebugInfoDirectories, }, s.config.ProcessArgs); err != nil { return err diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 0e29202a..579c6b07 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -117,13 +117,13 @@ func withTestProcess(name string, t *testing.T, fn func(p proc.Process, fixture var tracedir string switch testBackend { case "native": - p, err = native.Launch([]string{fixture.Path}, ".", false) + p, err = native.Launch([]string{fixture.Path}, ".", false, []string{}) case "lldb": - p, err = gdbserial.LLDBLaunch([]string{fixture.Path}, ".", false) + p, err = gdbserial.LLDBLaunch([]string{fixture.Path}, ".", false, []string{}) case "rr": protest.MustHaveRecordingAllowed(t) t.Log("recording") - p, tracedir, err = gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true) + p, tracedir, err = gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{}) t.Logf("replaying %q", tracedir) default: t.Fatalf("unknown backend %q", testBackend)