delve/pkg/proc/core/core_test.go
Alessandro Arzilli 200994bc8f
proc/*: only load floating point registers when needed (#1981)
Changes implementations of proc.Registers interface and the
op.DwarfRegisters struct so that floating point registers can be loaded
only when they are needed.
Removes the floatingPoint parameter from proc.Thread.Registers.
This accomplishes three things:

1. it simplifies the proc.Thread.Registers interface
2. it makes it impossible to accidentally create a broken set of saved
   registers or of op.DwarfRegisters by accidentally calling
   Registers(false)
3. it improves general performance of Delve by avoiding to load
   floating point registers as much as possible

Floating point registers are loaded under two circumstances:

1. When the Slice method is called with floatingPoint == true
2. When the Copy method is called

Benchmark before:

BenchmarkConditionalBreakpoints-4   	       1	4327350142 ns/op

Benchmark after:

BenchmarkConditionalBreakpoints-4   	       1	3852642917 ns/op

Updates #1549
2020-05-13 11:56:50 -07:00

470 lines
12 KiB
Go

package core
import (
"bytes"
"flag"
"fmt"
"go/constant"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/test"
)
var buildMode string
func TestMain(m *testing.M) {
flag.StringVar(&buildMode, "test-buildmode", "", "selects build mode")
flag.Parse()
if buildMode != "" && buildMode != "pie" {
fmt.Fprintf(os.Stderr, "unknown build mode %q", buildMode)
os.Exit(1)
}
os.Exit(test.RunTestsWithFixtures(m))
}
func assertNoError(err error, t testing.TB, s string) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
fname := filepath.Base(file)
t.Fatalf("failed assertion at %s:%d: %s - %s\n", fname, line, s, err)
}
}
func TestSplicedReader(t *testing.T) {
data := []byte{}
data2 := []byte{}
for i := 0; i < 100; i++ {
data = append(data, byte(i))
data2 = append(data2, byte(i+100))
}
type region struct {
data []byte
off uintptr
length uintptr
}
tests := []struct {
name string
regions []region
readAddr uintptr
readLen int
want []byte
}{
{
"Insert after",
[]region{
{data, 0, 1},
{data2, 1, 1},
},
0,
2,
[]byte{0, 101},
},
{
"Insert before",
[]region{
{data, 1, 1},
{data2, 0, 1},
},
0,
2,
[]byte{100, 1},
},
{
"Completely overwrite",
[]region{
{data, 1, 1},
{data2, 0, 3},
},
0,
3,
[]byte{100, 101, 102},
},
{
"Overwrite end",
[]region{
{data, 0, 2},
{data2, 1, 2},
},
0,
3,
[]byte{0, 101, 102},
},
{
"Overwrite start",
[]region{
{data, 0, 3},
{data2, 0, 2},
},
0,
3,
[]byte{100, 101, 2},
},
{
"Punch hole",
[]region{
{data, 0, 5},
{data2, 1, 3},
},
0,
5,
[]byte{0, 101, 102, 103, 4},
},
{
"Overlap two",
[]region{
{data, 10, 4},
{data, 14, 4},
{data2, 12, 4},
},
10,
8,
[]byte{10, 11, 112, 113, 114, 115, 16, 17},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mem := &splicedMemory{}
for _, region := range test.regions {
r := bytes.NewReader(region.data)
mem.Add(&offsetReaderAt{r, 0}, region.off, region.length)
}
got := make([]byte, test.readLen)
n, err := mem.ReadMemory(got, test.readAddr)
if n != test.readLen || err != nil || !reflect.DeepEqual(got, test.want) {
t.Errorf("ReadAt = %v, %v, %v, want %v, %v, %v", n, err, got, test.readLen, nil, test.want)
}
})
}
}
func withCoreFile(t *testing.T, name, args string) *proc.Target {
// This is all very fragile and won't work on hosts with non-default core patterns.
// Might be better to check in the binary and core?
tempDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
test.PathsToRemove = append(test.PathsToRemove, tempDir)
var buildFlags test.BuildFlags
if buildMode == "pie" {
buildFlags = test.BuildModePIE
}
fix := test.BuildFixture(name, buildFlags)
bashCmd := fmt.Sprintf("cd %v && ulimit -c unlimited && GOTRACEBACK=crash %v %s", tempDir, fix.Path, args)
exec.Command("bash", "-c", bashCmd).Run()
cores, err := filepath.Glob(path.Join(tempDir, "core*"))
switch {
case err != nil || len(cores) > 1:
t.Fatalf("Got %v, wanted one file named core* in %v", cores, tempDir)
case len(cores) == 0:
t.Skipf("core file was not produced, could not run test")
return nil
}
corePath := cores[0]
p, err := OpenCore(corePath, fix.Path, []string{})
if err != nil {
t.Errorf("OpenCore(%q) failed: %v", corePath, err)
pat, err := ioutil.ReadFile("/proc/sys/kernel/core_pattern")
t.Errorf("read core_pattern: %q, %v", pat, err)
apport, err := ioutil.ReadFile("/var/log/apport.log")
t.Errorf("read apport log: %q, %v", apport, err)
t.Fatalf("previous errors")
}
return p
}
func logRegisters(t *testing.T, regs proc.Registers, arch *proc.Arch) {
dregs := arch.RegistersToDwarfRegisters(0, regs)
dregs.Reg(^uint64(0))
for i := 0; i < dregs.CurrentSize(); i++ {
reg := dregs.Reg(uint64(i))
if reg == nil {
continue
}
name, _, value := arch.DwarfRegisterToString(i, reg)
t.Logf("%s = %s", name, value)
}
}
func TestCore(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return
}
p := withCoreFile(t, "panic", "")
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)
}
var panicking *proc.G
var panickingStack []proc.Stackframe
for _, g := range gs {
t.Logf("Goroutine %d", g.ID)
stack, err := g.Stacktrace(10, 0)
if err != nil {
t.Errorf("Stacktrace() on goroutine %v = %v", g, err)
}
for _, frame := range stack {
fnname := ""
if frame.Call.Fn != nil {
fnname = frame.Call.Fn.Name
}
t.Logf("\tframe %s:%d in %s %#x (systemstack: %v)", frame.Call.File, frame.Call.Line, fnname, frame.Call.PC, frame.SystemStack)
if frame.Current.Fn != nil && strings.Contains(frame.Current.Fn.Name, "panic") {
panicking = g
panickingStack = stack
}
}
}
if panicking == nil {
t.Fatalf("Didn't find a call to panic in goroutine stacks: %v", gs)
}
var mainFrame *proc.Stackframe
// Walk backward, because the current function seems to be main.main
// in the actual call to panic().
for i := len(panickingStack) - 1; i >= 0; i-- {
if panickingStack[i].Current.Fn != nil && panickingStack[i].Current.Fn.Name == "main.main" {
mainFrame = &panickingStack[i]
}
}
if mainFrame == nil {
t.Fatalf("Couldn't find main in stack %v", panickingStack)
}
msg, err := proc.FrameToScope(p.BinInfo(), p.CurrentThread(), nil, *mainFrame).EvalVariable("msg", proc.LoadConfig{MaxStringLen: 64})
if err != nil {
t.Fatalf("Couldn't EvalVariable(msg, ...): %v", err)
}
if constant.StringVal(msg.Value) != "BOOM!" {
t.Errorf("main.msg = %q, want %q", msg.Value, "BOOM!")
}
regs, err := p.CurrentThread().Registers()
if err != nil {
t.Fatalf("Couldn't get current thread registers: %v", err)
}
logRegisters(t, regs, p.BinInfo().Arch)
}
func TestCoreFpRegisters(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return
}
// in go1.10 the crash is executed on a different thread and registers are
// no longer available in the core dump.
if ver, _ := goversion.Parse(runtime.Version()); ver.Major < 0 || ver.AfterOrEqual(goversion.GoVersion{Major: 1, Minor: 10, Rev: -1}) {
t.Skip("not supported in go1.10 and later")
}
p := withCoreFile(t, "fputest/", "panic")
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)
}
var regs proc.Registers
for _, thread := range p.ThreadList() {
frames, err := proc.ThreadStacktrace(thread, 10)
if err != nil {
t.Errorf("ThreadStacktrace for %x = %v", thread.ThreadID(), err)
continue
}
for i := range frames {
if frames[i].Current.Fn == nil {
continue
}
if frames[i].Current.Fn.Name == "main.main" {
regs, err = thread.Registers()
if err != nil {
t.Fatalf("Could not get registers for thread %x, %v", thread.ThreadID(), err)
}
break
}
}
if regs != nil {
break
}
}
regtests := []struct{ name, value string }{
{"ST(0)", "0x3fffe666660000000000"},
{"ST(1)", "0x3fffd9999a0000000000"},
{"ST(2)", "0x3fffcccccd0000000000"},
{"ST(3)", "0x3fffc000000000000000"},
{"ST(4)", "0x3fffb333333333333000"},
{"ST(5)", "0x3fffa666666666666800"},
{"ST(6)", "0x3fff9999999999999800"},
{"ST(7)", "0x3fff8cccccccccccd000"},
// Unlike TestClientServer_FpRegisters in service/test/integration2_test
// we can not test the value of XMM0, it probably has been reused by
// something between the panic and the time we get the core dump.
{"XMM9", "0x3ff66666666666663ff4cccccccccccd"},
{"XMM10", "0x3fe666663fd9999a3fcccccd3fc00000"},
{"XMM3", "0x3ff199999999999a3ff3333333333333"},
{"XMM4", "0x3ff4cccccccccccd3ff6666666666666"},
{"XMM5", "0x3fcccccd3fc000003fe666663fd9999a"},
{"XMM6", "0x4004cccccccccccc4003333333333334"},
{"XMM7", "0x40026666666666664002666666666666"},
{"XMM8", "0x4059999a404ccccd4059999a404ccccd"},
}
arch := p.BinInfo().Arch
logRegisters(t, regs, arch)
dregs := arch.RegistersToDwarfRegisters(0, regs)
for _, regtest := range regtests {
found := false
dregs.Reg(^uint64(0))
for i := 0; i < dregs.CurrentSize(); i++ {
reg := dregs.Reg(uint64(i))
regname, _, regval := arch.DwarfRegisterToString(i, reg)
if reg != nil && regname == regtest.name {
found = true
if !strings.HasPrefix(regval, regtest.value) {
t.Fatalf("register %s expected %q got %q", regname, regtest.value, regval)
}
}
}
if !found {
t.Fatalf("register %s not found: %v", regtest.name, regs)
}
}
}
func TestCoreWithEmptyString(t *testing.T) {
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
return
}
p := withCoreFile(t, "coreemptystring", "")
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
assertNoError(err, t, "GoroutinesInfo")
var mainFrame *proc.Stackframe
mainSearch:
for _, g := range gs {
stack, err := g.Stacktrace(10, 0)
assertNoError(err, t, "Stacktrace()")
for _, frame := range stack {
if frame.Current.Fn != nil && frame.Current.Fn.Name == "main.main" {
mainFrame = &frame
break mainSearch
}
}
}
if mainFrame == nil {
t.Fatal("could not find main.main frame")
}
scope := proc.FrameToScope(p.BinInfo(), p.CurrentThread(), nil, *mainFrame)
loadConfig := proc.LoadConfig{FollowPointers: true, MaxVariableRecurse: 1, MaxStringLen: 64, MaxArrayValues: 64, MaxStructFields: -1}
v1, err := scope.EvalVariable("t", loadConfig)
assertNoError(err, t, "EvalVariable(t)")
assertNoError(v1.Unreadable, t, "unreadable variable 't'")
t.Logf("t = %#v\n", v1)
v2, err := scope.EvalVariable("s", loadConfig)
assertNoError(err, t, "EvalVariable(s)")
assertNoError(v2.Unreadable, t, "unreadable variable 's'")
t.Logf("s = %#v\n", v2)
}
func TestMinidump(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("minidumps can only be produced on windows")
}
var buildFlags test.BuildFlags
if buildMode == "pie" {
buildFlags = test.BuildModePIE
}
fix := test.BuildFixture("sleep", buildFlags)
mdmpPath := procdump(t, fix.Path)
p, err := OpenCore(mdmpPath, fix.Path, []string{})
if err != nil {
t.Fatalf("OpenCore: %v", err)
}
gs, _, err := proc.GoroutinesInfo(p, 0, 0)
if err != nil || len(gs) == 0 {
t.Fatalf("GoroutinesInfo() = %v, %v; wanted at least one goroutine", gs, err)
}
t.Logf("%d goroutines", len(gs))
foundMain, foundTime := false, false
for _, g := range gs {
stack, err := g.Stacktrace(10, 0)
if err != nil {
t.Errorf("Stacktrace() on goroutine %v = %v", g, err)
}
t.Logf("goroutine %d", g.ID)
for _, frame := range stack {
name := "?"
if frame.Current.Fn != nil {
name = frame.Current.Fn.Name
}
t.Logf("\t%s:%d in %s %#x", frame.Current.File, frame.Current.Line, name, frame.Current.PC)
if frame.Current.Fn == nil {
continue
}
switch frame.Current.Fn.Name {
case "main.main":
foundMain = true
case "time.Sleep":
foundTime = true
}
}
if foundMain != foundTime {
t.Errorf("found main.main but no time.Sleep (or viceversa) %v %v", foundMain, foundTime)
}
}
if !foundMain {
t.Fatalf("could not find main goroutine")
}
}
func procdump(t *testing.T, exePath string) string {
exeDir := filepath.Dir(exePath)
cmd := exec.Command("procdump64", "-accepteula", "-ma", "-n", "1", "-s", "3", "-x", exeDir, exePath, "quit")
out, err := cmd.CombinedOutput() // procdump exits with non-zero status on success, so we have to ignore the error here
if !strings.Contains(string(out), "Dump count reached.") {
t.Fatalf("possible error running procdump64, output: %q, error: %v", string(out), err)
}
dh, err := os.Open(exeDir)
if err != nil {
t.Fatalf("could not open executable file directory %q: %v", exeDir, err)
}
defer dh.Close()
fis, err := dh.Readdir(-1)
if err != nil {
t.Fatalf("could not read executable file directory %q: %v", exeDir, err)
}
t.Logf("looking for dump file")
exeName := filepath.Base(exePath)
for _, fi := range fis {
name := fi.Name()
t.Logf("\t%s", name)
if strings.HasPrefix(name, exeName) && strings.HasSuffix(name, ".dmp") {
mdmpPath := filepath.Join(exeDir, name)
test.PathsToRemove = append(test.PathsToRemove, mdmpPath)
return mdmpPath
}
}
t.Fatalf("could not find dump file")
return ""
}