
Go1.11 uses the is_stmt flag of .debug_line to communicate which assembly instructions are good places for breakpoints, we should respect this flag. These changes were introduced by: * https://go-review.googlesource.com/c/go/+/102435/ Additionally when setting next breakpoints ignore all PC addresses that belong to the same line as the one currently under at the cursor. This matches the behavior of gdb and avoids stopping multiple times at the heading line of a for statement with go1.11. Change: https://go-review.googlesource.com/c/go/+/110416 adds the prologue_end flag to the .debug_line section to communicate the end of the stack-split prologue. We should use it instead of pattern matching the disassembly when available. Fixes #550 type of interfaces 'c7cde8b'.
184 lines
4.6 KiB
Go
184 lines
4.6 KiB
Go
package proc
|
|
|
|
import (
|
|
"encoding/binary"
|
|
|
|
"golang.org/x/arch/x86/x86asm"
|
|
)
|
|
|
|
var maxInstructionLength uint64 = 15
|
|
|
|
type ArchInst x86asm.Inst
|
|
|
|
func asmDecode(mem []byte, pc uint64) (*ArchInst, error) {
|
|
inst, err := x86asm.Decode(mem, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
patchPCRel(pc, &inst)
|
|
r := ArchInst(inst)
|
|
return &r, nil
|
|
}
|
|
|
|
func (inst *ArchInst) Size() int {
|
|
return inst.Len
|
|
}
|
|
|
|
// converts PC relative arguments to absolute addresses
|
|
func patchPCRel(pc uint64, inst *x86asm.Inst) {
|
|
for i := range inst.Args {
|
|
rel, isrel := inst.Args[i].(x86asm.Rel)
|
|
if isrel {
|
|
inst.Args[i] = x86asm.Imm(int64(pc) + int64(rel) + int64(inst.Len))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (inst *AsmInstruction) Text(flavour AssemblyFlavour, bi *BinaryInfo) string {
|
|
if inst.Inst == nil {
|
|
return "?"
|
|
}
|
|
|
|
var text string
|
|
|
|
switch flavour {
|
|
case GNUFlavour:
|
|
text = x86asm.GNUSyntax(x86asm.Inst(*inst.Inst), inst.Loc.PC, bi.symLookup)
|
|
case GoFlavour:
|
|
text = x86asm.GoSyntax(x86asm.Inst(*inst.Inst), inst.Loc.PC, bi.symLookup)
|
|
case IntelFlavour:
|
|
fallthrough
|
|
default:
|
|
text = x86asm.IntelSyntax(x86asm.Inst(*inst.Inst), inst.Loc.PC, bi.symLookup)
|
|
}
|
|
|
|
return text
|
|
}
|
|
|
|
func (inst *AsmInstruction) IsCall() bool {
|
|
return inst.Inst.Op == x86asm.CALL || inst.Inst.Op == x86asm.LCALL
|
|
}
|
|
|
|
func resolveCallArg(inst *ArchInst, currentGoroutine bool, regs Registers, mem MemoryReadWriter, bininfo *BinaryInfo) *Location {
|
|
if inst.Op != x86asm.CALL && inst.Op != x86asm.LCALL {
|
|
return nil
|
|
}
|
|
|
|
var pc uint64
|
|
var err error
|
|
|
|
switch arg := inst.Args[0].(type) {
|
|
case x86asm.Imm:
|
|
pc = uint64(arg)
|
|
case x86asm.Reg:
|
|
if !currentGoroutine || regs == nil {
|
|
return nil
|
|
}
|
|
pc, err = regs.Get(int(arg))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
case x86asm.Mem:
|
|
if !currentGoroutine || regs == nil {
|
|
return nil
|
|
}
|
|
if arg.Segment != 0 {
|
|
return nil
|
|
}
|
|
base, err1 := regs.Get(int(arg.Base))
|
|
index, err2 := regs.Get(int(arg.Index))
|
|
if err1 != nil || err2 != nil {
|
|
return nil
|
|
}
|
|
addr := uintptr(int64(base) + int64(index*uint64(arg.Scale)) + arg.Disp)
|
|
//TODO: should this always be 64 bits instead of inst.MemBytes?
|
|
pcbytes := make([]byte, inst.MemBytes)
|
|
_, err := mem.ReadMemory(pcbytes, addr)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
pc = binary.LittleEndian.Uint64(pcbytes)
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
file, line, fn := bininfo.PCToLine(pc)
|
|
if fn == nil {
|
|
return nil
|
|
}
|
|
return &Location{PC: pc, File: file, Line: line, Fn: fn}
|
|
}
|
|
|
|
type instrseq []x86asm.Op
|
|
|
|
// Possible stacksplit prologues are inserted by stacksplit in
|
|
// $GOROOT/src/cmd/internal/obj/x86/obj6.go.
|
|
// The stacksplit prologue will always begin with loading curg in CX, this
|
|
// instruction is added by load_g_cx in the same file and is either 1 or 2
|
|
// MOVs.
|
|
var prologues []instrseq
|
|
|
|
func init() {
|
|
var tinyStacksplit = instrseq{x86asm.CMP, x86asm.JBE}
|
|
var smallStacksplit = instrseq{x86asm.LEA, x86asm.CMP, x86asm.JBE}
|
|
var bigStacksplit = instrseq{x86asm.MOV, x86asm.CMP, x86asm.JE, x86asm.LEA, x86asm.SUB, x86asm.CMP, x86asm.JBE}
|
|
var unixGetG = instrseq{x86asm.MOV}
|
|
var windowsGetG = instrseq{x86asm.MOV, x86asm.MOV}
|
|
|
|
prologues = make([]instrseq, 0, 2*3)
|
|
for _, getG := range []instrseq{unixGetG, windowsGetG} {
|
|
for _, stacksplit := range []instrseq{tinyStacksplit, smallStacksplit, bigStacksplit} {
|
|
prologue := make(instrseq, 0, len(getG)+len(stacksplit))
|
|
prologue = append(prologue, getG...)
|
|
prologue = append(prologue, stacksplit...)
|
|
prologues = append(prologues, prologue)
|
|
}
|
|
}
|
|
}
|
|
|
|
// firstPCAfterPrologueDisassembly returns the address of the first
|
|
// instruction after the prologue for function fn by disassembling fn and
|
|
// matching the instructions against known split-stack prologue patterns.
|
|
// If sameline is set firstPCAfterPrologueDisassembly will always return an
|
|
// address associated with the same line as fn.Entry
|
|
func firstPCAfterPrologueDisassembly(p Process, fn *Function, sameline bool) (uint64, error) {
|
|
var mem MemoryReadWriter = p.CurrentThread()
|
|
breakpoints := p.Breakpoints()
|
|
bi := p.BinInfo()
|
|
text, err := disassemble(mem, nil, breakpoints, bi, fn.Entry, fn.End, false)
|
|
if err != nil {
|
|
return fn.Entry, err
|
|
}
|
|
|
|
if len(text) <= 0 {
|
|
return fn.Entry, nil
|
|
}
|
|
|
|
for _, prologue := range prologues {
|
|
if len(prologue) >= len(text) {
|
|
continue
|
|
}
|
|
if checkPrologue(text, prologue) {
|
|
r := &text[len(prologue)]
|
|
if sameline {
|
|
if r.Loc.Line != text[0].Loc.Line {
|
|
return fn.Entry, nil
|
|
}
|
|
}
|
|
return r.Loc.PC, nil
|
|
}
|
|
}
|
|
|
|
return fn.Entry, nil
|
|
}
|
|
|
|
func checkPrologue(s []AsmInstruction, prologuePattern instrseq) bool {
|
|
line := s[0].Loc.Line
|
|
for i, op := range prologuePattern {
|
|
if s[i].Inst.Op != op || s[i].Loc.Line != line {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|