From 58db8322ef41963aeb042e7bd83536babe0d9656 Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Sun, 19 Apr 2015 17:11:33 -0500 Subject: [PATCH] Improve chan / goroutine coordination * Properly find next source line for goroutines blocked in chanrecv * Refactor breakpoint clearing * Refactor temp breakpoint setting --- Makefile | 4 +- _fixtures/testnextprog.go | 17 ++-- _fixtures/testvisitorprog.go | 6 ++ dwarf/frame/table.go | 2 +- proctl/breakpoints.go | 58 ++++++++++++- proctl/proctl.go | 85 ++++++++++++------- proctl/proctl_test.go | 93 +++++++++++---------- proctl/stack.go | 53 ++++++++++++ proctl/threads.go | 157 +++++++++++++++++++++-------------- proctl/threads_darwin.go | 4 +- proctl/variables.go | 116 ++++++++++++++++++++------ source/source.go | 124 +++++++++++++++++---------- source/source_test.go | 8 +- 13 files changed, 502 insertions(+), 225 deletions(-) create mode 100644 proctl/stack.go diff --git a/Makefile b/Makefile index c32fa55a..e8406b1e 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ endif test: ifeq "$(UNAME)" "Darwin" go test $(PREFIX)/command $(PREFIX)/dwarf/frame $(PREFIX)/dwarf/op $(PREFIX)/dwarf/util $(PREFIX)/source $(PREFIX)/dwarf/line - cd proctl && go test -c $(PREFIX)/proctl && codesign -s $(CERT) ./proctl.test && ./proctl.test && rm ./proctl.test + cd proctl && go test -c $(PREFIX)/proctl && codesign -s $(CERT) ./proctl.test && ./proctl.test -test.v && rm ./proctl.test else - go test ./... + go test -v ./... endif diff --git a/_fixtures/testnextprog.go b/_fixtures/testnextprog.go index c8ed0b57..8cc93e2e 100644 --- a/_fixtures/testnextprog.go +++ b/_fixtures/testnextprog.go @@ -1,8 +1,8 @@ +// fix lines package main import ( "fmt" - "runtime" "time" ) @@ -35,9 +35,14 @@ func testnext() { } func main() { - runtime.LockOSThread() - for { - testnext() - fmt.Println("foo") - } + d := make(chan int) + testnext() + go testgoroutine(9, d) + <-d + fmt.Println("done") +} + +// fix line +func testgoroutine(foo int, d chan int) { + d <- foo } diff --git a/_fixtures/testvisitorprog.go b/_fixtures/testvisitorprog.go index a5865d65..3cec4245 100644 --- a/_fixtures/testvisitorprog.go +++ b/_fixtures/testvisitorprog.go @@ -57,3 +57,9 @@ func endlesslooptest() { fmt.Println("foo") } } + +func decltest() { + var foo = "bar" + var baz = 9 + fmt.Println(foo, baz) +} diff --git a/dwarf/frame/table.go b/dwarf/frame/table.go index bd871766..eabb365c 100644 --- a/dwarf/frame/table.go +++ b/dwarf/frame/table.go @@ -159,7 +159,7 @@ func (frame *FrameContext) ExecuteUntilPC(instructions []byte) { // We only need to execute the instructions until // ctx.loc > ctx.addess (which is the address we // are currently at in the traced process). - for frame.address > frame.loc && frame.buf.Len() > 0 { + for frame.address >= frame.loc && frame.buf.Len() > 0 { executeDwarfInstruction(frame) } } diff --git a/proctl/breakpoints.go b/proctl/breakpoints.go index 7d333bf7..e840deed 100644 --- a/proctl/breakpoints.go +++ b/proctl/breakpoints.go @@ -16,12 +16,27 @@ type BreakPoint struct { OriginalData []byte ID int Temp bool + hardware bool + reg int } func (bp *BreakPoint) String() string { return fmt.Sprintf("Breakpoint %d at %#v %s:%d", bp.ID, bp.Addr, bp.File, bp.Line) } +func (bp *BreakPoint) Clear(thread *ThreadContext) (*BreakPoint, error) { + if bp.hardware { + if err := clearHardwareBreakpoint(bp.reg, thread.Id); err != nil { + return nil, err + } + return bp, nil + } + if _, err := writeMemory(thread, uintptr(bp.Addr), bp.OriginalData); err != nil { + return nil, fmt.Errorf("could not clear breakpoint %s", err) + } + return bp, nil +} + // Returned when trying to set a breakpoint at // an address that already has a breakpoint set for it. type BreakPointExistsError struct { @@ -59,7 +74,7 @@ func (dbp *DebuggedProcess) BreakpointExists(addr uint64) bool { return ok } -func (dbp *DebuggedProcess) newBreakpoint(fn, f string, l int, addr uint64, data []byte) *BreakPoint { +func (dbp *DebuggedProcess) newBreakpoint(fn, f string, l int, addr uint64, data []byte, temp bool) *BreakPoint { dbp.breakpointIDCounter++ return &BreakPoint{ FunctionName: fn, @@ -68,10 +83,18 @@ func (dbp *DebuggedProcess) newBreakpoint(fn, f string, l int, addr uint64, data Addr: addr, OriginalData: data, ID: dbp.breakpointIDCounter, + Temp: temp, } } -func (dbp *DebuggedProcess) setBreakpoint(tid int, addr uint64) (*BreakPoint, error) { +func (dbp *DebuggedProcess) newHardwareBreakpoint(fn, f string, l int, addr uint64, data []byte, temp bool, reg int) *BreakPoint { + bp := dbp.newBreakpoint(fn, f, l, addr, data, temp) + bp.hardware = true + bp.reg = reg + return bp +} + +func (dbp *DebuggedProcess) setBreakpoint(tid int, addr uint64, temp bool) (*BreakPoint, error) { var f, l, fn = dbp.goSymTable.PCToLine(uint64(addr)) if fn == nil { return nil, InvalidAddressError{address: addr} @@ -89,7 +112,7 @@ func (dbp *DebuggedProcess) setBreakpoint(tid int, addr uint64) (*BreakPoint, er if err := setHardwareBreakpoint(i, tid, addr); err != nil { return nil, fmt.Errorf("could not set hardware breakpoint: %v", err) } - dbp.HWBreakPoints[i] = dbp.newBreakpoint(fn.Name, f, l, addr, nil) + dbp.HWBreakPoints[i] = dbp.newHardwareBreakpoint(fn.Name, f, l, addr, nil, temp, i) return dbp.HWBreakPoints[i], nil } } @@ -103,6 +126,33 @@ func (dbp *DebuggedProcess) setBreakpoint(tid int, addr uint64) (*BreakPoint, er if _, err := writeMemory(thread, uintptr(addr), []byte{0xCC}); err != nil { return nil, err } - dbp.BreakPoints[addr] = dbp.newBreakpoint(fn.Name, f, l, addr, originalData) + dbp.BreakPoints[addr] = dbp.newBreakpoint(fn.Name, f, l, addr, originalData, temp) return dbp.BreakPoints[addr], nil } + +func (dbp *DebuggedProcess) clearBreakpoint(tid int, addr uint64) (*BreakPoint, error) { + thread := dbp.Threads[tid] + // Check for hardware breakpoint + for i, bp := range dbp.HWBreakPoints { + if bp == nil { + continue + } + if bp.Addr == addr { + _, err := bp.Clear(thread) + if err != nil { + return nil, err + } + dbp.HWBreakPoints[i] = nil + return bp, nil + } + } + // Check for software breakpoint + if bp, ok := dbp.BreakPoints[addr]; ok { + if _, err := bp.Clear(thread); err != nil { + return nil, err + } + delete(dbp.BreakPoints, addr) + return bp, nil + } + return nil, fmt.Errorf("no breakpoint at %#v", addr) +} diff --git a/proctl/proctl.go b/proctl/proctl.go index 46d96610..0f07d3c3 100644 --- a/proctl/proctl.go +++ b/proctl/proctl.go @@ -205,7 +205,11 @@ func (dbp *DebuggedProcess) RequestManualStop() error { // will set a hardware breakpoint. Otherwise we fall back to software // breakpoints, which are a bit more work for us. func (dbp *DebuggedProcess) Break(addr uint64) (*BreakPoint, error) { - return dbp.setBreakpoint(dbp.CurrentThread.Id, addr) + return dbp.setBreakpoint(dbp.CurrentThread.Id, addr, false) +} + +func (dbp *DebuggedProcess) TempBreak(addr uint64) (*BreakPoint, error) { + return dbp.setBreakpoint(dbp.CurrentThread.Id, addr, true) } // Sets a breakpoint by location string (function, file+line, address) @@ -219,30 +223,7 @@ func (dbp *DebuggedProcess) BreakByLocation(loc string) (*BreakPoint, error) { // Clears a breakpoint in the current thread. func (dbp *DebuggedProcess) Clear(addr uint64) (*BreakPoint, error) { - tid := dbp.CurrentThread.Id - // Check for hardware breakpoint - for i, bp := range dbp.HWBreakPoints { - if bp == nil { - continue - } - if bp.Addr == addr { - dbp.HWBreakPoints[i] = nil - if err := clearHardwareBreakpoint(i, tid); err != nil { - return nil, err - } - return bp, nil - } - } - // Check for software breakpoint - if bp, ok := dbp.BreakPoints[addr]; ok { - thread := dbp.Threads[tid] - if _, err := writeMemory(thread, uintptr(bp.Addr), bp.OriginalData); err != nil { - return nil, fmt.Errorf("could not clear breakpoint %s", err) - } - delete(dbp.BreakPoints, addr) - return bp, nil - } - return nil, fmt.Errorf("no breakpoint at %#v", addr) + return dbp.clearBreakpoint(dbp.CurrentThread.Id, addr) } // Clears a breakpoint by location (function, file+line, address, breakpoint id) @@ -261,15 +242,40 @@ func (dbp *DebuggedProcess) Status() *sys.WaitStatus { // Step over function calls. func (dbp *DebuggedProcess) Next() error { + if err := dbp.setChanRecvBreakpoints(); err != nil { + return err + } return dbp.run(dbp.next) } +func (dbp *DebuggedProcess) setChanRecvBreakpoints() error { + allg, err := dbp.GoroutinesInfo() + if err != nil { + return err + } + for _, g := range allg { + if g.ChanRecvBlocked() { + ret, err := g.chanRecvReturnAddr(dbp) + if err != nil { + return err + } + if _, err = dbp.TempBreak(ret); err != nil { + return err + } + } + } + return nil +} + func (dbp *DebuggedProcess) next() error { + defer dbp.clearTempBreakpoints() + curg, err := dbp.CurrentThread.curG() if err != nil { return err } - defer dbp.clearTempBreakpoints() + + var goroutineExiting bool for _, th := range dbp.Threads { if th.blocked() { // Continue threads that aren't running go code. if err := th.Continue(); err != nil { @@ -277,7 +283,16 @@ func (dbp *DebuggedProcess) next() error { } continue } - if err := th.Next(); err != nil { + if err = th.Next(); err != nil { + if err, ok := err.(GoroutineExitingError); ok { + if err.goid == curg.Id { + goroutineExiting = true + } + if err := th.Continue(); err != nil { + return err + } + continue + } return err } } @@ -287,11 +302,19 @@ func (dbp *DebuggedProcess) next() error { if err != nil { return err } - // Check if we've hit a breakpoint. + if goroutineExiting { + break + } if dbp.CurrentBreakpoint != nil { - if err = thread.clearTempBreakpoint(dbp.CurrentBreakpoint.Addr); err != nil { + bp, err := dbp.Clear(dbp.CurrentBreakpoint.Addr) + if err != nil { return err } + if !bp.hardware { + if err = thread.SetPC(bp.Addr); err != nil { + return err + } + } } // Grab the current goroutine for this thread. tg, err := thread.curG() @@ -398,7 +421,7 @@ func (dbp *DebuggedProcess) GoroutinesInfo() ([]*G, error) { allgptr := binary.LittleEndian.Uint64(faddr) for i := uint64(0); i < allglen; i++ { - g, err := parseG(dbp, allgptr+(i*uint64(ptrsize)), reader) + g, err := parseG(dbp.CurrentThread, allgptr+(i*uint64(ptrsize)), reader) if err != nil { return nil, err } @@ -432,7 +455,7 @@ func (dbp *DebuggedProcess) EvalSymbol(name string) (*Variable, error) { return dbp.CurrentThread.EvalSymbol(name) } -func (dbp *DebuggedProcess) CallFn(name string, fn func(*ThreadContext) error) error { +func (dbp *DebuggedProcess) CallFn(name string, fn func() error) error { return dbp.CurrentThread.CallFn(name, fn) } diff --git a/proctl/proctl_test.go b/proctl/proctl_test.go index 1a187d52..4fcca361 100644 --- a/proctl/proctl_test.go +++ b/proctl/proctl_test.go @@ -219,45 +219,18 @@ func TestClearBreakPoint(t *testing.T) { }) } -func TestNext(t *testing.T) { - var ( - err error - executablePath = "../_fixtures/testnextprog" - ) - - testcases := []struct { - begin, end int - }{ - {19, 20}, - {20, 23}, - {23, 24}, - {24, 26}, - {26, 31}, - {31, 23}, - {23, 24}, - {24, 26}, - {26, 31}, - {31, 23}, - {23, 24}, - {24, 26}, - {26, 27}, - {27, 34}, - {34, 41}, - {41, 40}, - {40, 41}, - } - - fp, err := filepath.Abs("../_fixtures/testnextprog.go") - if err != nil { - t.Fatal(err) - } +type nextTest struct { + begin, end int +} +func testnext(testcases []nextTest, initialLocation string, t *testing.T) { + var executablePath = "../_fixtures/testnextprog" withTestProcess(executablePath, t, func(p *DebuggedProcess) { - pc, _, _ := p.goSymTable.LineToPC(fp, testcases[0].begin) - _, err := p.Break(pc) + bp, err := p.BreakByLocation(initialLocation) assertNoError(err, t, "Break()") assertNoError(p.Continue(), t, "Continue()") - p.Clear(pc) + p.Clear(bp.Addr) + p.CurrentThread.SetPC(bp.Addr) f, ln := currentLineNumber(p, t) for _, tc := range testcases { @@ -273,9 +246,8 @@ func TestNext(t *testing.T) { } } - p.Clear(pc) if len(p.BreakPoints) != 0 { - t.Fatal("Not all breakpoints were cleaned up", len(p.HWBreakPoints)) + t.Fatal("Not all breakpoints were cleaned up", len(p.BreakPoints)) } for _, bp := range p.HWBreakPoints { if bp != nil { @@ -285,6 +257,43 @@ func TestNext(t *testing.T) { }) } +func TestNextGeneral(t *testing.T) { + testcases := []nextTest{ + {17, 19}, + {19, 20}, + {20, 23}, + {23, 24}, + {24, 26}, + {26, 31}, + {31, 23}, + {23, 24}, + {24, 26}, + {26, 31}, + {31, 23}, + {23, 24}, + {24, 26}, + {26, 27}, + {27, 34}, + } + testnext(testcases, "main.testnext", t) +} + +func TestNextGoroutine(t *testing.T) { + testcases := []nextTest{ + {46, 47}, + {47, 42}, + } + testnext(testcases, "main.testgoroutine", t) +} + +func TestNextFunctionReturn(t *testing.T) { + testcases := []nextTest{ + {13, 14}, + {14, 35}, + } + testnext(testcases, "main.helloworld", t) +} + func TestFindReturnAddress(t *testing.T) { var testfile, _ = filepath.Abs("../_fixtures/testnextprog") @@ -331,10 +340,9 @@ func TestFindReturnAddress(t *testing.T) { readMemory(p.CurrentThread, uintptr(addr), data) addr = binary.LittleEndian.Uint64(data) - linuxExpected := uint64(0x400fbc) - darwinExpected := uint64(0x23bc) - if addr != linuxExpected && addr != darwinExpected { - t.Fatalf("return address not found correctly, expected (linux) %#v or (darwin) %#v got %#v", linuxExpected, darwinExpected, addr) + _, l, _ := p.goSymTable.PCToLine(addr) + if l != 40 { + t.Fatalf("return address not found correctly, expected line 40") } }) } @@ -409,7 +417,8 @@ func TestFunctionCall(t *testing.T) { if fn.Name != "main.main" { t.Fatal("Program stopped at incorrect place") } - if err = p.CallFn("runtime.getg", func(th *ThreadContext) error { + if err = p.CallFn("runtime.getg", func() error { + th := p.CurrentThread pc, err := th.CurrentPC() if err != nil { t.Fatal(err) diff --git a/proctl/stack.go b/proctl/stack.go new file mode 100644 index 00000000..6a58111c --- /dev/null +++ b/proctl/stack.go @@ -0,0 +1,53 @@ +package proctl + +import ( + "debug/gosym" + "encoding/binary" +) + +type stackLocation struct { + addr uint64 + file string + line int + fn *gosym.Func +} + +// Takes an offset from RSP and returns the address of the +// instruction the currect function is going to return to. +func (thread *ThreadContext) ReturnAddress() (uint64, error) { + regs, err := thread.Registers() + if err != nil { + return 0, err + } + locations, err := thread.Process.stacktrace(regs.PC(), regs.SP(), 1) + if err != nil { + return 0, err + } + return locations[0].addr, nil +} + +func (dbp *DebuggedProcess) stacktrace(pc, sp uint64, depth int) ([]stackLocation, error) { + var ( + ret = pc + data = make([]byte, 8) + btoffset int64 + locations []stackLocation + retaddr uintptr + ) + for i := int64(0); i < int64(depth); i++ { + fde, err := dbp.frameEntries.FDEForPC(ret) + if err != nil { + return nil, err + } + btoffset += fde.ReturnAddressOffset(ret) + retaddr = uintptr(int64(sp) + btoffset + (i * 8)) + _, err = readMemory(dbp.CurrentThread, retaddr, data) + if err != nil { + return nil, err + } + ret = binary.LittleEndian.Uint64(data) + f, l, fn := dbp.goSymTable.PCToLine(ret) + locations = append(locations, stackLocation{addr: ret, file: f, line: l, fn: fn}) + } + return locations, nil +} diff --git a/proctl/threads.go b/proctl/threads.go index 7b8d0828..2e77408f 100644 --- a/proctl/threads.go +++ b/proctl/threads.go @@ -1,9 +1,10 @@ package proctl import ( - "encoding/binary" "fmt" + "path/filepath" + "github.com/derekparker/delve/dwarf/frame" sys "golang.org/x/sys/unix" ) @@ -16,6 +17,7 @@ type ThreadContext struct { Id int Process *DebuggedProcess Status *sys.WaitStatus + running bool os *OSSpecificDetails } @@ -65,7 +67,6 @@ func (thread *ThreadContext) Continue() error { return fmt.Errorf("could not step %s", err) } } - return thread.resume() } @@ -108,14 +109,14 @@ func (thread *ThreadContext) Step() (err error) { } // Call a function named `name`. This is currently _NOT_ safe. -func (thread *ThreadContext) CallFn(name string, fn func(*ThreadContext) error) error { +func (thread *ThreadContext) CallFn(name string, fn func() error) error { f := thread.Process.goSymTable.LookupFunc(name) if f == nil { return fmt.Errorf("could not find function %s", name) } // Set breakpoint at the end of the function (before it returns). - bp, err := thread.Process.Break(f.End - 2) + bp, err := thread.Break(f.End - 2) if err != nil { return err } @@ -134,7 +135,17 @@ func (thread *ThreadContext) CallFn(name string, fn func(*ThreadContext) error) if _, err = trapWait(thread.Process, -1); err != nil { return err } - return fn(thread) + return fn() +} + +// Set breakpoint using this thread. +func (thread *ThreadContext) Break(addr uint64) (*BreakPoint, error) { + return thread.Process.setBreakpoint(thread.Id, addr, false) +} + +// Clear breakpoint using this thread. +func (thread *ThreadContext) Clear(addr uint64) (*BreakPoint, error) { + return thread.Process.clearBreakpoint(thread.Id, addr) } // Step to next source line. @@ -166,34 +177,88 @@ func (thread *ThreadContext) Next() (err error) { // Get current file/line. f, l, _ := thread.Process.goSymTable.PCToLine(curpc) + if filepath.Ext(f) == ".go" { + if err = thread.next(curpc, fde, f, l); err != nil { + return err + } + } else { + if err = thread.cnext(curpc, fde, f, l); err != nil { + return err + } + } + return thread.Continue() +} - // Find any line we could potentially get to. - lines, err := thread.Process.ast.NextLines(f, l) +// Go routine is exiting. +type GoroutineExitingError struct { + goid int +} + +func (ge GoroutineExitingError) Error() string { + return fmt.Sprintf("goroutine %d is exiting", ge.goid) +} + +// This version of next uses the AST from the current source file to figure out all of the potential source lines +// we could end up at. +func (thread *ThreadContext) next(curpc uint64, fde *frame.FrameDescriptionEntry, file string, line int) error { + lines, err := thread.Process.ast.NextLines(file, line) if err != nil { return err } - // Set a breakpoint at every line reachable from our location. - for _, l := range lines { - pcs := thread.Process.lineInfo.AllPCsForFileLine(f, l) - for _, pc := range pcs { - if pc == curpc { - continue - } - if !fde.Cover(pc) { - pc = thread.ReturnAddressFromOffset(fde.ReturnAddressOffset(curpc)) - } - bp, err := thread.Process.Break(pc) - if err != nil { - if err, ok := err.(BreakPointExistsError); !ok { - return err - } - continue - } - bp.Temp = true + ret, err := thread.ReturnAddress() + if err != nil { + return err + } + + pcs := make([]uint64, 0, len(lines)) + for i := range lines { + pcs = append(pcs, thread.Process.lineInfo.AllPCsForFileLine(file, lines[i])...) + } + + var covered bool + for i := range pcs { + if fde.Cover(pcs[i]) { + covered = true + break } } - return thread.Continue() + + if !covered { + fn := thread.Process.goSymTable.PCToFunc(ret) + if fn != nil && fn.Name == "runtime.goexit" { + g, err := thread.curG() + if err != nil { + return err + } + return GoroutineExitingError{goid: g.Id} + } + pcs = append(pcs, ret) + } + + // Set a breakpoint at every line reachable from our location. + for _, pc := range pcs { + // Do not set breakpoint at our current location. + if pc == curpc { + continue + } + // If the PC is not covered by our frame, set breakpoint at return address. + if !fde.Cover(pc) { + pc = ret + } + if _, err := thread.Process.TempBreak(pc); err != nil { + if err, ok := err.(BreakPointExistsError); !ok { + return err + } + } + } + return nil +} + +func (thread *ThreadContext) cnext(curpc uint64, fde *frame.FrameDescriptionEntry, file string, line int) error { + // TODO(dp) We are not in a Go source file, we should fall back on DWARF line information + // and use that to set breakpoints. + return nil } func (thread *ThreadContext) SetPC(pc uint64) error { @@ -204,47 +269,15 @@ func (thread *ThreadContext) SetPC(pc uint64) error { return regs.SetPC(thread, pc) } -// Takes an offset from RSP and returns the address of the -// instruction the currect function is going to return to. -func (thread *ThreadContext) ReturnAddressFromOffset(offset int64) uint64 { - regs, err := thread.Registers() - if err != nil { - panic("Could not obtain register values") - } - - retaddr := int64(regs.SP()) + offset - data := make([]byte, 8) - readMemory(thread, uintptr(retaddr), data) - return binary.LittleEndian.Uint64(data) -} - -func (thread *ThreadContext) clearTempBreakpoint(pc uint64) error { - clearbp := func(bp *BreakPoint) error { - if _, err := thread.Process.Clear(bp.Addr); err != nil { - return err - } - return thread.SetPC(bp.Addr) - } - for _, bp := range thread.Process.HWBreakPoints { - if bp != nil && bp.Temp && bp.Addr == pc { - return clearbp(bp) - } - } - if bp, ok := thread.Process.BreakPoints[pc]; ok && bp.Temp { - return clearbp(bp) - } - return nil -} - func (thread *ThreadContext) curG() (*G, error) { var g *G - err := thread.CallFn("runtime.getg", func(t *ThreadContext) error { - regs, err := t.Registers() + err := thread.CallFn("runtime.getg", func() error { + regs, err := thread.Registers() if err != nil { return err } - reader := t.Process.dwarf.Reader() - g, err = parseG(t.Process, regs.SP()+uint64(ptrsize), reader) + reader := thread.Process.dwarf.Reader() + g, err = parseG(thread, regs.SP()+uint64(ptrsize), reader) return err }) return g, err diff --git a/proctl/threads_darwin.go b/proctl/threads_darwin.go index 14e322f3..eabd4306 100644 --- a/proctl/threads_darwin.go +++ b/proctl/threads_darwin.go @@ -16,7 +16,7 @@ func (t *ThreadContext) Halt() error { var kret C.kern_return_t kret = C.thread_suspend(t.os.thread_act) if kret != C.KERN_SUCCESS { - return fmt.Errorf("could not suspend task %d", t.Id) + return fmt.Errorf("could not suspend thread %d", t.Id) } return nil } @@ -50,7 +50,7 @@ func (t *ThreadContext) blocked() bool { // TODO(dp) cache the func pc to remove this lookup pc, _ := t.CurrentPC() fn := t.Process.goSymTable.PCToFunc(pc) - if fn != nil && ((fn.Name == "runtime.mach_semaphore_wait") || (fn.Name == "runtime.usleep")) { + if fn != nil && (fn.Name == "runtime.mach_semaphore_wait") { return true } return false diff --git a/proctl/variables.go b/proctl/variables.go index 4554bc45..c6348cf0 100644 --- a/proctl/variables.go +++ b/proctl/variables.go @@ -17,6 +17,9 @@ import ( const ( maxVariableRecurse = 1 maxArrayValues = 64 + + ChanRecv = "chan receive" + ChanSend = "chan send" ) type Variable struct { @@ -33,11 +36,28 @@ type M struct { } type G struct { - Id int - PC uint64 - File string - Line int - Func *gosym.Func + Id int + PC uint64 + SP uint64 + GoPC uint64 + File string + Line int + Func *gosym.Func + WaitReason string +} + +func (g *G) ChanRecvBlocked() bool { + return g.WaitReason == ChanRecv +} + +// chanRecvReturnAddr returns the address of the return from a channel read. +func (g *G) chanRecvReturnAddr(dbp *DebuggedProcess) (uint64, error) { + locs, err := dbp.stacktrace(g.PC, g.SP, 4) + if err != nil { + return 0, err + } + topLoc := locs[len(locs)-1] + return topLoc.addr, nil } const ptrsize uintptr = unsafe.Sizeof(int(1)) @@ -209,40 +229,80 @@ func parseAllMPtr(dbp *DebuggedProcess, reader *dwarf.Reader) (uint64, error) { return uint64(addr), nil } -func parseG(dbp *DebuggedProcess, addr uint64, reader *dwarf.Reader) (*G, error) { - gaddrbytes, err := dbp.CurrentThread.readMemory(uintptr(addr), ptrsize) +type NoGError struct { + tid int +} + +func (ng NoGError) Error() string { + return fmt.Sprintf("no G executing on thread %d", ng.tid) +} + +func parseG(thread *ThreadContext, addr uint64, reader *dwarf.Reader) (*G, error) { + gaddrbytes, err := thread.readMemory(uintptr(addr), ptrsize) if err != nil { return nil, fmt.Errorf("error derefing *G %s", err) } initialInstructions := append([]byte{op.DW_OP_addr}, gaddrbytes...) + gaddr := binary.LittleEndian.Uint64(gaddrbytes) + if gaddr == 0 { + return nil, NoGError{tid: thread.Id} + } reader.Seek(0) - goidaddr, err := offsetFor(dbp, "goid", reader, initialInstructions) + goidaddr, err := offsetFor("goid", reader, initialInstructions) if err != nil { return nil, err } - reader.Seek(0) - schedaddr, err := offsetFor(dbp, "sched", reader, initialInstructions) - if err != nil { - return nil, err - } - - goidbytes, err := dbp.CurrentThread.readMemory(uintptr(goidaddr), ptrsize) + goidbytes, err := thread.readMemory(uintptr(goidaddr), ptrsize) if err != nil { return nil, fmt.Errorf("error reading goid %s", err) } - schedbytes, err := dbp.CurrentThread.readMemory(uintptr(schedaddr+uint64(ptrsize)), ptrsize) + reader.Seek(0) + gopcaddr, err := offsetFor("gopc", reader, initialInstructions) if err != nil { - return nil, fmt.Errorf("error reading sched %s", err) + return nil, err } - gopc := binary.LittleEndian.Uint64(schedbytes) - f, l, fn := dbp.goSymTable.PCToLine(gopc) + gopcbytes, err := thread.readMemory(uintptr(gopcaddr), ptrsize) + if err != nil { + return nil, fmt.Errorf("error reading gopc %s", err) + } + reader.Seek(0) + schedaddr, err := offsetFor("sched", reader, initialInstructions) + if err != nil { + return nil, err + } + reader.Seek(0) + waitreasonaddr, err := offsetFor("waitreason", reader, initialInstructions) + if err != nil { + return nil, err + } + waitreason, err := thread.readString(uintptr(waitreasonaddr)) + if err != nil { + return nil, err + } + + spbytes, err := thread.readMemory(uintptr(schedaddr), ptrsize) + if err != nil { + return nil, fmt.Errorf("error reading goroutine SP %s", err) + } + gosp := binary.LittleEndian.Uint64(spbytes) + + pcbytes, err := thread.readMemory(uintptr(schedaddr+uint64(ptrsize)), ptrsize) + if err != nil { + return nil, fmt.Errorf("error reading goroutine PC %s", err) + } + gopc := binary.LittleEndian.Uint64(pcbytes) + + f, l, fn := thread.Process.goSymTable.PCToLine(gopc) g := &G{ - Id: int(binary.LittleEndian.Uint64(goidbytes)), - PC: gopc, - File: f, - Line: l, - Func: fn, + Id: int(binary.LittleEndian.Uint64(goidbytes)), + GoPC: binary.LittleEndian.Uint64(gopcbytes), + PC: gopc, + SP: gosp, + File: f, + Line: l, + Func: fn, + WaitReason: waitreason, } return g, nil } @@ -286,7 +346,7 @@ func addressFor(dbp *DebuggedProcess, name string, reader *dwarf.Reader) (uint64 return uint64(addr), nil } -func offsetFor(dbp *DebuggedProcess, name string, reader *dwarf.Reader, parentinstr []byte) (uint64, error) { +func offsetFor(name string, reader *dwarf.Reader, parentinstr []byte) (uint64, error) { entry, err := findDwarfEntry(name, reader, true) if err != nil { return 0, err @@ -895,13 +955,15 @@ func (thread *ThreadContext) readFunctionPtr(addr uintptr) (string, error) { } func (thread *ThreadContext) readMemory(addr uintptr, size uintptr) ([]byte, error) { - buf := make([]byte, size) + if size == 0 { + return nil, nil + } + buf := make([]byte, size) _, err := readMemory(thread, addr, buf) if err != nil { return nil, err } - return buf, nil } diff --git a/source/source.go b/source/source.go index 0f30de21..d1b725e9 100644 --- a/source/source.go +++ b/source/source.go @@ -1,6 +1,7 @@ package source import ( + "fmt" "go/ast" "go/parser" "go/token" @@ -33,6 +34,9 @@ func (s *Searcher) FirstNodeAt(fname string, line int) (ast.Node, error) { } return true }) + if node == nil { + return nil, fmt.Errorf("could not find node at %s:%d", fname, line) + } return node, nil } @@ -83,11 +87,11 @@ func (s *Searcher) NextLines(fname string, line int) (lines []int, err error) { if x.Else == nil { // Grab first line after entire 'if' block rbrace = s.fileset.Position(x.Body.Rbrace).Line - n, err := s.FirstNodeAt(fname, 1) + f, err := s.parse(fname) if err != nil { return nil, err } - ast.Inspect(n, func(n ast.Node) bool { + ast.Inspect(f, func(n ast.Node) bool { if n == nil { return true } @@ -156,9 +160,8 @@ func (s *Searcher) NextLines(fname string, line int) (lines []int, err error) { // end up at the top of the loop again. default: var ( - parents []*ast.BlockStmt - parentLines []int - parentLine int + parents []*ast.BlockStmt + parentBlockBeginLine int ) f, err := s.parse(fname) if err != nil { @@ -174,54 +177,85 @@ func (s *Searcher) NextLines(fname string, line int) (lines []int, err error) { if stmt, ok := n.(*ast.ForStmt); ok { parents = append(parents, stmt.Body) pos := s.fileset.Position(stmt.Pos()) - parentLine = pos.Line - parentLines = append(parentLines, pos.Line) + parentBlockBeginLine = pos.Line } + + if _, ok := n.(*ast.GenDecl); ok { + return true + } + + if st, ok := n.(*ast.DeclStmt); ok { + beginpos := s.fileset.Position(st.Pos()) + endpos := s.fileset.Position(st.End()) + if beginpos.Line < endpos.Line { + return true + } + } + + // Check to see if we've found the "next" line. pos := s.fileset.Position(n.Pos()) if line < pos.Line { if _, ok := n.(*ast.BlockStmt); ok { return true } - for { - if 0 < len(parents) { - parent := parents[len(parents)-1] - endLine := s.fileset.Position(parent.Rbrace).Line - if endLine < line { - if len(parents) == 1 { - parents = []*ast.BlockStmt{} - parentLines = []int{} - parentLine = 0 - } else { - parents = parents[0 : len(parents)-1] - parentLines = parentLines[0:len(parents)] - parent = parents[len(parents)-1] - parentLine = s.fileset.Position(parent.Pos()).Line - } - continue - } - if parentLine != 0 { - var endfound bool - ast.Inspect(f, func(n ast.Node) bool { - if n == nil || endfound { - return false - } - if _, ok := n.(*ast.BlockStmt); ok { - return true - } - pos := s.fileset.Position(n.Pos()) - if endLine < pos.Line { - endLine = pos.Line - endfound = true - return false - } - return true - }) - lines = append(lines, parentLine, endLine) - } + var ( + parent *ast.BlockStmt + parentEndLine int + ) + for len(parents) > 0 { + parent = parents[len(parents)-1] + + // Grab the line number of the right brace of the parent block. + parentEndLine = s.fileset.Position(parent.Rbrace).Line + + // Check to see if we're still within the parents block. + // If we are, we're done and that is our parent. + if parentEndLine > line { + parentBlockBeginLine = s.fileset.Position(parent.Pos()).Line + break } - break + // If we weren't, and there is only 1 parent, we no longer have one. + if len(parents) == 1 { + parent = nil + break + } + // Remove that parent from the stack. + parents = parents[0 : len(parents)-1] } - if _, ok := n.(*ast.BranchStmt); !ok { + if parent != nil { + var ( + endfound bool + beginFound bool + beginLine int + ) + + ast.Inspect(f, func(n ast.Node) bool { + if n == nil || endfound { + return false + } + if _, ok := n.(*ast.BlockStmt); ok { + return true + } + pos := s.fileset.Position(n.Pos()) + if parentBlockBeginLine < pos.Line && !beginFound { + beginFound = true + beginLine = pos.Line + return true + } + if parentEndLine < pos.Line { + if _, ok := n.(*ast.FuncDecl); !ok { + lines = append(lines, beginLine, pos.Line) + } + endfound = true + return false + } + return true + }) + lines = append(lines, parentBlockBeginLine) + } + switch n.(type) { + case *ast.BranchStmt, *ast.FuncDecl: + default: lines = append(lines, pos.Line) } found = true diff --git a/source/source_test.go b/source/source_test.go index aceb70ec..da5c356c 100644 --- a/source/source_test.go +++ b/source/source_test.go @@ -33,11 +33,13 @@ func TestNextLines(t *testing.T) { {8, []int{9, 10, 13}}, {15, []int{17, 19}}, {25, []int{27}}, - {22, []int{6, 25}}, + {22, []int{7, 25, 6}}, {33, []int{36}}, {36, []int{37, 40}}, - {47, []int{44, 51}}, - {57, []int{55, 56}}, + {47, []int{45, 51, 44}}, + {57, []int{55}}, + {30, []int{32}}, + {62, []int{63}}, } for i, c := range cases { lines, err := v.NextLines(tf, c.line)