proc/stack: use BP when FDE is not available

On Windows we can sometimes encounter threads stopped in locations for
which we do not have entries in debug_frame.
These cases seem to be due to calls to Windows API in the go runtime,
we can still produce a (partial) stack trace in this circumstance by
following frame pointers (starting with BP).
We still prefer debug_frame entries when available since go functions
do not have frame pointers before go1.8.
This commit is contained in:
aarzilli 2017-02-16 14:16:00 +01:00
parent 1a68f8d351
commit 92faa95bf9
9 changed files with 70 additions and 32 deletions

@ -1119,7 +1119,10 @@ func TestFrameEvaluation(t *testing.T) {
for _, g := range gs { for _, g := range gs {
frame := -1 frame := -1
frames, err := g.Stacktrace(10) frames, err := g.Stacktrace(10)
assertNoError(err, t, "GoroutineStacktrace()") if err != nil {
t.Logf("could not stacktrace goroutine %d: %v\n", g.ID, err)
continue
}
for i := range frames { for i := range frames {
if frames[i].Call.Fn != nil && frames[i].Call.Fn.Name == "main.agoroutine" { if frames[i].Call.Fn != nil && frames[i].Call.Fn.Name == "main.agoroutine" {
frame = i frame = i

@ -17,6 +17,7 @@ import (
type Registers interface { type Registers interface {
PC() uint64 PC() uint64
SP() uint64 SP() uint64
BP() uint64
CX() uint64 CX() uint64
TLS() uint64 TLS() uint64
Get(int) (uint64, error) Get(int) (uint64, error)

@ -88,6 +88,10 @@ func (r *Regs) SP() uint64 {
return r.rsp return r.rsp
} }
func (r *Regs) BP() uint64 {
return r.rbp
}
// CX returns the value of the RCX register. // CX returns the value of the RCX register.
func (r *Regs) CX() uint64 { func (r *Regs) CX() uint64 {
return r.rcx return r.rcx

@ -69,6 +69,10 @@ func (r *Regs) SP() uint64 {
return r.regs.Rsp return r.regs.Rsp
} }
func (r *Regs) BP() uint64 {
return r.regs.Rbp
}
// CX returns the value of RCX register. // CX returns the value of RCX register.
func (r *Regs) CX() uint64 { func (r *Regs) CX() uint64 {
return r.regs.Rcx return r.regs.Rcx

@ -107,6 +107,10 @@ func (r *Regs) SP() uint64 {
return r.rsp return r.rsp
} }
func (r *Regs) BP() uint64 {
return r.rbp
}
// CX returns the value of the RCX register. // CX returns the value of the RCX register.
func (r *Regs) CX() uint64 { func (r *Regs) CX() uint64 {
return r.rcx return r.rcx

@ -1,7 +1,6 @@
package proc package proc
import ( import (
"encoding/binary"
"errors" "errors"
"fmt" "fmt"
@ -62,7 +61,7 @@ func (t *Thread) stackIterator(stkbar []savedLR, stkbarPos int) (*stackIterator,
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newStackIterator(t.dbp, regs.PC(), regs.SP(), stkbar, stkbarPos), nil return newStackIterator(t.dbp, regs.PC(), regs.SP(), regs.BP(), stkbar, stkbarPos), nil
} }
// Stacktrace returns the stack trace for thread. // Stacktrace returns the stack trace for thread.
@ -83,7 +82,7 @@ func (g *G) stackIterator() (*stackIterator, error) {
if g.thread != nil { if g.thread != nil {
return g.thread.stackIterator(stkbar, g.stkbarPos) return g.thread.stackIterator(stkbar, g.stkbarPos)
} }
return newStackIterator(g.dbp, g.PC, g.SP, stkbar, g.stkbarPos), nil return newStackIterator(g.dbp, g.PC, g.SP, 0, stkbar, g.stkbarPos), nil
} }
// Stacktrace returns the stack trace for a goroutine. // Stacktrace returns the stack trace for a goroutine.
@ -114,12 +113,12 @@ func (n NullAddrError) Error() string {
// required to iterate and walk the program // required to iterate and walk the program
// stack. // stack.
type stackIterator struct { type stackIterator struct {
pc, sp uint64 pc, sp, bp uint64
top bool top bool
atend bool atend bool
frame Stackframe frame Stackframe
dbp *Process dbp *Process
err error err error
stackBarrierPC uint64 stackBarrierPC uint64
stkbar []savedLR stkbar []savedLR
@ -130,7 +129,7 @@ type savedLR struct {
val uint64 val uint64
} }
func newStackIterator(dbp *Process, pc, sp uint64, stkbar []savedLR, stkbarPos int) *stackIterator { func newStackIterator(dbp *Process, pc, sp, bp uint64, stkbar []savedLR, stkbarPos int) *stackIterator {
stackBarrierPC := dbp.goSymTable.LookupFunc(runtimeStackBarrier).Entry stackBarrierPC := dbp.goSymTable.LookupFunc(runtimeStackBarrier).Entry
if stkbar != nil { if stkbar != nil {
fn := dbp.goSymTable.PCToFunc(pc) fn := dbp.goSymTable.PCToFunc(pc)
@ -148,7 +147,7 @@ func newStackIterator(dbp *Process, pc, sp uint64, stkbar []savedLR, stkbarPos i
} }
stkbar = stkbar[stkbarPos:] stkbar = stkbar[stkbarPos:]
} }
return &stackIterator{pc: pc, sp: sp, top: true, dbp: dbp, err: nil, atend: false, stackBarrierPC: stackBarrierPC, stkbar: stkbar} return &stackIterator{pc: pc, sp: sp, bp: bp, top: true, dbp: dbp, err: nil, atend: false, stackBarrierPC: stackBarrierPC, stkbar: stkbar}
} }
// Next points the iterator to the next stack frame. // Next points the iterator to the next stack frame.
@ -156,7 +155,7 @@ func (it *stackIterator) Next() bool {
if it.err != nil || it.atend { if it.err != nil || it.atend {
return false return false
} }
it.frame, it.err = it.dbp.frameInfo(it.pc, it.sp, it.top) it.frame, it.err = it.dbp.frameInfo(it.pc, it.sp, it.bp, it.top)
if it.err != nil { if it.err != nil {
if _, nofde := it.err.(*frame.NoFDEForPCError); nofde && !it.top { if _, nofde := it.err.(*frame.NoFDEForPCError); nofde && !it.top {
it.frame = Stackframe{Current: Location{PC: it.pc, File: "?", Line: -1}, Call: Location{PC: it.pc, File: "?", Line: -1}, CFA: 0, Ret: 0} it.frame = Stackframe{Current: Location{PC: it.pc, File: "?", Line: -1}, Call: Location{PC: it.pc, File: "?", Line: -1}, CFA: 0, Ret: 0}
@ -167,13 +166,6 @@ func (it *stackIterator) Next() bool {
return false return false
} }
if it.frame.Current.Fn == nil {
if it.top {
it.err = fmt.Errorf("PC not associated to any function")
}
return false
}
if it.frame.Ret <= 0 { if it.frame.Ret <= 0 {
it.atend = true it.atend = true
return true return true
@ -186,7 +178,7 @@ func (it *stackIterator) Next() bool {
} }
// Look for "top of stack" functions. // Look for "top of stack" functions.
if it.frame.Current.Fn.Name == "runtime.goexit" || it.frame.Current.Fn.Name == "runtime.rt0_go" || it.frame.Current.Fn.Name == "runtime.mcall" { if it.frame.Current.Fn != nil && (it.frame.Current.Fn.Name == "runtime.goexit" || it.frame.Current.Fn.Name == "runtime.rt0_go" || it.frame.Current.Fn.Name == "runtime.mcall") {
it.atend = true it.atend = true
return true return true
} }
@ -194,6 +186,7 @@ func (it *stackIterator) Next() bool {
it.top = false it.top = false
it.pc = it.frame.Ret it.pc = it.frame.Ret
it.sp = uint64(it.frame.CFA) it.sp = uint64(it.frame.CFA)
it.bp, _ = readUintRaw(it.dbp.currentThread, uintptr(it.bp), int64(it.dbp.arch.PtrSize()))
return true return true
} }
@ -210,24 +203,35 @@ func (it *stackIterator) Err() error {
return it.err return it.err
} }
func (dbp *Process) frameInfo(pc, sp uint64, top bool) (Stackframe, error) { func (dbp *Process) frameInfo(pc, sp, bp uint64, top bool) (Stackframe, error) {
f, l, fn := dbp.PCToLine(pc)
fde, err := dbp.frameEntries.FDEForPC(pc) fde, err := dbp.frameEntries.FDEForPC(pc)
if err != nil { if _, nofde := err.(*frame.NoFDEForPCError); nofde {
return Stackframe{}, err if bp == 0 {
return Stackframe{}, err
}
// When no FDE is available attempt to use BP instead
retaddr := uintptr(int(bp) + dbp.arch.PtrSize())
cfa := int64(retaddr) + int64(dbp.arch.PtrSize())
return dbp.newStackframe(pc, cfa, retaddr, nil, top)
} }
spoffset, retoffset := fde.ReturnAddressOffset(pc) spoffset, retoffset := fde.ReturnAddressOffset(pc)
cfa := int64(sp) + spoffset cfa := int64(sp) + spoffset
retaddr := uintptr(cfa + retoffset) retaddr := uintptr(cfa + retoffset)
return dbp.newStackframe(pc, cfa, retaddr, fde, top)
}
func (dbp *Process) newStackframe(pc uint64, cfa int64, retaddr uintptr, fde *frame.FrameDescriptionEntry, top bool) (Stackframe, error) {
if retaddr == 0 { if retaddr == 0 {
return Stackframe{}, NullAddrError{} return Stackframe{}, NullAddrError{}
} }
data, err := dbp.currentThread.readMemory(retaddr, dbp.arch.PtrSize()) f, l, fn := dbp.PCToLine(pc)
ret, err := readUintRaw(dbp.currentThread, retaddr, int64(dbp.arch.PtrSize()))
if err != nil { if err != nil {
return Stackframe{}, err return Stackframe{}, err
} }
r := Stackframe{Current: Location{PC: pc, File: f, Line: l, Fn: fn}, CFA: cfa, FDE: fde, Ret: binary.LittleEndian.Uint64(data), addrret: uint64(retaddr)} r := Stackframe{Current: Location{PC: pc, File: f, Line: l, Fn: fn}, CFA: cfa, FDE: fde, Ret: ret, addrret: uint64(retaddr)}
if !top { if !top {
r.Call.File, r.Call.Line, r.Call.Fn = dbp.PCToLine(pc - 1) r.Call.File, r.Call.Line, r.Call.Fn = dbp.PCToLine(pc - 1)
r.Call.PC = r.Current.PC r.Call.PC = r.Current.PC

@ -212,6 +212,7 @@ func TestScopePrefix(t *testing.T) {
goroutinesOut := strings.Split(term.MustExec("goroutines"), "\n") goroutinesOut := strings.Split(term.MustExec("goroutines"), "\n")
agoroutines := []int{} agoroutines := []int{}
nonagoroutines := []int{}
curgid := -1 curgid := -1
for _, line := range goroutinesOut { for _, line := range goroutinesOut {
@ -235,14 +236,31 @@ func TestScopePrefix(t *testing.T) {
} }
if idx := strings.Index(line, " main.agoroutine "); idx < 0 { if idx := strings.Index(line, " main.agoroutine "); idx < 0 {
nonagoroutines = append(nonagoroutines, gid)
continue continue
} }
agoroutines = append(agoroutines, gid) agoroutines = append(agoroutines, gid)
} }
if len(agoroutines) != 10 { if len(agoroutines) > 10 {
t.Fatalf("Output of goroutines did not have 10 goroutines stopped on main.agoroutine: %q", goroutinesOut) t.Fatalf("Output of goroutines did not have 10 goroutines stopped on main.agoroutine (%d found): %q", len(agoroutines), goroutinesOut)
}
if len(agoroutines) < 10 {
extraAgoroutines := 0
for _, gid := range nonagoroutines {
stackOut := strings.Split(term.MustExec(fmt.Sprintf("goroutine %d stack", gid)), "\n")
for _, line := range stackOut {
if strings.HasSuffix(line, " main.agoroutine") {
extraAgoroutines++
break
}
}
}
if len(agoroutines)+extraAgoroutines < 10 {
t.Fatalf("Output of goroutines did not have 10 goroutines stopped on main.agoroutine (%d+%d found): %q", len(agoroutines), extraAgoroutines, goroutinesOut)
}
} }
if curgid < 0 { if curgid < 0 {

@ -13,8 +13,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/derekparker/delve/pkg/target"
"github.com/derekparker/delve/pkg/proc" "github.com/derekparker/delve/pkg/proc"
"github.com/derekparker/delve/pkg/target"
"github.com/derekparker/delve/service/api" "github.com/derekparker/delve/service/api"
) )
@ -750,7 +750,7 @@ func (d *Debugger) convertStacktrace(rawlocs []proc.Stackframe, cfg *proc.LoadCo
locations := make([]api.Stackframe, 0, len(rawlocs)) locations := make([]api.Stackframe, 0, len(rawlocs))
for i := range rawlocs { for i := range rawlocs {
frame := api.Stackframe{Location: api.ConvertLocation(rawlocs[i].Call)} frame := api.Stackframe{Location: api.ConvertLocation(rawlocs[i].Call)}
if cfg != nil { if cfg != nil && rawlocs[i].Current.Fn != nil {
var err error var err error
scope := rawlocs[i].Scope(d.target.CurrentThread()) scope := rawlocs[i].Scope(d.target.CurrentThread())
locals, err := scope.LocalVariables(*cfg) locals, err := scope.LocalVariables(*cfg)

@ -763,7 +763,7 @@ func TestClientServer_FullStacktrace(t *testing.T) {
if arg.Name != "i" { if arg.Name != "i" {
continue continue
} }
t.Logf("frame %d, variable i is %v\n", arg) t.Logf("frame %v, variable i is %v\n", frame, arg)
argn, err := strconv.Atoi(arg.Value) argn, err := strconv.Atoi(arg.Value)
if err == nil { if err == nil {
found[argn] = true found[argn] = true