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 {
frame := -1
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 {
if frames[i].Call.Fn != nil && frames[i].Call.Fn.Name == "main.agoroutine" {
frame = i

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

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

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

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

@ -1,7 +1,6 @@
package proc
import (
"encoding/binary"
"errors"
"fmt"
@ -62,7 +61,7 @@ func (t *Thread) stackIterator(stkbar []savedLR, stkbarPos int) (*stackIterator,
if err != nil {
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.
@ -83,7 +82,7 @@ func (g *G) stackIterator() (*stackIterator, error) {
if g.thread != nil {
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.
@ -114,12 +113,12 @@ func (n NullAddrError) Error() string {
// required to iterate and walk the program
// stack.
type stackIterator struct {
pc, sp uint64
top bool
atend bool
frame Stackframe
dbp *Process
err error
pc, sp, bp uint64
top bool
atend bool
frame Stackframe
dbp *Process
err error
stackBarrierPC uint64
stkbar []savedLR
@ -130,7 +129,7 @@ type savedLR struct {
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
if stkbar != nil {
fn := dbp.goSymTable.PCToFunc(pc)
@ -148,7 +147,7 @@ func newStackIterator(dbp *Process, pc, sp uint64, stkbar []savedLR, stkbarPos i
}
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.
@ -156,7 +155,7 @@ func (it *stackIterator) Next() bool {
if it.err != nil || it.atend {
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 _, 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}
@ -167,13 +166,6 @@ func (it *stackIterator) Next() bool {
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 {
it.atend = true
return true
@ -186,7 +178,7 @@ func (it *stackIterator) Next() bool {
}
// 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
return true
}
@ -194,6 +186,7 @@ func (it *stackIterator) Next() bool {
it.top = false
it.pc = it.frame.Ret
it.sp = uint64(it.frame.CFA)
it.bp, _ = readUintRaw(it.dbp.currentThread, uintptr(it.bp), int64(it.dbp.arch.PtrSize()))
return true
}
@ -210,24 +203,35 @@ func (it *stackIterator) Err() error {
return it.err
}
func (dbp *Process) frameInfo(pc, sp uint64, top bool) (Stackframe, error) {
f, l, fn := dbp.PCToLine(pc)
func (dbp *Process) frameInfo(pc, sp, bp uint64, top bool) (Stackframe, error) {
fde, err := dbp.frameEntries.FDEForPC(pc)
if err != nil {
return Stackframe{}, err
if _, nofde := err.(*frame.NoFDEForPCError); nofde {
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)
cfa := int64(sp) + spoffset
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 {
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 {
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 {
r.Call.File, r.Call.Line, r.Call.Fn = dbp.PCToLine(pc - 1)
r.Call.PC = r.Current.PC

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

@ -13,8 +13,8 @@ import (
"sync"
"time"
"github.com/derekparker/delve/pkg/target"
"github.com/derekparker/delve/pkg/proc"
"github.com/derekparker/delve/pkg/target"
"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))
for i := range rawlocs {
frame := api.Stackframe{Location: api.ConvertLocation(rawlocs[i].Call)}
if cfg != nil {
if cfg != nil && rawlocs[i].Current.Fn != nil {
var err error
scope := rawlocs[i].Scope(d.target.CurrentThread())
locals, err := scope.LocalVariables(*cfg)

@ -763,7 +763,7 @@ func TestClientServer_FullStacktrace(t *testing.T) {
if arg.Name != "i" {
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)
if err == nil {
found[argn] = true