From 64e01bfed13be5173f04eec01daef09654eb68a5 Mon Sep 17 00:00:00 2001 From: Derek Parker Date: Mon, 8 Dec 2014 17:40:59 -0600 Subject: [PATCH] Begin thread code isolation --- proctl/proctl_test.go | 3 +- proctl/threads.go | 315 ++++++++++++++++++++++++++++++++++ proctl/threads_linux_amd64.go | 314 +-------------------------------- 3 files changed, 321 insertions(+), 311 deletions(-) create mode 100644 proctl/threads.go diff --git a/proctl/proctl_test.go b/proctl/proctl_test.go index 70cdcaeb..aa935b92 100644 --- a/proctl/proctl_test.go +++ b/proctl/proctl_test.go @@ -3,7 +3,6 @@ package proctl_test import ( "bytes" "path/filepath" - "syscall" "testing" "github.com/derekparker/delve/helper" @@ -12,7 +11,7 @@ import ( func dataAtAddr(pid int, addr uint64) ([]byte, error) { data := make([]byte, 1) - _, err := syscall.PtracePeekData(pid, uintptr(addr), data) + _, err := proctl.ReadMemory(pid, uintptr(addr), data) if err != nil { return nil, err } diff --git a/proctl/threads.go b/proctl/threads.go new file mode 100644 index 00000000..8f1eac54 --- /dev/null +++ b/proctl/threads.go @@ -0,0 +1,315 @@ +package proctl + +import ( + "bytes" + "encoding/binary" + "fmt" + + "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, fmt.Errorf("could not get registers %s", 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 := ReadMemory(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 = WriteMemory(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 := WriteMemory(thread.Id, uintptr(bp.Addr), bp.OriginalData); err != nil { + return nil, fmt.Errorf("could not clear breakpoint %s", 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 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 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 +} diff --git a/proctl/threads_linux_amd64.go b/proctl/threads_linux_amd64.go index 9c05427a..b4146979 100644 --- a/proctl/threads_linux_amd64.go +++ b/proctl/threads_linux_amd64.go @@ -1,315 +1,11 @@ package proctl -import ( - "bytes" - "encoding/binary" - "fmt" +import "syscall" - "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 +func WriteMemory(tid int, addr uintptr, data []byte) (int, error) { + return syscall.PtracePokeData(tid, addr, data) } -// 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, fmt.Errorf("could not get registers %s", 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, fmt.Errorf("could not clear breakpoint %s", 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 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 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 ReadMemory(tid int, addr uintptr, data []byte) (int, error) { + return syscall.PtracePeekData(tid, addr, data) }