delve/pkg/proc/dump.go
2024-10-11 12:34:25 -07:00

425 lines
9.1 KiB
Go

package proc
import (
"bytes"
"debug/elf"
"encoding/binary"
"errors"
"fmt"
"runtime"
"sync"
"github.com/go-delve/delve/pkg/elfwriter"
"github.com/go-delve/delve/pkg/version"
)
var (
ErrMemoryMapNotSupported = errors.New("MemoryMap not supported")
)
// DumpState represents the current state of a core dump in progress.
type DumpState struct {
Mutex sync.Mutex
Dumping bool
AllDone bool
Canceled bool
DoneChan chan struct{}
ThreadsDone, ThreadsTotal int
MemDone, MemTotal uint64
Err error
}
// DumpFlags is used to configure (*Target).Dump
type DumpFlags uint16
const (
DumpPlatformIndependent DumpFlags = 1 << iota // always use platform-independent notes format
)
// MemoryMapEntry represent a memory mapping in the target process.
type MemoryMapEntry struct {
Addr uint64
Size uint64
Read, Write, Exec bool
Filename string
Offset uint64
}
func (state *DumpState) setErr(err error) {
if err == nil {
return
}
state.Mutex.Lock()
if state.Err == nil {
state.Err = err
}
state.Mutex.Unlock()
}
func (state *DumpState) setThreadsTotal(n int) {
state.Mutex.Lock()
state.ThreadsTotal = n
state.ThreadsDone = 0
state.Mutex.Unlock()
}
func (state *DumpState) threadDone() {
state.Mutex.Lock()
state.ThreadsDone++
state.Mutex.Unlock()
}
func (state *DumpState) setMemTotal(n uint64) {
state.Mutex.Lock()
state.MemTotal = n
state.Mutex.Unlock()
}
func (state *DumpState) memDone(delta uint64) {
state.Mutex.Lock()
state.MemDone += delta
state.Mutex.Unlock()
}
func (state *DumpState) isCanceled() bool {
state.Mutex.Lock()
defer state.Mutex.Unlock()
return state.Canceled
}
// Dump writes a core dump to out. State is updated as the core dump is written.
func (t *Target) Dump(out elfwriter.WriteCloserSeeker, flags DumpFlags, state *DumpState) {
defer func() {
state.Mutex.Lock()
if ierr := recover(); ierr != nil {
state.Err = newInternalError(ierr, 2)
}
err := out.Close()
if state.Err == nil && err != nil {
state.Err = fmt.Errorf("error writing output file: %v", err)
}
state.Dumping = false
state.Mutex.Unlock()
if state.DoneChan != nil {
close(state.DoneChan)
}
}()
bi := t.BinInfo()
var fhdr elf.FileHeader
fhdr.Class = elf.ELFCLASS64
fhdr.Data = elf.ELFDATA2LSB
fhdr.Version = elf.EV_CURRENT
switch bi.GOOS {
case "linux":
fhdr.OSABI = elf.ELFOSABI_LINUX
case "freebsd":
fhdr.OSABI = elf.ELFOSABI_FREEBSD
default:
// There is no OSABI value for windows or macOS because nobody generates ELF core dumps on those systems.
fhdr.OSABI = 0xff
}
fhdr.Type = elf.ET_CORE
switch bi.Arch.Name {
case "amd64":
fhdr.Machine = elf.EM_X86_64
case "386":
fhdr.Machine = elf.EM_386
case "arm64":
fhdr.Machine = elf.EM_AARCH64
case "ppc64le":
fhdr.Machine = elf.EM_PPC64
case "riscv64":
fhdr.Machine = elf.EM_RISCV
default:
panic("not implemented")
}
fhdr.Entry = 0
w := elfwriter.New(out, &fhdr)
notes := []elfwriter.Note{}
entryPoint, err := t.EntryPoint()
if err != nil {
state.setErr(err)
return
}
notes = append(notes, elfwriter.Note{
Type: elfwriter.DelveHeaderNoteType,
Name: "Delve Header",
Data: []byte(fmt.Sprintf("%s/%s\n%s\n%s%d\n%s%#x\n", bi.GOOS, bi.Arch.Name, version.DelveVersion.String(), elfwriter.DelveHeaderTargetPidPrefix, t.pid, elfwriter.DelveHeaderEntryPointPrefix, entryPoint)),
})
threads := t.ThreadList()
state.setThreadsTotal(len(threads))
var threadsDone bool
if flags&DumpPlatformIndependent == 0 {
threadsDone, notes, err = t.proc.DumpProcessNotes(notes, state.threadDone)
if err != nil {
state.setErr(err)
return
}
}
if !threadsDone {
for _, th := range threads {
if w.Err != nil {
state.setErr(fmt.Errorf("error writing to output file: %v", w.Err))
return
}
if state.isCanceled() {
return
}
notes = t.dumpThreadNotes(notes, state, th)
state.threadDone()
}
}
memmap, err := t.proc.MemoryMap()
if err != nil {
state.setErr(err)
return
}
memmapFilter := make([]MemoryMapEntry, 0, len(memmap))
memtot := uint64(0)
for i := range memmap {
mme := &memmap[i]
if t.shouldDumpMemory(mme) {
memmapFilter = append(memmapFilter, *mme)
memtot += mme.Size
}
}
state.setMemTotal(memtot)
for i := range memmapFilter {
mme := &memmapFilter[i]
if w.Err != nil {
state.setErr(fmt.Errorf("error writing to output file: %v", w.Err))
return
}
if state.isCanceled() {
return
}
t.dumpMemory(state, w, mme)
}
notesProg := w.WriteNotes(notes)
w.Progs = append(w.Progs, notesProg)
w.WriteProgramHeaders()
if w.Err != nil {
state.setErr(fmt.Errorf("error writing to output file: %v", w.Err))
}
state.Mutex.Lock()
state.AllDone = true
state.Mutex.Unlock()
}
// dumpThreadNotes appends notes describing a thread (thread id and its
// registers) using a platform-independent format.
func (t *Target) dumpThreadNotes(notes []elfwriter.Note, state *DumpState, th Thread) []elfwriter.Note {
// If the backend doesn't provide a way to dump a thread we use a custom format for the note:
// - thread_id (8 bytes)
// - pc value (8 bytes)
// - sp value (8 bytes)
// - bp value (8 bytes)
// - tls value (8 bytes)
// - has_gaddr (1 byte)
// - gaddr value (8 bytes)
// - num_registers (4 bytes)
// Followed by a list of num_register, each as follows:
// - register_name_len (2 bytes)
// - register_name (register_name_len bytes)
// - register_data_len (2 bytes)
// - register_data (register_data_len bytes)
buf := new(bytes.Buffer)
_ = binary.Write(buf, binary.LittleEndian, uint64(th.ThreadID()))
regs, err := th.Registers()
if err != nil {
state.setErr(err)
return notes
}
for _, specialReg := range []uint64{regs.PC(), regs.SP(), regs.BP(), regs.TLS()} {
binary.Write(buf, binary.LittleEndian, specialReg)
}
gaddr, hasGaddr := regs.GAddr()
binary.Write(buf, binary.LittleEndian, hasGaddr)
binary.Write(buf, binary.LittleEndian, gaddr)
regsv, err := regs.Slice(true)
if err != nil {
state.setErr(err)
return notes
}
binary.Write(buf, binary.LittleEndian, uint32(len(regsv)))
for _, reg := range regsv {
binary.Write(buf, binary.LittleEndian, uint16(len(reg.Name)))
buf.WriteString(reg.Name)
if reg.Reg.Bytes != nil {
binary.Write(buf, binary.LittleEndian, uint16(len(reg.Reg.Bytes)))
buf.Write(reg.Reg.Bytes)
} else {
binary.Write(buf, binary.LittleEndian, uint16(8))
binary.Write(buf, binary.LittleEndian, reg.Reg.Uint64Val)
}
}
return append(notes, elfwriter.Note{
Type: elfwriter.DelveThreadNodeType,
Name: "",
Data: buf.Bytes(),
})
}
func (t *Target) dumpMemory(state *DumpState, w *elfwriter.Writer, mme *MemoryMapEntry) {
var flags elf.ProgFlag
if mme.Read {
flags |= elf.PF_R
}
if mme.Write {
flags |= elf.PF_W
}
if mme.Exec {
flags |= elf.PF_X
}
w.Progs = append(w.Progs, &elf.ProgHeader{
Type: elf.PT_LOAD,
Flags: flags,
Off: uint64(w.Here()),
Vaddr: mme.Addr,
Paddr: 0,
Filesz: mme.Size,
Memsz: mme.Size,
Align: 0,
})
buf := make([]byte, 1024*1024)
addr := mme.Addr
sz := mme.Size
mem := t.Memory()
for sz > 0 {
if w.Err != nil {
state.setErr(fmt.Errorf("error writing to output file: %v", w.Err))
return
}
if state.isCanceled() {
return
}
chunk := buf
if uint64(len(chunk)) > sz {
chunk = chunk[:sz]
}
n, err := mem.ReadMemory(chunk, addr)
for i := n; i < len(chunk); i++ {
chunk[i] = 0
}
// Errors and short reads are ignored, the most likely reason is that
// (*ProcessInternal).MemoryMap gave us a bad mapping that can't be read
// and the behavior that's maximally useful to the user is to generate an
// incomplete dump.
w.Write(chunk)
addr += uint64(len(chunk))
sz -= uint64(len(chunk))
if err == nil {
state.memDone(uint64(len(chunk)))
}
}
}
func (t *Target) shouldDumpMemory(mme *MemoryMapEntry) bool {
if !mme.Read {
return false
}
exeimg := t.BinInfo().Images[0]
if mme.Write || mme.Filename == "" || mme.Filename != exeimg.Path {
return true
}
isgo := false
for _, cu := range exeimg.compileUnits {
if cu.isgo {
isgo = true
break
}
}
if !isgo {
return true
}
exe, err := elf.Open(exeimg.Path)
if err != nil {
return true
}
if exe.Type != elf.ET_EXEC {
return true
}
for _, prog := range exe.Progs {
if prog.Type == elf.PT_LOAD && (prog.Flags&elf.PF_W == 0) && (prog.Flags&elf.PF_R != 0) && (prog.Vaddr == mme.Addr) && (prog.Memsz == mme.Size) && (prog.Off == mme.Offset) {
return false
}
}
return true
}
type internalError struct {
Err interface{}
Stack []internalErrorFrame
}
type internalErrorFrame struct {
Pc uintptr
Func string
File string
Line int
}
func newInternalError(ierr interface{}, skip int) *internalError {
r := &internalError{ierr, nil}
for i := skip; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fname := "<unknown>"
fn := runtime.FuncForPC(pc)
if fn != nil {
fname = fn.Name()
}
r.Stack = append(r.Stack, internalErrorFrame{pc, fname, file, line})
}
return r
}
func (err *internalError) Error() string {
var out bytes.Buffer
fmt.Fprintf(&out, "Internal debugger error: %v\n", err.Err)
for _, frame := range err.Stack {
fmt.Fprintf(&out, "%s (%#x)\n\t%s:%d\n", frame.Func, frame.Pc, frame.File, frame.Line)
}
return out.String()
}