x/debug/core: add initial implementation

Add library for reading core dumps.

Update golang/go#21356

Change-Id: Ia7fa09d9e78a7c30fbdec184a5cf6f73b0994c4a
Reviewed-on: https://go-review.googlesource.com/72830
Reviewed-by: Peter Weinberger <pjw@google.com>
diff --git a/core/address.go b/core/address.go
new file mode 100644
index 0000000..414cd61
--- /dev/null
+++ b/core/address.go
@@ -0,0 +1,40 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package core
+
+// An Address is a location in the inferior's address space.
+type Address uint64
+
+// Sub subtracts b from a. Requires a >= b.
+func (a Address) Sub(b Address) int64 {
+	return int64(a - b)
+}
+
+// Add adds x to address a.
+func (a Address) Add(x int64) Address {
+	return a + Address(x)
+}
+
+// Max returns the larger of a and b.
+func (a Address) Max(b Address) Address {
+	if a > b {
+		return a
+	}
+	return b
+}
+
+// Min returns the smaller of a and b.
+func (a Address) Min(b Address) Address {
+	if a < b {
+		return a
+	}
+	return b
+}
+
+// Align rounds a up to a multiple of x.
+// x must be a power of 2.
+func (a Address) Align(x int64) Address {
+	return (a + Address(x) - 1) & ^(Address(x) - 1)
+}
diff --git a/core/core_test.go b/core/core_test.go
new file mode 100644
index 0000000..23a56ec
--- /dev/null
+++ b/core/core_test.go
@@ -0,0 +1,102 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package core
+
+import (
+	"testing"
+)
+
+// loadTest loads a simple core file which resulted from running the
+// following program on linux/amd64 with go 1.9.0 (the earliest supported runtime):
+// package main
+// func main() {
+//         _ = *(*int)(nil)
+// }
+func loadExample(t *testing.T) *Process {
+	p, err := Core("testdata/core", "testdata")
+	if err != nil {
+		t.Fatalf("can't load test core file: %s", err)
+	}
+	return p
+}
+
+// TestMappings makes sure we can find and load some data.
+func TestMappings(t *testing.T) {
+	p := loadExample(t)
+	s, err := p.Symbols()
+	if err != nil {
+		t.Errorf("can't read symbols: %s\n", err)
+	}
+
+	a := s["main.main"]
+	m := p.findMapping(a)
+	if m == nil {
+		t.Errorf("text mapping missing")
+	}
+	if m.Perm() != Read|Exec {
+		t.Errorf("bad code section permissions")
+	}
+	if opcode := p.ReadUint8(a); opcode != 0x31 {
+		// 0x31 = xorl instruction.
+		// There's no particular reason why this instruction
+		// is first. This just tests that reading code works
+		// for our specific test binary.
+		t.Errorf("opcode=0x%x, want 0x31", opcode)
+	}
+
+	a = s["runtime.class_to_size"]
+	m = p.findMapping(a)
+	if m == nil {
+		t.Errorf("data mapping missing")
+	}
+	if m.Perm() != Read|Write {
+		t.Errorf("bad data section permissions")
+	}
+	if size := p.ReadUint16(a.Add(2)); size != 8 {
+		t.Errorf("class_to_size[1]=%d, want 8", size)
+	}
+}
+
+// TestConfig checks the configuration accessors.
+func TestConfig(t *testing.T) {
+	p := loadExample(t)
+	if arch := p.Arch(); arch != "amd64" {
+		t.Errorf("arch=%s, want amd64", arch)
+	}
+	if size := p.PtrSize(); size != 8 {
+		t.Errorf("ptrSize=%d, want 8", size)
+	}
+	if log := p.LogPtrSize(); log != 3 {
+		t.Errorf("logPtrSize=%d, want 3", log)
+	}
+	if bo := p.ByteOrder(); bo.String() != "LittleEndian" {
+		t.Errorf("got %s, want LittleEndian", bo)
+	}
+}
+
+// TestThread makes sure we get information about running threads.
+func TestThread(t *testing.T) {
+	p := loadExample(t)
+	syms, err := p.Symbols()
+	if err != nil {
+		t.Errorf("can't read symbols: %s\n", err)
+	}
+	raise := syms["runtime.raise"]
+	var size int64 = 1 << 30
+	for _, a := range syms {
+		if a > raise && a.Sub(raise) < size {
+			size = a.Sub(raise)
+		}
+	}
+	found := false
+	for _, thr := range p.Threads() {
+		if thr.PC() >= raise && thr.PC() < raise.Add(size) {
+			found = true
+		}
+	}
+	if !found {
+		t.Errorf("can't find thread that did runtime.raise")
+	}
+}
diff --git a/core/mapping.go b/core/mapping.go
new file mode 100644
index 0000000..a678dc8
--- /dev/null
+++ b/core/mapping.go
@@ -0,0 +1,168 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package core
+
+import (
+	"fmt"
+	"os"
+	"strings"
+)
+
+// A Mapping represents a contiguous subset of the inferior's address space.
+type Mapping struct {
+	min  Address
+	max  Address
+	perm Perm
+
+	f   *os.File // file backing this region
+	off int64    // offset of start of this mapping in f
+
+	// For regions originally backed by a file but now in the core file,
+	// (probably because it is copy-on-write) this is the original data source.
+	// This info is just for printing; the data in this source is stale.
+	origF   *os.File
+	origOff int64
+
+	// Contents of f at offset off. Length=max-min.
+	contents []byte
+}
+
+// Min returns the lowest virtual address of the mapping.
+func (m *Mapping) Min() Address {
+	return m.min
+}
+
+// Max returns the virtual address of the byte just beyond the mapping.
+func (m *Mapping) Max() Address {
+	return m.max
+}
+
+// Size returns int64(Max-Min)
+func (m *Mapping) Size() int64 {
+	return m.max.Sub(m.min)
+}
+
+// Perm returns the permissions on the mapping.
+func (m *Mapping) Perm() Perm {
+	return m.perm
+}
+
+// Source returns the backing file and offset for the mapping, or "", 0 if none.
+func (m *Mapping) Source() (string, int64) {
+	if m.f == nil {
+		return "", 0
+	}
+	return m.f.Name(), m.off
+}
+
+// CopyOnWrite reports whether the mapping is a copy-on-write region, i.e.
+// it started as a mapped file and is now writeable.
+// TODO: is this distinguishable from a write-back region?
+func (m *Mapping) CopyOnWrite() bool {
+	return m.origF != nil
+}
+
+// For CopyOnWrite mappings, OrigSource returns the file/offset of the
+// original copy of the data, or "", 0 if none.
+func (m *Mapping) OrigSource() (string, int64) {
+	if m.origF == nil {
+		return "", 0
+	}
+	return m.origF.Name(), m.origOff
+}
+
+// A Perm represents the permissions allowed for a Mapping.
+type Perm uint8
+
+const (
+	Read Perm = 1 << iota
+	Write
+	Exec
+)
+
+func (p Perm) String() string {
+	var a [3]string
+	b := a[:0]
+	if p&Read != 0 {
+		b = append(b, "Read")
+	}
+	if p&Write != 0 {
+		b = append(b, "Write")
+	}
+	if p&Exec != 0 {
+		b = append(b, "Exec")
+	}
+	if len(b) == 0 {
+		b = append(b, "None")
+	}
+	return strings.Join(b, "|")
+}
+
+// We assume that OS pages are at least 4K in size. So every mapping
+// starts and ends at a multiple of 4K.
+// We divide the other 64-12 = 52 bits into levels in a page table.
+type pageTable0 [1 << 10]*Mapping
+type pageTable1 [1 << 10]*pageTable0
+type pageTable2 [1 << 10]*pageTable1
+type pageTable3 [1 << 10]*pageTable2
+type pageTable4 [1 << 12]*pageTable3
+
+// findMapping is simple enough that it inlines.
+func (p *Process) findMapping(a Address) *Mapping {
+	t3 := p.pageTable[a>>52]
+	if t3 == nil {
+		return nil
+	}
+	t2 := t3[a>>42%(1<<10)]
+	if t2 == nil {
+		return nil
+	}
+	t1 := t2[a>>32%(1<<10)]
+	if t1 == nil {
+		return nil
+	}
+	t0 := t1[a>>22%(1<<10)]
+	if t0 == nil {
+		return nil
+	}
+	return t0[a>>12%(1<<10)]
+}
+
+func (p *Process) addMapping(m *Mapping) error {
+	if m.min%(1<<12) != 0 {
+		return fmt.Errorf("mapping start %x isn't a multiple of 4096", m.min)
+	}
+	if m.max%(1<<12) != 0 {
+		return fmt.Errorf("mapping end %x isn't a multiple of 4096", m.max)
+	}
+	for a := m.min; a < m.max; a += 1 << 12 {
+		i3 := a >> 52
+		t3 := p.pageTable[i3]
+		if t3 == nil {
+			t3 = new(pageTable3)
+			p.pageTable[i3] = t3
+		}
+		i2 := a >> 42 % (1 << 10)
+		t2 := t3[i2]
+		if t2 == nil {
+			t2 = new(pageTable2)
+			t3[i2] = t2
+		}
+		i1 := a >> 32 % (1 << 10)
+		t1 := t2[i1]
+		if t1 == nil {
+			t1 = new(pageTable1)
+			t2[i1] = t1
+		}
+		i0 := a >> 22 % (1 << 10)
+		t0 := t1[i0]
+		if t0 == nil {
+			t0 = new(pageTable0)
+			t1[i0] = t0
+		}
+		t0[a>>12%(1<<10)] = m
+	}
+	return nil
+}
diff --git a/core/process.go b/core/process.go
new file mode 100644
index 0000000..9afc491
--- /dev/null
+++ b/core/process.go
@@ -0,0 +1,530 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The core library is used to process ELF core dump files.  You can
+// open a core dump file and read from addresses in the process that
+// dumped core, called the "inferior". Some ancillary information
+// about the inferior is also provided, like architecture and OS
+// thread state.
+//
+// There's nothing Go-specific about this library, it could
+// just as easily be used to read a C++ core dump. See ../gocore
+// for the next layer up, a Go-specific core dump reader.
+//
+// The Read* operations all panic with an error (the builtin Go type)
+// if the inferior is not readable at the address requested.
+package core
+
+import (
+	"debug/dwarf"
+	"debug/elf" // TODO: use golang.org/x/debug/elf instead?
+	"encoding/binary"
+	"fmt"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"syscall"
+)
+
+// A Process represents the state of the process that core dumped.
+type Process struct {
+	base         string             // base directory from which files in the core can be found
+	exec         []*os.File         // executables (more than one for shlibs)
+	mappings     []*Mapping         // virtual address mappings
+	threads      []*Thread          // os threads (TODO: map from pid?)
+	arch         string             // amd64, ...
+	ptrSize      int64              // 4 or 8
+	logPtrSize   uint               // 2 or 3
+	byteOrder    binary.ByteOrder   //
+	littleEndian bool               // redundant with byteOrder
+	syms         map[string]Address // symbols (could be empty if executable is stripped)
+	symErr       error              // an error encountered while reading symbols
+	dwarf        *dwarf.Data        // debugging info (could be nil)
+	dwarfErr     error              // an error encountered while reading DWARF
+	pageTable    pageTable4         // for fast address->mapping lookups
+}
+
+// Mappings returns a list of virtual memory mappings for p.
+func (p *Process) Mappings() []*Mapping {
+	return p.mappings
+}
+
+// Readable reports whether the address a is readable.
+func (p *Process) Readable(a Address) bool {
+	return p.findMapping(a) != nil
+}
+
+// ReadableN reports whether the n bytes starting at address a are readable.
+func (p *Process) ReadableN(a Address, n int64) bool {
+	for {
+		m := p.findMapping(a)
+		if m == nil || m.perm&Read == 0 {
+			return false
+		}
+		c := m.max.Sub(a)
+		if n <= c {
+			return true
+		}
+		n -= c
+		a = a.Add(c)
+	}
+}
+
+// Writeable reports whether the address a was writeable (by the inferior at the time of the core dump).
+func (p *Process) Writeable(a Address) bool {
+	m := p.findMapping(a)
+	if m == nil {
+		return false
+	}
+	return m.perm&Write != 0
+}
+
+// Threads returns information about each OS thread in the inferior.
+func (p *Process) Threads() []*Thread {
+	return p.threads
+}
+
+func (p *Process) Arch() string {
+	return p.arch
+}
+
+// PtrSize returns the size in bytes of a pointer in the inferior.
+func (p *Process) PtrSize() int64 {
+	return p.ptrSize
+}
+func (p *Process) LogPtrSize() uint {
+	return p.logPtrSize
+}
+
+func (p *Process) ByteOrder() binary.ByteOrder {
+	return p.byteOrder
+}
+
+func (p *Process) DWARF() (*dwarf.Data, error) {
+	return p.dwarf, p.dwarfErr
+}
+
+// Symbols returns a mapping from name to inferior address, along with
+// any error encountered during reading the symbol information.
+// (There may be both an error and some returned symbols.)
+// Symbols might not be available with core files from stripped binaries.
+func (p *Process) Symbols() (map[string]Address, error) {
+	return p.syms, p.symErr
+}
+
+// Core takes the name of a core file and returns a Process that
+// represents the state of the inferior that generated the core file.
+func Core(coreFile, base string) (*Process, error) {
+	core, err := os.Open(coreFile)
+	if err != nil {
+		return nil, err
+	}
+
+	p := new(Process)
+	p.base = base
+	if err := p.readCore(core); err != nil {
+		return nil, err
+	}
+	if err := p.readExec(); err != nil {
+		return nil, err
+	}
+
+	// Double-check that we have underlying data available for all mappings.
+	for _, m := range p.mappings {
+		if m.f == nil {
+			return nil, fmt.Errorf("incomplete mapping %x %x", m.min, m.max)
+		}
+	}
+
+	// Sort then merge mappings, just to clean up a bit.
+	sort.Slice(p.mappings, func(i, j int) bool {
+		return p.mappings[i].min < p.mappings[j].min
+	})
+	ms := p.mappings[1:]
+	p.mappings = p.mappings[:1]
+	for _, m := range ms {
+		k := p.mappings[len(p.mappings)-1]
+		if m.min == k.max &&
+			m.perm == k.perm &&
+			m.f == k.f &&
+			m.off == k.off+k.Size() {
+			k.max = m.max
+			// TODO: also check origF?
+		} else {
+			p.mappings = append(p.mappings, m)
+		}
+	}
+
+	// Memory map all the mappings.
+	for _, m := range p.mappings {
+		var err error
+		m.contents, err = syscall.Mmap(int(m.f.Fd()), m.off, int(m.max.Sub(m.min)), syscall.PROT_READ, syscall.MAP_SHARED)
+		if err != nil {
+			return nil, fmt.Errorf("can't memory map %s at %d: %s\n", m.f, m.off, err)
+		}
+	}
+
+	// Build page table for mapping lookup.
+	for _, m := range p.mappings {
+		err := p.addMapping(m)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return p, nil
+}
+
+func (p *Process) readCore(core *os.File) error {
+	e, err := elf.NewFile(core)
+	if err != nil {
+		return err
+	}
+	if e.Type != elf.ET_CORE {
+		return fmt.Errorf("%s is not a core file", core.Name())
+	}
+	switch e.Class {
+	case elf.ELFCLASS32:
+		p.ptrSize = 4
+		p.logPtrSize = 2
+	case elf.ELFCLASS64:
+		p.ptrSize = 8
+		p.logPtrSize = 3
+	default:
+		return fmt.Errorf("unknown elf class %s\n", e.Class)
+	}
+	switch e.Machine {
+	case elf.EM_386:
+		p.arch = "386"
+	case elf.EM_X86_64:
+		p.arch = "amd64"
+		// TODO: detect amd64p32?
+	case elf.EM_ARM:
+		p.arch = "arm"
+	case elf.EM_AARCH64:
+		p.arch = "arm64"
+	case elf.EM_MIPS:
+		p.arch = "mips"
+	case elf.EM_MIPS_RS3_LE:
+		p.arch = "mipsle"
+		// TODO: value for mips64?
+	case elf.EM_PPC64:
+		if e.ByteOrder.String() == "LittleEndian" {
+			p.arch = "ppc64le"
+		} else {
+			p.arch = "ppc64"
+		}
+	case elf.EM_S390:
+		p.arch = "s390x"
+	default:
+		return fmt.Errorf("unknown arch %s\n", e.Machine)
+	}
+	p.byteOrder = e.ByteOrder
+	// We also compute explicitly what byte order the inferior is.
+	// Just using p.byteOrder to decode fields makes any arguments passed to it
+	// escape to the heap.  We use explicit binary.{Little,Big}Endian.UintXX
+	// calls when we want to avoid heap-allocating the buffer.
+	p.littleEndian = e.ByteOrder.String() == "LittleEndian"
+
+	// Load virtual memory mappings.
+	for _, prog := range e.Progs {
+		if prog.Type == elf.PT_LOAD {
+			if err := p.readLoad(core, e, prog); err != nil {
+				return err
+			}
+		}
+	}
+	// Load notes (includes file mapping information).
+	for _, prog := range e.Progs {
+		if prog.Type == elf.PT_NOTE {
+			if err := p.readNote(core, e, prog.Off, prog.Filesz); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (p *Process) readLoad(f *os.File, e *elf.File, prog *elf.Prog) error {
+	min := Address(prog.Vaddr)
+	max := min.Add(int64(prog.Memsz))
+	var perm Perm
+	if prog.Flags&elf.PF_R != 0 {
+		perm |= Read
+	}
+	if prog.Flags&elf.PF_W != 0 {
+		perm |= Write
+		if prog.Filesz != prog.Memsz {
+			return fmt.Errorf("Data at address %x is not complete. The core has %x bytes, we need %x bytes", min, prog.Filesz, prog.Memsz)
+			// For non-writeable sections, the underlying data
+			// might not be in the core file itself.
+			// The underlying data will be found during readNote.
+		}
+	}
+	if prog.Flags&elf.PF_X != 0 {
+		perm |= Exec
+	}
+	if perm == 0 {
+		// TODO: keep these nothing-mapped mappings?
+		return nil
+	}
+	m := &Mapping{min: min, max: max, perm: perm}
+	p.mappings = append(p.mappings, m)
+	if prog.Filesz > 0 {
+		// Data backing this mapping is in the core file.
+		m.f = f
+		m.off = int64(prog.Off)
+		if prog.Filesz < uint64(m.max.Sub(m.min)) {
+			// We only have partial data for this mapping in the core file.
+			// Trim the mapping and allocate an anonymous mapping for the remainder.
+			m2 := &Mapping{min: m.min.Add(int64(prog.Filesz)), max: m.max, perm: m.perm}
+			m.max = m2.min
+			p.mappings = append(p.mappings, m2)
+		}
+	}
+	return nil
+}
+
+func (p *Process) readNote(f *os.File, e *elf.File, off, size uint64) error {
+	// TODO: add this to debug/elf?
+	const NT_FILE elf.NType = 0x46494c45
+
+	b := make([]byte, size)
+	_, err := f.ReadAt(b, int64(off))
+	if err != nil {
+		return err
+	}
+	for len(b) > 0 {
+		namesz := e.ByteOrder.Uint32(b)
+		b = b[4:]
+		descsz := e.ByteOrder.Uint32(b)
+		b = b[4:]
+		typ := elf.NType(e.ByteOrder.Uint32(b))
+		b = b[4:]
+		name := string(b[:namesz-1])
+		b = b[(namesz+3)/4*4:]
+		desc := b[:descsz]
+		b = b[(descsz+3)/4*4:]
+
+		if name == "CORE" && typ == NT_FILE {
+			err := p.readNTFile(f, e, desc)
+			if err != nil {
+				return err
+			}
+		}
+		if name == "CORE" && typ == elf.NT_PRSTATUS {
+			// An OS thread (an M)
+			err := p.readPRStatus(f, e, desc)
+			if err != nil {
+				return err
+			}
+		}
+		// TODO: NT_FPREGSET for floating-point registers
+		// TODO: NT_PRPSINFO for ???
+	}
+	return nil
+}
+
+func (p *Process) readNTFile(f *os.File, e *elf.File, desc []byte) error {
+	// TODO: 4 instead of 8 for 32-bit machines?
+	count := e.ByteOrder.Uint64(desc)
+	desc = desc[8:]
+	pagesize := e.ByteOrder.Uint64(desc)
+	desc = desc[8:]
+	filenames := string(desc[3*8*count:])
+	desc = desc[:3*8*count]
+	for i := uint64(0); i < count; i++ {
+		min := Address(e.ByteOrder.Uint64(desc))
+		desc = desc[8:]
+		max := Address(e.ByteOrder.Uint64(desc))
+		desc = desc[8:]
+		off := int64(e.ByteOrder.Uint64(desc) * pagesize)
+		desc = desc[8:]
+
+		var name string
+		j := strings.IndexByte(filenames, 0)
+		if j >= 0 {
+			name = filenames[:j]
+			filenames = filenames[j+1:]
+		} else {
+			name = filenames
+			filenames = ""
+		}
+
+		backing, err := os.Open(filepath.Join(p.base, name))
+		if err != nil {
+			// Can't find mapped file.
+			// TODO: if we debug on a different machine,
+			// provide a way to map from core's file spec to a real file.
+			return fmt.Errorf("can't open mapped file: %v\n", err)
+		}
+
+		// TODO: this is O(n^2). Shouldn't be a big problem in practice.
+		p.splitMappingsAt(min)
+		p.splitMappingsAt(max)
+		for _, m := range p.mappings {
+			if m.max <= min || m.min >= max {
+				continue
+			}
+			// m should now be entirely in [min,max]
+			if !(m.min >= min && m.max <= max) {
+				panic("mapping overlapping end of file region")
+			}
+			if m.f == nil {
+				if m.perm&Write != 0 {
+					panic("writeable data missing from core")
+				}
+				m.f = backing
+				m.off = int64(off) + m.min.Sub(min)
+			} else {
+				// Data is both in the core file and in a mapped file.
+				// The mapped file may be stale (even if it is readonly now,
+				// it may have been writeable at some point).
+				// Keep the file+offset just for printing.
+				m.origF = backing
+				m.origOff = int64(off) + m.min.Sub(min)
+			}
+
+			// Save a reference to the executable files.
+			// We keep them around so we can try to get symbols from them.
+			// TODO: we should really only keep those files for which the
+			// symbols return the correct addresses given where the file is
+			// mapped in memory. Not sure what to do here.
+			// Seems to work for the base executable, for now.
+			if m.perm&Exec != 0 {
+				found := false
+				for _, x := range p.exec {
+					if x == m.f {
+						found = true
+						break
+					}
+
+				}
+				if !found {
+					p.exec = append(p.exec, m.f)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+// splitMappingsAt ensures that a is not in the middle of any mapping.
+// Splits mappings as necessary.
+func (p *Process) splitMappingsAt(a Address) {
+	for _, m := range p.mappings {
+		if a < m.min || a > m.max {
+			continue
+		}
+		if a == m.min || a == m.max {
+			return
+		}
+		// Split this mapping at a.
+		m2 := new(Mapping)
+		*m2 = *m
+		m.max = a
+		m2.min = a
+		if m2.f != nil {
+			m2.off += m.Size()
+		}
+		if m2.origF != nil {
+			m2.origOff += m.Size()
+		}
+		p.mappings = append(p.mappings, m2)
+		return
+	}
+}
+
+func (p *Process) readPRStatus(f *os.File, e *elf.File, desc []byte) error {
+	t := &Thread{}
+	p.threads = append(p.threads, t)
+	// Linux
+	//   sys/procfs.h:
+	//     struct elf_prstatus {
+	//       ...
+	//       pid_t	pr_pid;
+	//       ...
+	//       elf_gregset_t pr_reg;	/* GP registers */
+	//       ...
+	//     };
+	//   typedef struct elf_prstatus prstatus_t;
+	// Register numberings are listed in sys/user.h.
+	// prstatus layout will probably be different for each arch/os combo.
+	switch p.arch {
+	default:
+		// TODO: return error here?
+	case "amd64":
+		// 32 = offsetof(prstatus_t, pr_pid), 4 = sizeof(pid_t)
+		t.pid = uint64(p.byteOrder.Uint32(desc[32 : 32+4]))
+		// 112 = offsetof(prstatus_t, pr_reg), 216 = sizeof(elf_gregset_t)
+		reg := desc[112 : 112+216]
+		for i := 0; i < len(reg); i += 8 {
+			t.regs = append(t.regs, p.byteOrder.Uint64(reg[i:]))
+		}
+		// Registers are:
+		//  0: r15
+		//  1: r14
+		//  2: r13
+		//  3: r12
+		//  4: rbp
+		//  5: rbx
+		//  6: r11
+		//  7: r10
+		//  8: r9
+		//  9: r8
+		// 10: rax
+		// 11: rcx
+		// 12: rdx
+		// 13: rsi
+		// 14: rdi
+		// 15: orig_rax
+		// 16: rip
+		// 17: cs
+		// 18: eflags
+		// 19: rsp
+		// 20: ss
+		// 21: fs_base
+		// 22: gs_base
+		// 23: ds
+		// 24: es
+		// 25: fs
+		// 26: gs
+		t.pc = Address(t.regs[16])
+		t.sp = Address(t.regs[19])
+	}
+	return nil
+}
+
+func (p *Process) readExec() error {
+	p.syms = map[string]Address{}
+	for _, exec := range p.exec {
+		e, err := elf.NewFile(exec)
+		if err != nil {
+			return err
+		}
+		if e.Type != elf.ET_EXEC {
+			// This happens for shared libraries, the core file itself, ...
+			continue
+		}
+		syms, err := e.Symbols()
+		if err != nil {
+			p.symErr = fmt.Errorf("can't read symbols from %s", exec.Name())
+		} else {
+			for _, s := range syms {
+				p.syms[s.Name] = Address(s.Value)
+			}
+		}
+		// An error while reading DWARF info is not an immediate error,
+		// but any error will be returned if the caller asks for DWARF.
+		dwarf, err := e.DWARF()
+		if err != nil {
+			p.dwarfErr = fmt.Errorf("can't read DWARF info from %s: %s", exec.Name(), err)
+		} else {
+			p.dwarf = dwarf
+		}
+	}
+	return nil
+}
diff --git a/core/read.go b/core/read.go
new file mode 100644
index 0000000..6d3b7dd
--- /dev/null
+++ b/core/read.go
@@ -0,0 +1,145 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package core
+
+import (
+	"encoding/binary"
+	"fmt"
+)
+
+// All the Read* functions below will panic if something goes wrong.
+
+// ReadAt reads len(b) bytes at address a in the inferior
+// and stores them in b.
+func (p *Process) ReadAt(b []byte, a Address) {
+	for {
+		m := p.findMapping(a)
+		if m == nil {
+			panic(fmt.Errorf("address %x is not mapped in the core file", a))
+		}
+		n := copy(b, m.contents[a.Sub(m.min):])
+		if n == len(b) {
+			return
+		}
+		// Modify request to get data from the next mapping.
+		b = b[n:]
+		a = a.Add(int64(n))
+	}
+}
+
+// ReadUint8 returns a uint8 read from address a of the inferior.
+func (p *Process) ReadUint8(a Address) uint8 {
+	m := p.findMapping(a)
+	if m == nil {
+		panic(fmt.Errorf("address %x is not mapped in the core file", a))
+	}
+	return m.contents[a.Sub(m.min)]
+}
+
+// ReadUint16 returns a uint16 read from address a of the inferior.
+func (p *Process) ReadUint16(a Address) uint16 {
+	m := p.findMapping(a)
+	if m == nil {
+		panic(fmt.Errorf("address %x is not mapped in the core file", a))
+	}
+	b := m.contents[a.Sub(m.min):]
+	if len(b) < 2 {
+		var buf [2]byte
+		b = buf[:]
+		p.ReadAt(b, a)
+	}
+	if p.littleEndian {
+		return binary.LittleEndian.Uint16(b)
+	}
+	return binary.BigEndian.Uint16(b)
+}
+
+// ReadUint32 returns a uint32 read from address a of the inferior.
+func (p *Process) ReadUint32(a Address) uint32 {
+	m := p.findMapping(a)
+	if m == nil {
+		panic(fmt.Errorf("address %x is not mapped in the core file", a))
+	}
+	b := m.contents[a.Sub(m.min):]
+	if len(b) < 4 {
+		var buf [4]byte
+		b = buf[:]
+		p.ReadAt(b, a)
+	}
+	if p.littleEndian {
+		return binary.LittleEndian.Uint32(b)
+	}
+	return binary.BigEndian.Uint32(b)
+}
+
+// ReadUint64 returns a uint64 read from address a of the inferior.
+func (p *Process) ReadUint64(a Address) uint64 {
+	m := p.findMapping(a)
+	if m == nil {
+		panic(fmt.Errorf("address %x is not mapped in the core file", a))
+	}
+	b := m.contents[a.Sub(m.min):]
+	if len(b) < 8 {
+		var buf [8]byte
+		b = buf[:]
+		p.ReadAt(b, a)
+	}
+	if p.littleEndian {
+		return binary.LittleEndian.Uint64(b)
+	}
+	return binary.BigEndian.Uint64(b)
+}
+
+// ReadInt8 returns an int8 read from address a of the inferior.
+func (p *Process) ReadInt8(a Address) int8 {
+	return int8(p.ReadUint8(a))
+}
+
+// ReadInt16 returns an int16 read from address a of the inferior.
+func (p *Process) ReadInt16(a Address) int16 {
+	return int16(p.ReadUint16(a))
+}
+
+// ReadInt32 returns an int32 read from address a of the inferior.
+func (p *Process) ReadInt32(a Address) int32 {
+	return int32(p.ReadUint32(a))
+}
+
+// ReadInt64 returns an int64 read from address a of the inferior.
+func (p *Process) ReadInt64(a Address) int64 {
+	return int64(p.ReadUint64(a))
+}
+
+// ReadUintptr returns a uint of pointer size read from address a of the inferior.
+func (p *Process) ReadUintptr(a Address) uint64 {
+	if p.ptrSize == 4 {
+		return uint64(p.ReadUint32(a))
+	}
+	return p.ReadUint64(a)
+}
+
+// ReadInt returns an int (of pointer size) read from address a of the inferior.
+func (p *Process) ReadInt(a Address) int64 {
+	if p.ptrSize == 4 {
+		return int64(p.ReadInt32(a))
+	}
+	return p.ReadInt64(a)
+}
+
+// ReadPtr returns a pointer loaded from address a of the inferior.
+func (p *Process) ReadPtr(a Address) Address {
+	return Address(p.ReadUintptr(a))
+}
+
+// ReadCString reads a null-terminated string starting at address a.
+func (p *Process) ReadCString(a Address) string {
+	for n := int64(0); ; n++ {
+		if p.ReadUint8(a.Add(n)) == 0 {
+			b := make([]byte, n)
+			p.ReadAt(b, a)
+			return string(b)
+		}
+	}
+}
diff --git a/core/testdata/README b/core/testdata/README
new file mode 100644
index 0000000..11d9761
--- /dev/null
+++ b/core/testdata/README
@@ -0,0 +1,14 @@
+This directory contains a simple core file for use in testing.
+
+It was generated by running the following program with go1.9.0.
+
+package main
+
+func main() {
+	_ = *(*int)(nil)
+}
+
+The core file includes the executable by reference using an absolute
+path.  The executable was at /tmp/test, so that path is reproduced
+here so the core dump reader can find the executable using this
+testdata directory as the base directory.
diff --git a/core/testdata/core b/core/testdata/core
new file mode 100644
index 0000000..55318de
--- /dev/null
+++ b/core/testdata/core
Binary files differ
diff --git a/core/testdata/tmp/test b/core/testdata/tmp/test
new file mode 100755
index 0000000..f3353b2
--- /dev/null
+++ b/core/testdata/tmp/test
Binary files differ
diff --git a/core/thread.go b/core/thread.go
new file mode 100644
index 0000000..8c7517e
--- /dev/null
+++ b/core/thread.go
@@ -0,0 +1,35 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package core
+
+// A Thread represents an operating system thread.
+type Thread struct {
+	pid  uint64   // thread/process ID
+	regs []uint64 // set depends on arch
+	pc   Address  // program counter
+	sp   Address  // stack pointer
+}
+
+func (t *Thread) Pid() uint64 {
+	return t.pid
+}
+
+// Regs returns the set of register values for the thread.
+// What registers go where is architecture-dependent.
+// TODO: document for each architecture.
+// TODO: do this in some arch-independent way?
+func (t *Thread) Regs() []uint64 {
+	return t.regs
+}
+
+func (t *Thread) PC() Address {
+	return t.pc
+}
+
+func (t *Thread) SP() Address {
+	return t.sp
+}
+
+// TODO: link register?