diff --git a/_fixtures/deepstack.go b/_fixtures/deepstack.go new file mode 100644 index 00000000..53f1931c --- /dev/null +++ b/_fixtures/deepstack.go @@ -0,0 +1,15 @@ +package main + +import "runtime" + +func deepCall(n int) { + if n == 0 { + runtime.Breakpoint() + return + } + deepCall(n - 1) +} + +func main() { + deepCall(10000) +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index ac0ee96e..d1cdc82e 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -1495,6 +1495,34 @@ func BenchmarkLocalVariables(b *testing.B) { }) } +func BenchmarkStacktrace(b *testing.B) { + withTestProcess("deepstack", b, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { + assertNoError(grp.Continue(), b, "Continue()") + + g, err := proc.GetG(p.CurrentThread()) + assertNoError(err, b, "GetG()") + if g == nil { + b.Fatal("no current goroutine") + } + + frames, err := proc.GoroutineStacktrace(p, g, 600, 0) + assertNoError(err, b, "GoroutineStacktrace()") + if len(frames) < 500 { + b.Fatalf("expected at least 500 frames, got %d", len(frames)) + } + b.Logf("stack depth: %d frames", len(frames)) + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := proc.GoroutineStacktrace(p, g, 600, 0) + if err != nil { + b.Fatal(err) + } + } + }) +} + func TestCondBreakpoint(t *testing.T) { protest.AllowRecording(t) withTestProcess("parallel_next", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { diff --git a/pkg/proc/stack.go b/pkg/proc/stack.go index 7e490a25..5bdff66a 100644 --- a/pkg/proc/stack.go +++ b/pkg/proc/stack.go @@ -13,6 +13,7 @@ import ( "github.com/go-delve/delve/pkg/dwarf/frame" "github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/dwarf/reader" + "github.com/go-delve/delve/pkg/goversion" "github.com/go-delve/delve/pkg/logflags" ) @@ -243,6 +244,13 @@ type stackIterator struct { count int + // canUseFP is true when the frame pointer has been validated as stable + // for FP-based unwinding. At the top of the stack the topmost function + // may be frameless (BP inherited from caller), so canUseFP starts false. + // It becomes true when BP + 2*PtrSize == CFA, indicating that BP is the + // current frame's own frame pointer. + canUseFP bool + opts StacktraceOptions } @@ -536,16 +544,153 @@ func (it *stackIterator) appendInlineCalls(callback func(Stackframe) bool, frame return callback(frame) } -// advanceRegs calculates the DwarfRegisters for a next stack frame +// advanceRegs calculates the DwarfRegisters for the next stack frame // (corresponding to it.pc). // -// The computation uses the registers for the current stack frame (it.regs) and -// the corresponding Frame Descriptor Entry (FDE) retrieved from the DWARF info. +// The computation uses the registers for the current stack frame in it.regs. +// When possible a simple frame pointer based unwinding is done, otherwise +// the Frame Descriptor Entry (FDE) corresponding to the current frame, +// retrieved from DWARF, is used. // // The new set of registers is returned. it.regs is not updated, except for // it.regs.CFA; the caller has to eventually switch it.regs when the iterator // advances to the next frame. func (it *stackIterator) advanceRegs() (callFrameRegs op.DwarfRegisters, ret uint64, retaddr uint64) { + if callFrameRegs, ret, retaddr, ok := it.tryFramePointerUnwind(); ok { + return callFrameRegs, ret, retaddr + } + callFrameRegs, ret, retaddr = it.advanceRegsDWARF() + if !it.canUseFP && it.err == nil { + ptrSize := uint64(it.bi.Arch.PtrSize()) + stable := false + + switch it.bi.Arch.Name { + case "amd64": + // AMD64: BP + 2*PtrSize == CFA + bp := it.regs.BP() + cfa := uint64(it.regs.CFA) + stable = (bp != 0 && bp+2*ptrSize == cfa) + + case "arm64": + // ARM64: BP + 2*PtrSize == CFA + bp := it.regs.BP() + cfa := uint64(it.regs.CFA) + stable = (bp != 0 && bp+2*ptrSize == cfa) + + // Disable hybrid FP unwinding on Windows when DW_AT_producer is missing + // or unparsable (otherwise canUseFP could activate despite the Go 1.24/1.25 + // guard), and for Go 1.24/1.25 due to test failures. See golang/go#63630. + if it.bi.GOOS == "windows" { + ver := goversion.ParseProducer(it.bi.Producer()) + if ver.Major < 1 || !ver.IsDevelBuild() && ver.Major == 1 && (ver.Minor == 24 || ver.Minor == 25) { + stable = false + } + } + } + + if stable { + it.canUseFP = true + // AArch64: DWARF may omit X29 (BP) rules; inject savedBP when missing + if it.bi.Arch.Name == "arm64" { + bp := it.regs.BP() + if (callFrameRegs.Reg(callFrameRegs.BPRegNum) == nil || callFrameRegs.BP() == 0) && bp != 0 { + savedBP, err := readUintRaw(it.mem, bp, int64(ptrSize)) + if err == nil { + callFrameRegs.AddReg(callFrameRegs.BPRegNum, op.DwarfRegisterFromUint64(savedBP)) + } + } + } + } + } + return callFrameRegs, ret, retaddr +} + +func (it *stackIterator) tryFramePointerUnwind() (callFrameRegs op.DwarfRegisters, ret uint64, retaddr uint64, ok bool) { + // Frame pointer unwinding is only implemented for amd64 and arm64. + if it.bi.Arch.Name != "amd64" && it.bi.Arch.Name != "arm64" { + return op.DwarfRegisters{}, 0, 0, false + } + + if !it.canUseFP { + return op.DwarfRegisters{}, 0, 0, false + } + + bp := it.regs.BP() + if bp == 0 { + return op.DwarfRegisters{}, 0, 0, false + } + + if it.g != nil && !it.systemstack { + if bp < it.g.stack.lo || bp >= it.g.stack.hi { + return op.DwarfRegisters{}, 0, 0, false + } + } + + fn := it.bi.PCToFunc(it.pc) + // Frame pointer conventions are only guaranteed for Go code. + if fn == nil || !fn.cu.isgo { + return op.DwarfRegisters{}, 0, 0, false + } + + switch fn.Name { + case "runtime.asmcgocall", + "runtime.cgocallback_gofunc", "runtime.cgocallback", + "runtime.goexit", "runtime.goexit0", "runtime.goexit1", + "runtime.mstart", "runtime.mstart0", "runtime.mstart1", + "runtime.systemstack_switch", + "runtime.sigreturn", "runtime.sigtrampgo", + "runtime.sigpanic", + "crosscall2": + return op.DwarfRegisters{}, 0, 0, false + } + + ptrSize := uint64(it.bi.Arch.PtrSize()) + + savedBP, err := readUintRaw(it.mem, bp, int64(ptrSize)) + if err != nil { + return op.DwarfRegisters{}, 0, 0, false + } + + retaddr = bp + ptrSize + ret, err = readUintRaw(it.mem, retaddr, int64(ptrSize)) + if err != nil { + return op.DwarfRegisters{}, 0, 0, false + } + + cfa := int64(bp + 2*ptrSize) + + if ret == 0 || it.bi.PCToFunc(ret) == nil { + return op.DwarfRegisters{}, 0, 0, false + } + + it.regs.CFA = cfa + + callimage := it.bi.funcToImage(fn) + callFrameRegs = op.DwarfRegisters{ + StaticBase: callimage.StaticBase, + ByteOrder: it.regs.ByteOrder, + PCRegNum: it.regs.PCRegNum, + SPRegNum: it.regs.SPRegNum, + BPRegNum: it.regs.BPRegNum, + LRRegNum: it.regs.LRRegNum, + } + + callFrameRegs.AddReg(callFrameRegs.SPRegNum, op.DwarfRegisterFromUint64(uint64(cfa))) + callFrameRegs.AddReg(callFrameRegs.BPRegNum, op.DwarfRegisterFromUint64(savedBP)) + + if it.bi.Arch.usesLR { + callFrameRegs.AddReg(callFrameRegs.LRRegNum, op.DwarfRegisterFromUint64(ret)) + } + + if logflags.Stack() { + logflags.StackLogger().Debugf("advanceRegs (fp) at %#x: BP=%#x savedBP=%#x ret=%#x CFA=%#x", it.pc, bp, savedBP, ret, cfa) + } + + return callFrameRegs, ret, retaddr, true +} + +// advanceRegsDWARF unwinds the stack using DWARF Call Frame Information. +func (it *stackIterator) advanceRegsDWARF() (callFrameRegs op.DwarfRegisters, ret uint64, retaddr uint64) { logger := logflags.StackLogger() fde, err := it.bi.frameEntries.FDEForPC(it.pc) @@ -560,7 +705,7 @@ func (it *stackIterator) advanceRegs() (callFrameRegs op.DwarfRegisters, ret uin framectx = it.bi.Arch.fixFrameUnwindContext(fctxt, it.pc, it.bi) } - logger.Debugf("advanceRegs at %#x", it.pc) + logger.Debugf("advanceRegs (DWARF) at %#x", it.pc) cfareg, err := it.executeFrameRegRule(0, framectx.CFA, 0) if cfareg == nil {