delve/pkg/proc/dwarf_expr_test.go

263 lines
8.8 KiB
Go
Raw Normal View History

// Tests for loading variables that have complex location expressions. They
// are only produced for optimized code (for both Go and C) therefore we can
// not get the compiler to produce them reliably enough for tests.
package proc_test
import (
"bytes"
"debug/dwarf"
"encoding/binary"
"fmt"
"go/constant"
"testing"
"github.com/go-delve/delve/pkg/dwarf/dwarfbuilder"
"github.com/go-delve/delve/pkg/dwarf/godwarf"
"github.com/go-delve/delve/pkg/dwarf/op"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/linutil"
)
const defaultCFA = 0xc420051d00
func fakeBinaryInfo(t *testing.T, dwb *dwarfbuilder.Builder) *proc.BinaryInfo {
abbrev, aranges, frame, info, line, pubnames, ranges, str, loc, err := dwb.Build()
2018-03-20 10:05:35 +00:00
assertNoError(err, t, "dwarfbuilder.Build")
dwdata, err := dwarf.New(abbrev, aranges, frame, info, line, pubnames, ranges, str)
assertNoError(err, t, "creating dwarf")
bi := proc.NewBinaryInfo("linux", "amd64")
bi.LoadFromData(dwdata, frame, line, loc)
return bi
}
// fakeMemory implements proc.MemoryReadWriter by reading from a byte slice.
// Byte 0 of "data" is at address "base".
type fakeMemory struct {
base uint64
data []byte
}
func newFakeMemory(base uint64, contents ...interface{}) *fakeMemory {
mem := &fakeMemory{base: base}
var buf bytes.Buffer
for _, x := range contents {
binary.Write(&buf, binary.LittleEndian, x)
}
mem.data = buf.Bytes()
return mem
}
func (mem *fakeMemory) ReadMemory(data []byte, addr uintptr) (int, error) {
if uint64(addr) < mem.base {
return 0, fmt.Errorf("read out of bounds %d %#x", len(data), addr)
}
start := uint64(addr) - mem.base
end := uint64(len(data)) + start
if end > uint64(len(mem.data)) {
panic(fmt.Errorf("read out of bounds %d %#x", len(data), addr))
}
copy(data, mem.data[start:end])
return len(data), nil
}
func (mem *fakeMemory) WriteMemory(uintptr, []byte) (int, error) {
return 0, fmt.Errorf("not implemented")
}
func uintExprCheck(t *testing.T, scope *proc.EvalScope, expr string, tgt uint64) {
thevar, err := scope.EvalExpression(expr, normalLoadConfig)
assertNoError(err, t, fmt.Sprintf("EvalExpression(%s)", expr))
if thevar.Unreadable != nil {
t.Errorf("variable %q unreadable: %v", expr, thevar.Unreadable)
} else {
if v, _ := constant.Uint64Val(thevar.Value); v != tgt {
t.Errorf("expected value %x got %x for %q", tgt, v, expr)
}
}
}
proc: support inlining Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
2017-11-13 15:54:08 +00:00
func dwarfExprCheck(t *testing.T, mem proc.MemoryReadWriter, regs op.DwarfRegisters, bi *proc.BinaryInfo, testCases map[string]uint16, fn *proc.Function) *proc.EvalScope {
scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: fn}, Regs: regs, Mem: mem, Gvar: nil, BinInfo: bi}
for name, value := range testCases {
uintExprCheck(t, scope, name, uint64(value))
}
return scope
}
func dwarfRegisters(regs *linutil.AMD64Registers) op.DwarfRegisters {
a := proc.AMD64Arch("linux")
dwarfRegs := a.RegistersToDwarfRegisters(regs, 0)
dwarfRegs.CFA = defaultCFA
dwarfRegs.FrameBase = defaultCFA
return dwarfRegs
}
func TestDwarfExprRegisters(t *testing.T) {
testCases := map[string]uint16{
"a": 0x1234,
"b": 0x4321,
"c": 0x2143,
}
dwb := dwarfbuilder.New()
uint16off := dwb.AddBaseType("uint16", dwarfbuilder.DW_ATE_unsigned, 2)
dwb.AddSubprogram("main.main", 0x40100, 0x41000)
dwb.Attr(dwarf.AttrFrameBase, dwarfbuilder.LocationBlock(op.DW_OP_call_frame_cfa))
dwb.AddVariable("a", uint16off, dwarfbuilder.LocationBlock(op.DW_OP_reg0))
dwb.AddVariable("b", uint16off, dwarfbuilder.LocationBlock(op.DW_OP_fbreg, int(8)))
dwb.AddVariable("c", uint16off, dwarfbuilder.LocationBlock(op.DW_OP_regx, int(1)))
dwb.TagClose()
bi := fakeBinaryInfo(t, dwb)
proc: support inlining Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
2017-11-13 15:54:08 +00:00
mainfn := bi.LookupFunc["main.main"]
mem := newFakeMemory(defaultCFA, uint64(0), uint64(testCases["b"]), uint16(testCases["pair.v"]))
regs := linutil.AMD64Registers{Regs: &linutil.AMD64PtraceRegs{}}
regs.Regs.Rax = uint64(testCases["a"])
regs.Regs.Rdx = uint64(testCases["c"])
proc: support inlining Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
2017-11-13 15:54:08 +00:00
dwarfExprCheck(t, mem, dwarfRegisters(&regs), bi, testCases, mainfn)
}
func TestDwarfExprComposite(t *testing.T) {
testCases := map[string]uint16{
"pair.k": 0x8765,
"pair.v": 0x5678,
"n": 42,
}
const stringVal = "this is a string"
dwb := dwarfbuilder.New()
uint16off := dwb.AddBaseType("uint16", dwarfbuilder.DW_ATE_unsigned, 2)
intoff := dwb.AddBaseType("int", dwarfbuilder.DW_ATE_signed, 8)
byteoff := dwb.AddBaseType("uint8", dwarfbuilder.DW_ATE_unsigned, 1)
byteptroff := dwb.AddPointerType("*uint8", byteoff)
pairoff := dwb.AddStructType("main.pair", 4)
dwb.Attr(godwarf.AttrGoKind, uint8(25))
dwb.AddMember("k", uint16off, dwarfbuilder.LocationBlock(op.DW_OP_plus_uconst, uint(0)))
dwb.AddMember("v", uint16off, dwarfbuilder.LocationBlock(op.DW_OP_plus_uconst, uint(2)))
dwb.TagClose()
stringoff := dwb.AddStructType("string", 16)
dwb.Attr(godwarf.AttrGoKind, uint8(24))
dwb.AddMember("str", byteptroff, dwarfbuilder.LocationBlock(op.DW_OP_plus_uconst, uint(0)))
dwb.AddMember("len", intoff, dwarfbuilder.LocationBlock(op.DW_OP_plus_uconst, uint(8)))
dwb.TagClose()
dwb.AddSubprogram("main.main", 0x40100, 0x41000)
dwb.AddVariable("pair", pairoff, dwarfbuilder.LocationBlock(
op.DW_OP_reg2, op.DW_OP_piece, uint(2),
op.DW_OP_call_frame_cfa, op.DW_OP_consts, int(16), op.DW_OP_plus, op.DW_OP_piece, uint(2)))
dwb.AddVariable("s", stringoff, dwarfbuilder.LocationBlock(
op.DW_OP_reg1, op.DW_OP_piece, uint(8),
op.DW_OP_reg0, op.DW_OP_piece, uint(8)))
dwb.AddVariable("n", intoff, dwarfbuilder.LocationBlock(op.DW_OP_reg3))
dwb.TagClose()
bi := fakeBinaryInfo(t, dwb)
proc: support inlining Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
2017-11-13 15:54:08 +00:00
mainfn := bi.LookupFunc["main.main"]
mem := newFakeMemory(defaultCFA, uint64(0), uint64(0), uint16(testCases["pair.v"]), []byte(stringVal))
var regs linutil.AMD64Registers
regs.Regs = &linutil.AMD64PtraceRegs{}
regs.Regs.Rax = uint64(len(stringVal))
regs.Regs.Rdx = defaultCFA + 18
regs.Regs.Rcx = uint64(testCases["pair.k"])
regs.Regs.Rbx = uint64(testCases["n"])
proc: support inlining Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
2017-11-13 15:54:08 +00:00
scope := dwarfExprCheck(t, mem, dwarfRegisters(&regs), bi, testCases, mainfn)
thevar, err := scope.EvalExpression("s", normalLoadConfig)
assertNoError(err, t, fmt.Sprintf("EvalExpression(%s)", "s"))
if thevar.Unreadable != nil {
t.Errorf("variable \"s\" unreadable: %v", thevar.Unreadable)
} else {
if v := constant.StringVal(thevar.Value); v != stringVal {
t.Errorf("expected value %q got %q", stringVal, v)
}
}
}
func TestDwarfExprLoclist(t *testing.T) {
const before = 0x1234
const after = 0x4321
dwb := dwarfbuilder.New()
uint16off := dwb.AddBaseType("uint16", dwarfbuilder.DW_ATE_unsigned, 2)
dwb.AddSubprogram("main.main", 0x40100, 0x41000)
dwb.AddVariable("a", uint16off, []dwarfbuilder.LocEntry{
{0x40100, 0x40700, dwarfbuilder.LocationBlock(op.DW_OP_call_frame_cfa)},
{0x40700, 0x41000, dwarfbuilder.LocationBlock(op.DW_OP_call_frame_cfa, op.DW_OP_consts, int(2), op.DW_OP_plus)},
})
dwb.TagClose()
bi := fakeBinaryInfo(t, dwb)
proc: support inlining Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
2017-11-13 15:54:08 +00:00
mainfn := bi.LookupFunc["main.main"]
mem := newFakeMemory(defaultCFA, uint16(before), uint16(after))
regs := linutil.AMD64Registers{Regs: &linutil.AMD64PtraceRegs{}}
proc: support inlining Go 1.10 added inlined calls to debug_info, this commit adds support for DW_TAG_inlined_call to delve, both for stack traces (where inlined calls will appear as normal stack frames) and to correct the behavior of next, step and stepout. The calls to Next and Frame of stackIterator continue to work unchanged and only return real stack frames, after reading each line appendInlinedCalls is called to unpacked all the inlined calls that involve the current PC. The fake stack frames produced by appendInlinedCalls are distinguished from real stack frames by having the Inlined attribute set to true. Also their Current and Call locations are treated differently. The Call location will be changed to represent the position inside the inlined call, while the Current location will always reference the real stack frame. This is done because: * next, step and stepout need to access the debug_info entry of the real function they are stepping through * we are already manipulating Call in different ways while Current is just what we read from the call stack The strategy remains mostly the same, we disassemble the function and we set a breakpoint on each instruction corresponding to a different file:line. The function in question will be the one corresponding to the first real (i.e. non-inlined) stack frame. * If the current function contains inlined calls, 'next' will not set any breakpoints on instructions that belong to inlined calls. We do not do this for 'step'. * If we are inside an inlined call that makes other inlined functions, 'next' will not set any breakpoints that belong to inlined calls that are children of the current inlined call. * If the current function is inlined the breakpoint on the return address won't be set, because inlined frames don't have a return address. * The code we use for stepout doesn't work at all if we are inside an inlined call, instead we call 'next' but instruct it to remove all PCs belonging to the current inlined call.
2017-11-13 15:54:08 +00:00
scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: mainfn}, Regs: dwarfRegisters(&regs), Mem: mem, Gvar: nil, BinInfo: bi}
uintExprCheck(t, scope, "a", before)
scope.PC = 0x40800
uintExprCheck(t, scope, "a", after)
}
func TestIssue1419(t *testing.T) {
// trying to read a slice variable with a location list that tries to read
// from registers we don't have should not cause a panic.
dwb := dwarfbuilder.New()
uint64off := dwb.AddBaseType("uint64", dwarfbuilder.DW_ATE_unsigned, 8)
intoff := dwb.AddBaseType("int", dwarfbuilder.DW_ATE_signed, 8)
intptroff := dwb.AddPointerType("*int", intoff)
sliceoff := dwb.AddStructType("[]int", 24)
dwb.Attr(godwarf.AttrGoKind, uint8(23))
dwb.AddMember("array", intptroff, dwarfbuilder.LocationBlock(op.DW_OP_plus_uconst, uint(0)))
dwb.AddMember("len", uint64off, dwarfbuilder.LocationBlock(op.DW_OP_plus_uconst, uint(8)))
dwb.AddMember("cap", uint64off, dwarfbuilder.LocationBlock(op.DW_OP_plus_uconst, uint(16)))
dwb.TagClose()
dwb.AddSubprogram("main.main", 0x40100, 0x41000)
dwb.AddVariable("a", sliceoff, dwarfbuilder.LocationBlock(op.DW_OP_reg2, op.DW_OP_piece, uint(8), op.DW_OP_reg2, op.DW_OP_piece, uint(8), op.DW_OP_reg2, op.DW_OP_piece, uint(8)))
dwb.TagClose()
bi := fakeBinaryInfo(t, dwb)
mainfn := bi.LookupFunc["main.main"]
mem := newFakeMemory(defaultCFA)
scope := &proc.EvalScope{Location: proc.Location{PC: 0x40100, Fn: mainfn}, Regs: op.DwarfRegisters{}, Mem: mem, Gvar: nil, BinInfo: bi}
va, err := scope.EvalExpression("a", normalLoadConfig)
assertNoError(err, t, "EvalExpression(a)")
t.Logf("%#x\n", va.Addr)
t.Logf("%v", va)
if va.Unreadable == nil {
t.Fatalf("expected 'a' to be unreadable but it wasn't")
}
if va.Unreadable.Error() != "could not read 8 bytes from register 2 (size: 0)" {
t.Fatalf("wrong unreadable reason for variable 'a': %v", va.Unreadable)
}
}