diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go index 06b8fd6020..aa1b100149 100644 --- a/cmd/dlv/dlv_test.go +++ b/cmd/dlv/dlv_test.go @@ -26,7 +26,6 @@ import ( "github.com/go-delve/delve/pkg/terminal" "github.com/go-delve/delve/service/dap" "github.com/go-delve/delve/service/dap/daptest" - "github.com/go-delve/delve/service/debugger" "github.com/go-delve/delve/service/rpc2" godap "github.com/google/go-dap" "golang.org/x/tools/go/packages" @@ -282,57 +281,6 @@ func TestContinue(t *testing.T) { cmd.Wait() } -// TestChildProcessExitWhenNoDebugInfo verifies that the child process exits when dlv launch the binary without debug info -func TestChildProcessExitWhenNoDebugInfo(t *testing.T) { - noDebugFlags := protest.LinkStrip - // -s doesn't strip symbols on Mac, use -w instead - if runtime.GOOS == "darwin" { - noDebugFlags = protest.LinkDisableDWARF - } - - if _, err := exec.LookPath("ps"); err != nil { - t.Skip("test skipped, `ps` not found") - } - - dlvbin := getDlvBin(t) - - fix := protest.BuildFixture("http_server", noDebugFlags) - - // dlv exec the binary file and expect error. - out, err := exec.Command(dlvbin, "exec", "--headless", "--log", fix.Path).CombinedOutput() - t.Log(string(out)) - if err == nil { - t.Fatalf("Expected err when launching the binary without debug info, but got nil") - } - // Test only for dlv's prefix of the error like "could not launch process: could not open debug info" - if !strings.Contains(string(out), "could not launch process") || !strings.Contains(string(out), debugger.NoDebugWarning) { - t.Fatalf("Expected logged error 'could not launch process: ... - %s'", debugger.NoDebugWarning) - } - - // search the running process named fix.Name - cmd := exec.Command("ps", "-aux") - stdout, err := cmd.StdoutPipe() - assertNoError(err, t, "stdout pipe") - defer stdout.Close() - - assertNoError(cmd.Start(), t, "start `ps -aux`") - - var foundFlag bool - scan := bufio.NewScanner(stdout) - for scan.Scan() { - t.Log(scan.Text()) - if strings.Contains(scan.Text(), fix.Name) { - foundFlag = true - break - } - } - cmd.Wait() - - if foundFlag { - t.Fatalf("Expected child process exited, but found it running") - } -} - // TestRedirect verifies that redirecting stdin works func TestRedirect(t *testing.T) { const listenAddr = "127.0.0.1:40573" @@ -711,57 +659,6 @@ func TestDAPCmd(t *testing.T) { cmd.Wait() } -func TestDAPCmdWithNoDebugBinary(t *testing.T) { - const listenAddr = "127.0.0.1:40579" - - dlvbin := getDlvBin(t) - - cmd := exec.Command(dlvbin, "dap", "--log", "--listen", listenAddr) - stdout, err := cmd.StdoutPipe() - assertNoError(err, t, "stdout pipe") - defer stdout.Close() - stderr, err := cmd.StderrPipe() - assertNoError(err, t, "stderr pipe") - defer stderr.Close() - assertNoError(cmd.Start(), t, "start dap instance") - - scanOut := bufio.NewScanner(stdout) - scanErr := bufio.NewScanner(stderr) - // Wait for the debug server to start - scanOut.Scan() - listening := "DAP server listening at: " + listenAddr - if scanOut.Text() != listening { - cmd.Process.Kill() // release the port - t.Fatalf("Unexpected stdout:\ngot %q\nwant %q", scanOut.Text(), listening) - } - go func() { // Capture logging - for scanErr.Scan() { - t.Log(scanErr.Text()) - } - }() - - // Exec the stripped debuggee and expect things to fail - noDebugFlags := protest.LinkStrip - // -s doesn't strip symbols on Mac, use -w instead - if runtime.GOOS == "darwin" { - noDebugFlags = protest.LinkDisableDWARF - } - fixture := protest.BuildFixture("increment", noDebugFlags) - go func() { - for scanOut.Scan() { - t.Errorf("Unexpected stdout: %s", scanOut.Text()) - } - }() - client := daptest.NewClient(listenAddr) - client.LaunchRequest("exec", fixture.Path, false) - client.ExpectErrorResponse(t) - client.DisconnectRequest() - client.ExpectDisconnectResponse(t) - client.ExpectTerminatedEvent(t) - client.Close() - cmd.Wait() -} - func newDAPRemoteClient(t *testing.T, addr string, isDlvAttach bool, isMulti bool) *daptest.Client { c := daptest.NewClient(addr) c.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true}) diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 4cd41dc4d8..3deafc6731 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -4,6 +4,7 @@ import ( "bytes" "debug/dwarf" "debug/elf" + "debug/gosym" "debug/macho" "debug/pe" "encoding/binary" @@ -332,7 +333,7 @@ func FindFunctionLocation(p Process, funcName string, lineOffset int) ([]uint64, if lineOffset > 0 { fn := origfns[0] - filename, lineno := fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry) + filename, lineno := bi.EntryLineForFunc(fn) return FindFileLocation(p, filename, lineno+lineOffset) } @@ -364,14 +365,16 @@ func FindFunctionLocation(p Process, funcName string, lineOffset int) ([]uint64, // If sameline is set FirstPCAfterPrologue will always return an // address associated with the same line as fn.Entry. func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error) { - pc, _, line, ok := fn.cu.lineInfo.PrologueEndPC(fn.Entry, fn.End) - if ok { - if !sameline { - return pc, nil - } - _, entryLine := fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry) - if entryLine == line { - return pc, nil + if fn.cu.lineInfo != nil { + pc, _, line, ok := fn.cu.lineInfo.PrologueEndPC(fn.Entry, fn.End) + if ok { + if !sameline { + return pc, nil + } + _, entryLine := p.BinInfo().EntryLineForFunc(fn) + if entryLine == line { + return pc, nil + } } } @@ -380,7 +383,7 @@ func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error return fn.Entry, err } - if pc == fn.Entry { + if pc == fn.Entry && fn.cu.lineInfo != nil { // Look for the first instruction with the stmt flag set, so that setting a // breakpoint with file:line and with the function name always result on // the same instruction being selected. @@ -601,6 +604,25 @@ func (fn *Function) PrologueEndPC() uint64 { return pc } +func (fn *Function) AllPCs(excludeFile string, excludeLine int) ([]uint64, error) { + if !fn.cu.image.Stripped() { + return fn.cu.lineInfo.AllPCsBetween(fn.Entry, fn.End-1, excludeFile, excludeLine) + } + var pcs []uint64 + fnFile, lastLine, _ := fn.cu.image.symTable.PCToLine(fn.Entry) + for pc := fn.Entry; pc < fn.End; pc++ { + f, line, pcfn := fn.cu.image.symTable.PCToLine(pc) + if pcfn == nil { + continue + } + if f == fnFile && line > lastLine { + lastLine = line + pcs = append(pcs, pc) + } + } + return pcs, nil +} + // From $GOROOT/src/runtime/traceback.go:597 // exportedRuntime reports whether the function is an exported runtime function. // It is only for runtime functions, so ASCII A-Z is fine. @@ -719,6 +741,9 @@ func (bi *BinaryInfo) LastModified() time.Time { // DwarfReader returns a reader for the dwarf data func (so *Image) DwarfReader() *reader.Reader { + if so.dwarf == nil { + return nil + } return reader.New(so.dwarf) } @@ -731,13 +756,26 @@ func (bi *BinaryInfo) Types() ([]string, error) { return types, nil } +func (bi *BinaryInfo) EntryLineForFunc(fn *Function) (string, int) { + return bi.pcToLine(fn, fn.Entry) +} + +func (bi *BinaryInfo) pcToLine(fn *Function, pc uint64) (string, int) { + if fn.cu.lineInfo == nil { + f, l, _ := fn.cu.image.symTable.PCToLine(pc) + return f, l + } + f, l := fn.cu.lineInfo.PCToLine(fn.Entry, pc) + return f, l +} + // PCToLine converts an instruction address to a file/line/function. func (bi *BinaryInfo) PCToLine(pc uint64) (string, int, *Function) { fn := bi.PCToFunc(pc) if fn == nil { return "", 0, nil } - f, ln := fn.cu.lineInfo.PCToLine(fn.Entry, pc) + f, ln := bi.pcToLine(fn, pc) return f, ln, fn } @@ -810,6 +848,8 @@ type Image struct { debugAddr *godwarf.DebugAddrSection debugLineStr []byte + symTable *gosym.Table + typeCache map[dwarf.Offset]godwarf.Type compileUnits []*compileUnit // compileUnits is sorted by increasing DWARF offset @@ -835,6 +875,10 @@ func (image *Image) registerRuntimeTypeToDIE(entry *dwarf.Entry, ardr *reader.Re } } +func (image *Image) Stripped() bool { + return image.dwarf == nil +} + // AddImage adds the specified image to bi, loading data asynchronously. // Addr is the relocated entry point for the executable and staticBase (i.e. // the relocation offset) for all other images. @@ -1405,7 +1449,22 @@ func loadBinaryInfoElf(bi *BinaryInfo, image *Image, path string, addr uint64, w var serr error sepFile, dwarfFile, serr = bi.openSeparateDebugInfo(image, elfFile, bi.DebugInfoDirectories) if serr != nil { - return serr + fmt.Fprintln(os.Stderr, "Warning: no debug info found, some functionality will be missing such as stack traces and variable evaluation.") + symTable, err := readPcLnTableElf(elfFile, path) + if err != nil { + return fmt.Errorf("could not create symbol table from %s ", path) + } + image.symTable = symTable + for _, f := range image.symTable.Funcs { + cu := &compileUnit{} + cu.image = image + fn := Function{Name: f.Name, Entry: f.Entry, End: f.End, cu: cu} + bi.Functions = append(bi.Functions, fn) + } + for f := range image.symTable.Files { + bi.Sources = append(bi.Sources, f) + } + return nil } image.sepDebugCloser = sepFile image.dwarf, err = dwarfFile.DWARF() diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go index 2e7cdf725c..c5910e319c 100644 --- a/pkg/proc/breakpoints.go +++ b/pkg/proc/breakpoints.go @@ -554,7 +554,7 @@ func (t *Target) setEBPFTracepointOnFunc(fn *Function, goidOffset int64) error { if t.BinInfo().Producer() != "" && goversion.ProducerAfterOrEqual(t.BinInfo().Producer(), 1, 15) { variablesFlags |= reader.VariablesTrustDeclLine } - _, l, _ := t.BinInfo().PCToLine(fn.Entry) + _, l := t.BinInfo().EntryLineForFunc(fn) var args []ebpf.UProbeArgMap varEntries := reader.Variables(dwarfTree, fn.Entry, l, variablesFlags) diff --git a/pkg/proc/fncall.go b/pkg/proc/fncall.go index bd0df51534..a1b94a7291 100644 --- a/pkg/proc/fncall.go +++ b/pkg/proc/fncall.go @@ -1053,7 +1053,7 @@ func readStackVariable(t *Target, thread Thread, regs Registers, off uint64, typ func fakeFunctionEntryScope(scope *EvalScope, fn *Function, cfa int64, sp uint64) error { scope.PC = fn.Entry scope.Fn = fn - scope.File, scope.Line, _ = scope.BinInfo.PCToLine(fn.Entry) + scope.File, scope.Line = scope.BinInfo.EntryLineForFunc(fn) scope.Regs.CFA = cfa scope.Regs.Reg(scope.Regs.SPRegNum).Uint64Val = sp diff --git a/pkg/proc/goroutine_cache.go b/pkg/proc/goroutine_cache.go index 70e7f61cf5..4020dad014 100644 --- a/pkg/proc/goroutine_cache.go +++ b/pkg/proc/goroutine_cache.go @@ -12,6 +12,9 @@ func (gcache *goroutineCache) init(bi *BinaryInfo) { exeimage := bi.Images[0] rdr := exeimage.DwarfReader() + if rdr == nil { + return + } gcache.allglenAddr, _ = rdr.AddrFor("runtime.allglen", exeimage.StaticBase, bi.Arch.PtrSize()) diff --git a/pkg/proc/pclntab.go b/pkg/proc/pclntab.go new file mode 100644 index 0000000000..3451533233 --- /dev/null +++ b/pkg/proc/pclntab.go @@ -0,0 +1,75 @@ +package proc + +import ( + "bytes" + "debug/buildinfo" + "debug/elf" + "debug/gosym" + "encoding/binary" + "fmt" + "strings" +) + +// From go/src/debug/gosym/pclntab.go +const ( + go12magic = 0xfffffffb + go116magic = 0xfffffffa + go118magic = 0xfffffff0 + go120magic = 0xfffffff1 +) + +// Select the magic number based on the Go version +func magicNumber(goVersion string) []byte { + bs := make([]byte, 4) + var magic uint32 + if strings.Compare(goVersion, "go1.20") >= 0 { + magic = go120magic + } else if strings.Compare(goVersion, "go1.18") >= 0 { + magic = go118magic + } else if strings.Compare(goVersion, "go1.16") >= 0 { + magic = go116magic + } else { + magic = go12magic + } + binary.LittleEndian.PutUint32(bs, magic) + return bs +} + +func readPcLnTableElf(exe *elf.File, path string) (*gosym.Table, error) { + // Default section label is .gopclntab + sectionLabel := ".gopclntab" + + section := exe.Section(sectionLabel) + if section == nil { + // binary may be built with -pie + sectionLabel = ".data.rel.ro" + section = exe.Section(sectionLabel) + if section == nil { + return nil, fmt.Errorf("could not read section .gopclntab") + } + } + tableData, err := section.Data() + if err != nil { + return nil, fmt.Errorf("found section but could not read .gopclntab") + } + + bi, err := buildinfo.ReadFile(path) + if err != nil { + return nil, err + } + + // Find .gopclntab by magic number even if there is no section label + magic := magicNumber(bi.GoVersion) + pclntabIndex := bytes.Index(tableData, magic) + if pclntabIndex < 0 { + return nil, fmt.Errorf("could not find magic number in %s ", path) + } + tableData = tableData[pclntabIndex:] + addr := exe.Section(".text").Addr + lineTable := gosym.NewLineTable(tableData, addr) + symTable, err := gosym.NewTable([]byte{}, lineTable) + if err != nil { + return nil, fmt.Errorf("could not create symbol table from %s ", path) + } + return symTable, nil +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 874434c85c..10f7876d70 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -3165,69 +3165,18 @@ func TestShadowedFlag(t *testing.T) { }) } -func TestAttachStripped(t *testing.T) { - if testBackend == "lldb" && runtime.GOOS == "linux" { - bs, _ := ioutil.ReadFile("/proc/sys/kernel/yama/ptrace_scope") - if bs == nil || strings.TrimSpace(string(bs)) != "0" { - t.Logf("can not run TestAttachStripped: %v\n", bs) - return - } - } - if testBackend == "rr" { - return - } - if runtime.GOOS == "darwin" { - t.Log("-s does not produce stripped executables on macOS") - return - } - if buildMode != "" { - t.Skip("not enabled with buildmode=PIE") - } - fixture := protest.BuildFixture("testnextnethttp", protest.LinkStrip) - cmd := exec.Command(fixture.Path) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assertNoError(cmd.Start(), t, "starting fixture") - - // wait for testnextnethttp to start listening - t0 := time.Now() - for { - conn, err := net.Dial("tcp", "127.0.0.1:9191") - if err == nil { - conn.Close() - break - } - time.Sleep(50 * time.Millisecond) - if time.Since(t0) > 10*time.Second { - t.Fatal("fixture did not start") - } - } - - var p *proc.TargetGroup - var err error - - switch testBackend { - case "native": - 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, []string{}) - default: - t.Fatalf("unknown backend %q", testBackend) - } - - t.Logf("error is %v", err) - - if err == nil { - p.Detach(true) - t.Fatalf("expected error after attach, got nothing") - } else { - cmd.Process.Kill() - } - os.Remove(fixture.Path) +func TestDebugStripped(t *testing.T) { + // Currently only implemented for Linux ELF executables. + // TODO(derekparker): Add support for Mach-O and PE. + skipUnlessOn(t, "linux only", "linux") + withTestProcessArgs("testnextprog", t, "", []string{}, protest.LinkStrip, func(p *proc.Target, grp *proc.TargetGroup, f protest.Fixture) { + setFunctionBreakpoint(p, t, "main.main") + assertNoError(grp.Continue(), t, "Continue") + assertCurrentLocationFunction(p, t, "main.main") + assertLineNumber(p, t, 37, "first continue") + assertNoError(grp.Next(), t, "Next") + assertLineNumber(p, t, 38, "after next") + }) } func TestIssue844(t *testing.T) { diff --git a/pkg/proc/stack.go b/pkg/proc/stack.go index 9fdcb93e8a..d4791df812 100644 --- a/pkg/proc/stack.go +++ b/pkg/proc/stack.go @@ -253,6 +253,9 @@ func (it *stackIterator) Err() error { // frameBase calculates the frame base pseudo-register for DWARF for fn and // the current frame. func (it *stackIterator) frameBase(fn *Function) int64 { + if fn.cu.image.Stripped() { + return 0 + } dwarfTree, err := fn.cu.image.getDwarfTree(fn.offset) if err != nil { return 0 @@ -695,6 +698,6 @@ func (d *Defer) DeferredFunc(p *Target) (file string, line int, fn *Function) { if fn == nil { return "", 0, nil } - file, line = fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry) + file, line = bi.EntryLineForFunc(fn) return file, line, fn } diff --git a/pkg/proc/target_exec.go b/pkg/proc/target_exec.go index ed9da60e00..6473429409 100644 --- a/pkg/proc/target_exec.go +++ b/pkg/proc/target_exec.go @@ -392,7 +392,7 @@ func stepInstructionOut(dbp *Target, curthread Thread, fnname1, fnname2 string) } loc, err := curthread.Location() var locFnName string - if loc.Fn != nil { + if loc.Fn != nil && !loc.Fn.cu.image.Stripped() { locFnName = loc.Fn.Name // Calls to runtime.Breakpoint are inlined in some versions of Go when // inlining is enabled. Here we attempt to resolve any inlining. @@ -677,7 +677,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error { } } - if !backward { + if !backward && !topframe.Current.Fn.cu.image.Stripped() { _, err = setDeferBreakpoint(dbp, text, topframe, sameGCond, stepInto) if err != nil { return err @@ -685,7 +685,7 @@ func next(dbp *Target, stepInto, inlinedStepOut bool) error { } // Add breakpoints on all the lines in the current function - pcs, err := topframe.Current.Fn.cu.lineInfo.AllPCsBetween(topframe.Current.Fn.Entry, topframe.Current.Fn.End-1, topframe.Current.File, topframe.Current.Line) + pcs, err := topframe.Current.Fn.AllPCs(topframe.Current.File, topframe.Current.Line) if err != nil { return err } @@ -865,6 +865,11 @@ func FindDeferReturnCalls(text []AsmInstruction) []uint64 { // If includeCurrentFn is true it will also remove all instructions // belonging to the current function. func removeInlinedCalls(pcs []uint64, topframe Stackframe) ([]uint64, error) { + // TODO(derekparker) it should be possible to still use some internal + // runtime information to do this. + if topframe.Call.Fn == nil || topframe.Call.Fn.cu.image.Stripped() { + return pcs, nil + } dwarfTree, err := topframe.Call.Fn.cu.image.getDwarfTree(topframe.Call.Fn.offset) if err != nil { return pcs, err @@ -1061,7 +1066,7 @@ func skipAutogeneratedWrappersOut(g *G, thread Thread, startTopframe, startRetfr if frame.Current.Fn == nil { return } - file, line := frame.Current.Fn.cu.lineInfo.PCToLine(frame.Current.Fn.Entry, frame.Current.Fn.Entry) + file, line := g.Thread.BinInfo().EntryLineForFunc(frame.Current.Fn) if !isAutogeneratedOrDeferReturn(Location{File: file, Line: line, Fn: frame.Current.Fn}) { return &frames[i-1], &frames[i] } diff --git a/pkg/proc/variables.go b/pkg/proc/variables.go index 65f5040dc1..26b4fb713d 100644 --- a/pkg/proc/variables.go +++ b/pkg/proc/variables.go @@ -535,7 +535,7 @@ func (g *G) StartLoc(tgt *Target) Location { if fn == nil { return Location{PC: g.StartPC} } - f, l := fn.cu.lineInfo.PCToLine(fn.Entry, fn.Entry) + f, l := tgt.BinInfo().EntryLineForFunc(fn) return Location{PC: fn.Entry, File: f, Line: l, Fn: fn} } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 4cd6e72ab1..c452d9e0d1 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -1315,6 +1315,13 @@ func (d *Debugger) collectBreakpointInformation(apiThread *api.Thread, thread pr tgt := d.target.TargetForThread(thread.ThreadID()) + // If we're dealing with a stripped binary don't attempt to load more + // information, we won't be able to. + img := tgt.BinInfo().PCToImage(bp.Addr) + if img != nil && img.Stripped() { + return nil + } + if bp.Goroutine { g, err := proc.GetG(thread) if err != nil {