mirror of
https://github.com/go-delve/delve.git
synced 2026-05-05 12:16:12 +02:00
proc: implement frame pointer unwinding (#4288)
* Add BenchmarkStacktrace test * proc: implement frame pointer unwinding Add hybrid stack unwinding that attempts frame-pointer-based unwinding before falling back to DWARF. * Rename useDWARF and change to funcToImage * Update comment Remove _fixtures/deeprecursion.go * Add check of non-go code * Fix ARM64 FP unwind false positives and Windows failures.
This commit is contained in:
parent
954ef88092
commit
1d5a7eb404
15
_fixtures/deepstack.go
Normal file
15
_fixtures/deepstack.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user