mirror of
https://github.com/go-delve/delve.git
synced 2025-11-01 20:20:40 +08:00
Since fixing scheduler handling bugs, a new bug was exposed where Step was calling Clear off of the DebuggedProcess struct. This is incorrect, and should be handled by the thread itself and not delegated.
344 lines
8.0 KiB
Go
344 lines
8.0 KiB
Go
package proctl
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
"github.com/derekparker/delve/dwarf/frame"
|
|
)
|
|
|
|
// ThreadContext represents a single thread of execution in the
|
|
// traced program.
|
|
type ThreadContext struct {
|
|
Id int
|
|
Process *DebuggedProcess
|
|
Status *syscall.WaitStatus
|
|
Regs *syscall.PtraceRegs
|
|
}
|
|
|
|
// Obtains register values from the debugged process.
|
|
func (thread *ThreadContext) Registers() (*syscall.PtraceRegs, error) {
|
|
err := syscall.PtraceGetRegs(thread.Id, thread.Regs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return thread.Regs, nil
|
|
}
|
|
|
|
// Returns the current PC for this thread id.
|
|
func (thread *ThreadContext) CurrentPC() (uint64, error) {
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return regs.PC(), nil
|
|
}
|
|
|
|
// PrintInfo prints out the thread status
|
|
// including: PC, tid, file, line, and function.
|
|
func (thread *ThreadContext) PrintInfo() error {
|
|
pc, err := thread.CurrentPC()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f, l, fn := thread.Process.GoSymTable.PCToLine(pc)
|
|
if fn != nil {
|
|
fmt.Printf("Thread %d at %#v %s:%d %s\n", thread.Id, pc, f, l, fn.Name)
|
|
} else {
|
|
fmt.Printf("Thread %d at %#v\n", thread.Id, pc)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Sets a software breakpoint at addr, and stores it in the process wide
|
|
// break point table. Setting a break point must be thread specific due to
|
|
// ptrace actions needing the thread to be in a signal-delivery-stop in order
|
|
// to initiate any ptrace command. Otherwise, it really doesn't matter
|
|
// as we're only dealing with threads.
|
|
func (thread *ThreadContext) Break(addr uintptr) (*BreakPoint, error) {
|
|
var (
|
|
int3 = []byte{0xCC}
|
|
f, l, fn = thread.Process.GoSymTable.PCToLine(uint64(addr))
|
|
originalData = make([]byte, 1)
|
|
)
|
|
|
|
if fn == nil {
|
|
return nil, InvalidAddressError{address: addr}
|
|
}
|
|
|
|
_, err := syscall.PtracePeekData(thread.Id, addr, originalData)
|
|
if err != nil {
|
|
fmt.Println("PEEK ERR")
|
|
return nil, err
|
|
}
|
|
|
|
if bytes.Equal(originalData, int3) {
|
|
return nil, BreakPointExistsError{f, l, addr}
|
|
}
|
|
|
|
_, err = syscall.PtracePokeData(thread.Id, addr, int3)
|
|
if err != nil {
|
|
fmt.Println("POKE ERR")
|
|
return nil, err
|
|
}
|
|
|
|
breakpoint := &BreakPoint{
|
|
FunctionName: fn.Name,
|
|
File: f,
|
|
Line: l,
|
|
Addr: uint64(addr),
|
|
OriginalData: originalData,
|
|
}
|
|
|
|
thread.Process.BreakPoints[uint64(addr)] = breakpoint
|
|
|
|
return breakpoint, nil
|
|
}
|
|
|
|
// Clears a software breakpoint, and removes it from the process level
|
|
// break point table.
|
|
func (thread *ThreadContext) Clear(pc uint64) (*BreakPoint, error) {
|
|
bp, ok := thread.Process.BreakPoints[pc]
|
|
if !ok {
|
|
return nil, fmt.Errorf("No breakpoint currently set for %#v", pc)
|
|
}
|
|
|
|
if _, err := syscall.PtracePokeData(thread.Id, uintptr(bp.Addr), bp.OriginalData); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
delete(thread.Process.BreakPoints, pc)
|
|
|
|
return bp, nil
|
|
}
|
|
|
|
func (thread *ThreadContext) Continue() error {
|
|
// Check whether we are stopped at a breakpoint, and
|
|
// if so, single step over it before continuing.
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get registers %s", err)
|
|
}
|
|
|
|
if _, ok := thread.Process.BreakPoints[regs.PC()-1]; ok {
|
|
err := thread.Step()
|
|
if err != nil {
|
|
return fmt.Errorf("could not step %s", err)
|
|
}
|
|
}
|
|
|
|
return syscall.PtraceCont(thread.Id, 0)
|
|
}
|
|
|
|
// Single steps this thread a single instruction, ensuring that
|
|
// we correctly handle the likely case that we are at a breakpoint.
|
|
func (thread *ThreadContext) Step() (err error) {
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get registers %s", err)
|
|
}
|
|
|
|
bp, ok := thread.Process.BreakPoints[regs.PC()-1]
|
|
if ok {
|
|
// Clear the breakpoint so that we can continue execution.
|
|
_, err = thread.Clear(bp.Addr)
|
|
if err != nil {
|
|
return fmt.Errorf("could not clear breakpoint %s", err)
|
|
}
|
|
|
|
// Reset program counter to our restored instruction.
|
|
regs.SetPC(bp.Addr)
|
|
err = syscall.PtraceSetRegs(thread.Id, regs)
|
|
if err != nil {
|
|
return fmt.Errorf("could not set registers %s", err)
|
|
}
|
|
|
|
// Restore breakpoint now that we have passed it.
|
|
defer func() {
|
|
_, err = thread.Break(uintptr(bp.Addr))
|
|
}()
|
|
}
|
|
|
|
err = syscall.PtraceSingleStep(thread.Id)
|
|
if err != nil {
|
|
return fmt.Errorf("step failed: %s", err.Error())
|
|
}
|
|
|
|
_, _, err = wait(thread.Id, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Step to next source line. Next will step over functions,
|
|
// and will follow through to the return address of a function.
|
|
// Next is implemented on the thread context, however during the
|
|
// course of this function running, it's very likely that the
|
|
// goroutine our M is executing will switch to another M, therefore
|
|
// this function cannot assume all execution will happen on this thread
|
|
// in the traced process.
|
|
func (thread *ThreadContext) Next() (err error) {
|
|
pc, err := thread.CurrentPC()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, ok := thread.Process.BreakPoints[pc-1]; ok {
|
|
pc-- // Decrement PC to account for BreakPoint
|
|
}
|
|
|
|
_, l, _ := thread.Process.GoSymTable.PCToLine(pc)
|
|
fde, err := thread.Process.FrameEntries.FDEForPC(pc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
step := func() (uint64, error) {
|
|
err = thread.Step()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return thread.CurrentPC()
|
|
}
|
|
|
|
ret := thread.ReturnAddressFromOffset(fde.ReturnAddressOffset(pc))
|
|
for {
|
|
pc, err = step()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !fde.Cover(pc) && pc != ret {
|
|
err := thread.continueToReturnAddress(pc, fde)
|
|
if err != nil {
|
|
if _, ok := err.(InvalidAddressError); !ok {
|
|
return err
|
|
}
|
|
}
|
|
pc, _ = thread.CurrentPC()
|
|
}
|
|
|
|
_, nl, _ := thread.Process.GoSymTable.PCToLine(pc)
|
|
if nl != l {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (thread *ThreadContext) continueToReturnAddress(pc uint64, fde *frame.FrameDescriptionEntry) error {
|
|
for !fde.Cover(pc) {
|
|
// Our offset here is be 0 because we
|
|
// have stepped into the first instruction
|
|
// of this function. Therefore the function
|
|
// has not had a chance to modify its' stack
|
|
// and change our offset.
|
|
addr := thread.ReturnAddressFromOffset(0)
|
|
bp, err := thread.Break(uintptr(addr))
|
|
if err != nil {
|
|
if _, ok := err.(BreakPointExistsError); !ok {
|
|
return err
|
|
}
|
|
}
|
|
bp.temp = true
|
|
// Ensure we cleanup after ourselves no matter what.
|
|
defer thread.clearTempBreakpoint(bp.Addr)
|
|
|
|
for {
|
|
err = thread.Continue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// We wait on -1 here because once we continue this
|
|
// thread, it's very possible the scheduler could of
|
|
// change the goroutine context on us, we there is
|
|
// no guarantee that waiting on this tid will ever
|
|
// return.
|
|
wpid, _, err := trapWait(thread.Process, -1, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if wpid != thread.Id {
|
|
thread = thread.Process.Threads[wpid]
|
|
}
|
|
pc, _ = thread.CurrentPC()
|
|
if (pc - 1) == bp.Addr {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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.Rsp) + offset
|
|
data := make([]byte, 8)
|
|
syscall.PtracePeekText(thread.Id, uintptr(retaddr), data)
|
|
return binary.LittleEndian.Uint64(data)
|
|
}
|
|
|
|
func (thread *ThreadContext) clearTempBreakpoint(pc uint64) error {
|
|
if bp, ok := thread.Process.BreakPoints[pc]; ok {
|
|
_, err := thread.Clear(bp.Addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reset program counter to our restored instruction.
|
|
regs, err := thread.Registers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
regs.SetPC(bp.Addr)
|
|
return syscall.PtraceSetRegs(thread.Id, regs)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func threadIds(pid int) []int {
|
|
var threads []int
|
|
dir, err := os.Open(fmt.Sprintf("/proc/%d/task", pid))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer dir.Close()
|
|
|
|
names, err := dir.Readdirnames(0)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, strid := range names {
|
|
tid, err := strconv.Atoi(strid)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if tid != pid {
|
|
threads = append(threads, tid)
|
|
}
|
|
}
|
|
|
|
return threads
|
|
}
|