Skip to content

Commit

Permalink
A very basic debugger that lets you set breakpoints!
Browse files Browse the repository at this point in the history
  • Loading branch information
lizrice authored Nov 2, 2017
1 parent 7ef985c commit 6a741eb
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 0 deletions.
50 changes: 50 additions & 0 deletions input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)

func inputContinue(pid int) bool {
sub := false
scanner := bufio.NewScanner(os.Stdin)
fmt.Printf("\n(C)ontinue, (S)tep, set (B)reakpoint or (Q)uit? > ")
for {
scanner.Scan()
input := scanner.Text()
switch strings.ToUpper(input) {
case "C":
return true
case "S":
return false
case "B":
fmt.Printf(" Enter line number in %s: > ", targetfile)
sub = true
case "Q":
os.Exit(0)
default:
if sub {
line, _ = strconv.Atoi(input)
breakpointSet, originalCode = setBreak(pid, targetfile, line)
return true
}
fmt.Printf("Unexpected input %s\n", input)
fmt.Printf("\n(C)ontinue, (S)tep, set (B)reakpoint or (Q)uit? > ")
}
}
}

func setBreak(pid int, filename string, line int) (bool, []byte) {
var err error
pc, _, err = symTable.LineToPC(filename, line)
if err != nil {
fmt.Printf("Can't find breakpoint for %s, %d\n", filename, line)
return false, []byte{}
}

// fmt.Printf("Stopping at %X\n", pc)
return true, replaceCode(pid, pc, []byte{0xCC})
}
104 changes: 104 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"debug/gosym"
"fmt"
"os"
"os/exec"
"syscall"
)

var targetfile string
var line int
var pc uint64
var fn *gosym.Func
var symTable *gosym.Table
var regs syscall.PtraceRegs
var ws syscall.WaitStatus
var originalCode []byte
var breakpointSet bool

var interruptCode = []byte{0xCC}

func main() {
target := os.Args[1]
symTable = getSymbolTable(target)
fn = symTable.LookupFunc("main.main")
targetfile, line, fn = symTable.PCToLine(fn.Entry)
run(target)
}

func run(target string) {
var filename string

cmd := exec.Command(target)
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.SysProcAttr = &syscall.SysProcAttr{
Ptrace: true,
}

cmd.Start()
err := cmd.Wait()
if err != nil {
fmt.Printf("Wait returned: %v\n\n", err)
}

pid := cmd.Process.Pid
pgid, _ := syscall.Getpgid(pid)

must(syscall.PtraceSetOptions(pid, syscall.PTRACE_O_TRACECLONE))

if inputContinue(pid) {
must(syscall.PtraceCont(pid, 0))
} else {
must(syscall.PtraceSingleStep(pid))
}

for {
wpid, err := syscall.Wait4(-1*pgid, &ws, 0, nil)
must(err)
if ws.Exited() {
if wpid == pid {
break
}
} else {
// We are only interested in tracing if we're stopped by a trap and
// if the trap was generated by our breakpoint.
// Cloning a child process also generates a trap, and we want to ignore that.
if ws.StopSignal() == syscall.SIGTRAP && ws.TrapCause() != syscall.PTRACE_EVENT_CLONE {
must(syscall.PtraceGetRegs(wpid, &regs))
filename, line, fn = symTable.PCToLine(regs.Rip)
fmt.Printf("Stopped at %s at %d in %s\n", fn.Name, line, filename)
outputStack(symTable, wpid, &regs)

if breakpointSet {
replaceCode(wpid, pc, originalCode)
breakpointSet = false
}

if inputContinue(wpid) {
must(syscall.PtraceCont(wpid, 0))
} else {
must(syscall.PtraceSingleStep(wpid))
}
} else {
must(syscall.PtraceCont(wpid, 0))
}
}
}
}

func replaceCode(pid int, breakpoint uint64, code []byte) []byte {
original := make([]byte, len(code))
syscall.PtracePeekData(pid, uintptr(breakpoint), original)
syscall.PtracePokeData(pid, uintptr(breakpoint), code)
return original
}

func must(err error) {
if err != nil {
panic(err)
}
}
64 changes: 64 additions & 0 deletions output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package main

import (
"debug/gosym"
"encoding/binary"
"fmt"
"syscall"
)

func outputStack(symTable *gosym.Table, pid int, regs *syscall.PtraceRegs) {

// fmt.Printf("%#v\n", regs)

_, _, fn = symTable.PCToLine(regs.Rip)

sp := regs.Rsp
bp := regs.Rbp
var i uint64
var nextbp uint64

for {
i = 0
frameSize := bp - sp + 8

if frameSize > 1000 || bp == 0 {
fmt.Printf("Strange frame size: SP: %X | BP : %X \n", sp, bp)
return
// frameSize = 32
// bp = sp + frameSize - 8
}

b := make([]byte, frameSize)
_, err := syscall.PtracePeekData(pid, uintptr(sp), b)
if err != nil {
panic(err)
}

content := binary.LittleEndian.Uint64(b[i : i+8])
_, lineno, nextfn := symTable.PCToLine(content)
if nextfn != nil {
fn = nextfn
// fmt.Printf(" %X %X: return to %s line %d\n", sp, content, fn.Name, lineno)
fmt.Printf(" called by %s line %d\n", fn.Name, lineno)
}

for i = 8; sp+i <= bp; i += 8 {
content := binary.LittleEndian.Uint64(b[i : i+8])
if sp+i == bp {
// fmt.Printf("Frame added calling %s\n", fn.Name)
nextbp = content
}
// fmt.Printf(" %X %X \n", sp+i, content)
}

if fn.Name == "main.main" || fn.Name == "runtime.main" {
break
}

sp = sp + i
bp = nextbp
}

fmt.Println()
}
37 changes: 37 additions & 0 deletions symbols.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"debug/elf"
"debug/gosym"
)

func getSymbolTable(prog string) *gosym.Table {
exe, err := elf.Open(prog)
if err != nil {
panic(err)
}
defer exe.Close()

addr := exe.Section(".text").Addr

lineTableData, err := exe.Section(".gopclntab").Data()
if err != nil {
panic(err)
}
lineTable := gosym.NewLineTable(lineTableData, addr)
if err != nil {
panic(err)
}

symTableData, err := exe.Section(".gosymtab").Data()
if err != nil {
panic(err)
}

symTable, err := gosym.NewTable(symTableData, lineTable)
if err != nil {
panic(err)
}

return symTable
}

0 comments on commit 6a741eb

Please sign in to comment.