diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md index a36d0f11..bea2522f 100644 --- a/Documentation/backend_test_health.md +++ b/Documentation/backend_test_health.md @@ -1,19 +1,19 @@ Tests skipped by each supported backend: -* 386 skipped = 2.1% (3/146) +* 386 skipped = 2% (3/147) * 1 broken * 2 broken - cgo stacktraces -* arm64 skipped = 2.1% (3/146) +* arm64 skipped = 2% (3/147) * 2 broken * 1 broken - global variable symbolication -* darwin/lldb skipped = 0.68% (1/146) +* darwin/lldb skipped = 0.68% (1/147) * 1 upstream issue -* freebsd skipped = 7.5% (11/146) +* freebsd skipped = 7.5% (11/147) * 11 broken -* linux/386/pie skipped = 0.68% (1/146) +* linux/386/pie skipped = 0.68% (1/147) * 1 broken -* pie skipped = 0.68% (1/146) +* pie skipped = 0.68% (1/147) * 1 upstream issue - https://github.com/golang/go/issues/29322 -* windows skipped = 1.4% (2/146) +* windows skipped = 1.4% (2/147) * 1 broken * 1 upstream issue diff --git a/Documentation/cli/README.md b/Documentation/cli/README.md index 1f6022eb..3cab6f7e 100644 --- a/Documentation/cli/README.md +++ b/Documentation/cli/README.md @@ -80,6 +80,7 @@ Command | Description [clear-checkpoint](#clear-checkpoint) | Deletes checkpoint. [config](#config) | Changes configuration parameters. [disassemble](#disassemble) | Disassembler. +[dump](#dump) | Creates a core dump from the current process state [edit](#edit) | Open where you are in $DELVE_EDITOR or $EDITOR [exit](#exit) | Exit the debugger. [funcs](#funcs) | Print list of functions. @@ -247,6 +248,14 @@ Move the current frame down. Move the current frame down by . The second form runs the command on the given frame. +## dump +Creates a core dump from the current process state + + dump + +The core dump is always written in ELF, even on systems (windows, macOS) where this is not customary. For environments other than linux/amd64 threads and registers are dumped in a format that only Delve can read back. + + ## edit Open where you are in $DELVE_EDITOR or $EDITOR diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index 5136f6b4..b56a64b2 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -28,6 +28,9 @@ raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, UnsafeCall) create_breakpoint(Breakpoint) | Equivalent to API call [CreateBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateBreakpoint) detach(Kill) | Equivalent to API call [Detach](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Detach) disassemble(Scope, StartPC, EndPC, Flavour) | Equivalent to API call [Disassemble](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Disassemble) +dump_cancel() | Equivalent to API call [DumpCancel](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.DumpCancel) +dump_start(Destination) | Equivalent to API call [DumpStart](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.DumpStart) +dump_wait(Wait) | Equivalent to API call [DumpWait](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.DumpWait) eval(Scope, Expr, Cfg) | Equivalent to API call [Eval](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Eval) examine_memory(Address, Length) | Equivalent to API call [ExamineMemory](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ExamineMemory) find_location(Scope, Loc, IncludeNonExecutableLines, SubstitutePathRules) | Equivalent to API call [FindLocation](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.FindLocation) diff --git a/Documentation/usage/dlv_core.md b/Documentation/usage/dlv_core.md index e7d0382f..db790e42 100644 --- a/Documentation/usage/dlv_core.md +++ b/Documentation/usage/dlv_core.md @@ -11,7 +11,7 @@ The core command will open the specified core file and the associated executable and let you examine the state of the process when the core dump was taken. -Currently supports linux/amd64 and linux/arm64 core files and windows/amd64 minidumps. +Currently supports linux/amd64 and linux/arm64 core files, windows/amd64 minidumps and core files generated by Delve's 'dump' command. ``` dlv core diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index 8171feff..649276b6 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -290,7 +290,7 @@ The core command will open the specified core file and the associated executable and let you examine the state of the process when the core dump was taken. -Currently supports linux/amd64 and linux/arm64 core files and windows/amd64 minidumps.`, +Currently supports linux/amd64 and linux/arm64 core files, windows/amd64 minidumps and core files generated by Delve's 'dump' command.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if len(args) != 2 { return errors.New("you must provide a core file and an executable") diff --git a/pkg/elfwriter/delve_core_notes.go b/pkg/elfwriter/delve_core_notes.go new file mode 100644 index 00000000..15b82bcd --- /dev/null +++ b/pkg/elfwriter/delve_core_notes.go @@ -0,0 +1,11 @@ +package elfwriter + +const ( + DelveHeaderNoteType = 0x444C5645 // DLVE + DelveThreadNodeType = 0x444C5654 // DLVT + + DelveHeaderTargetPidPrefix = "Target Pid: " + DelveHeaderEntryPointPrefix = "Entry Point: " +) + +//TODO(aarzilli): these constants probably need to be in a better place. diff --git a/pkg/elfwriter/writer.go b/pkg/elfwriter/writer.go new file mode 100644 index 00000000..3876b260 --- /dev/null +++ b/pkg/elfwriter/writer.go @@ -0,0 +1,183 @@ +// elfwriter is a package to write ELF files without having their entire +// contents in memory at any one time. +// This package is incomplete, only features needed to write core files are +// implemented, notably missing: +// - section headers +// - program headers at the beginning of the file + +package elfwriter + +import ( + "debug/elf" + "encoding/binary" + "io" +) + +// WriteCloserSeeker is the union of io.Writer, io.Closer and io.Seeker. +type WriteCloserSeeker interface { + io.Writer + io.Seeker + io.Closer +} + +// Writer writes ELF files. +type Writer struct { + w WriteCloserSeeker + Err error + Progs []*elf.ProgHeader + + seekProgHeader int64 + seekProgNum int64 +} + +type Note struct { + Type elf.NType + Name string + Data []byte +} + +// New creates a new Writer. +func New(w WriteCloserSeeker, fhdr *elf.FileHeader) *Writer { + const ( + ehsize = 64 + phentsize = 56 + ) + + if seek, _ := w.Seek(0, io.SeekCurrent); seek != 0 { + panic("can't write halfway through a file") + } + + r := &Writer{w: w} + + if fhdr.Class != elf.ELFCLASS64 { + panic("unsupported") + } + + if fhdr.Data != elf.ELFDATA2LSB { + panic("unsupported") + } + + // e_ident + r.Write([]byte{0x7f, 'E', 'L', 'F', byte(fhdr.Class), byte(fhdr.Data), byte(fhdr.Version), byte(fhdr.OSABI), byte(fhdr.ABIVersion), 0, 0, 0, 0, 0, 0, 0}) + + r.u16(uint16(fhdr.Type)) // e_type + r.u16(uint16(fhdr.Machine)) // e_machine + r.u32(uint32(fhdr.Version)) // e_version + r.u64(0) // e_entry + r.seekProgHeader = r.Here() + r.u64(0) // e_phoff + r.u64(0) // e_shoff + r.u32(0) // e_flags + r.u16(ehsize) // e_ehsize + r.u16(phentsize) // e_phentsize + r.seekProgNum = r.Here() + r.u16(0) // e_phnum + r.u16(0) // e_shentsize + r.u16(0) // e_shnum + r.u16(uint16(elf.SHN_UNDEF)) // e_shstrndx + + // Sanity check, size of file header should be the same as ehsize + if sz, _ := w.Seek(0, io.SeekCurrent); sz != ehsize { + panic("internal error, ELF header size") + } + + return r +} + +// WriteNotes writes notes to the current location, returns a ProgHeader describing the +// notes. +func (w *Writer) WriteNotes(notes []Note) *elf.ProgHeader { + if len(notes) == 0 { + return nil + } + h := &elf.ProgHeader{ + Type: elf.PT_NOTE, + Align: 4, + } + for i := range notes { + note := ¬es[i] + w.Align(4) + if h.Off == 0 { + h.Off = uint64(w.Here()) + } + w.u32(uint32(len(note.Name))) + w.u32(uint32(len(note.Data))) + w.u32(uint32(note.Type)) + w.Write([]byte(note.Name)) + w.Align(4) + w.Write(note.Data) + } + h.Filesz = uint64(w.Here()) - h.Off + return h +} + +// WriteProgramHeaders writes the program headers at the current location +// and patches the file header accordingly. +func (w *Writer) WriteProgramHeaders() { + phoff := w.Here() + + // Patch File Header + w.w.Seek(w.seekProgHeader, io.SeekStart) + w.u64(uint64(phoff)) + w.w.Seek(w.seekProgNum, io.SeekStart) + w.u64(uint64(len(w.Progs))) + w.w.Seek(0, io.SeekEnd) + + for _, prog := range w.Progs { + w.u32(uint32(prog.Type)) + w.u32(uint32(prog.Flags)) + w.u64(prog.Off) + w.u64(prog.Vaddr) + w.u64(prog.Paddr) + w.u64(prog.Filesz) + w.u64(prog.Memsz) + w.u64(prog.Align) + } +} + +// Here returns the current seek offset from the start of the file. +func (w *Writer) Here() int64 { + r, err := w.w.Seek(0, io.SeekCurrent) + if err != nil && w.Err == nil { + w.Err = err + } + return r +} + +// Align writes as many padding bytes as needed to make the current file +// offset a multiple of align. +func (w *Writer) Align(align int64) { + off := w.Here() + alignOff := (off + (align - 1)) &^ (align - 1) + if alignOff-off > 0 { + w.Write(make([]byte, alignOff-off)) + } +} + +func (w *Writer) Write(buf []byte) { + _, err := w.w.Write(buf) + if err != nil && w.Err == nil { + w.Err = err + } +} + +func (w *Writer) u16(n uint16) { + err := binary.Write(w.w, binary.LittleEndian, n) + if err != nil && w.Err == nil { + w.Err = err + } +} + +func (w *Writer) u32(n uint32) { + err := binary.Write(w.w, binary.LittleEndian, n) + if err != nil && w.Err == nil { + w.Err = err + } +} + +func (w *Writer) u64(n uint64) { + err := binary.Write(w.w, binary.LittleEndian, n) + if err != nil && w.Err == nil { + w.Err = err + } +} diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go index 4e357ac5..4855dbb7 100644 --- a/pkg/proc/core/core.go +++ b/pkg/proc/core/core.go @@ -5,9 +5,13 @@ import ( "fmt" "io" + "github.com/go-delve/delve/pkg/elfwriter" "github.com/go-delve/delve/pkg/proc" ) +// ErrNoThreads core file did not contain any threads. +var ErrNoThreads = errors.New("no threads found in core file") + // A splicedMemory represents a memory space formed from multiple regions, // each of which may override previously regions. For example, in the following // core, the program text was loaded at 0x400000: @@ -189,7 +193,7 @@ var ( type openFn func(string, string) (*process, proc.Thread, error) -var openFns = []openFn{readLinuxCore, readAMD64Minidump} +var openFns = []openFn{readLinuxOrPlatformIndependentCore, readAMD64Minidump} // ErrUnrecognizedFormat is returned when the core file is not recognized as // any of the supported formats. @@ -212,11 +216,16 @@ func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.Target, e return nil, err } + if currentThread == nil { + return nil, ErrNoThreads + } + return proc.NewTarget(p, currentThread, proc.NewTargetConfig{ Path: exePath, DebugInfoDirs: debugInfoDirs, DisableAsyncPreempt: false, - StopReason: proc.StopAttached}) + StopReason: proc.StopAttached, + CanDump: false}) } // BinInfo will return the binary info. @@ -447,3 +456,11 @@ func (p *process) FindThread(threadID int) (proc.Thread, bool) { t, ok := p.Threads[threadID] return t, ok } + +func (p *process) MemoryMap() ([]proc.MemoryMapEntry, error) { + return nil, proc.ErrMemoryMapNotSupported +} + +func (p *process) DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (threadsDone bool, out []elfwriter.Note, err error) { + return false, notes, nil +} diff --git a/pkg/proc/core/delve_core.go b/pkg/proc/core/delve_core.go new file mode 100644 index 00000000..3d2fefd2 --- /dev/null +++ b/pkg/proc/core/delve_core.go @@ -0,0 +1,165 @@ +package core + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/go-delve/delve/pkg/dwarf/op" + "github.com/go-delve/delve/pkg/elfwriter" + "github.com/go-delve/delve/pkg/proc" +) + +func platformFromNotes(notes []*note) (goos, goarch string, err error) { + for _, note := range notes { + if note.Type != elfwriter.DelveHeaderNoteType { + continue + } + lines := strings.Split(string(note.Desc.([]byte)), "\n") + v := strings.Split(lines[0], "/") + if len(v) != 2 { + return "", "", fmt.Errorf("malformed delve header note: %q", string(note.Desc.([]byte))) + } + return v[0], v[1], nil + } + panic("internal error") +} + +func threadsFromDelveNotes(p *process, notes []*note) (proc.Thread, error) { + var currentThread proc.Thread + for _, note := range notes { + if note.Type == elfwriter.DelveHeaderNoteType { + buf := bytes.NewBuffer(note.Desc.([]byte)) + for { + line, err := buf.ReadString('\n') + if err != nil { + break + } + if len(line) > 0 && line[len(line)-1] == '\n' { + line = line[:len(line)-1] + } + switch { + case strings.HasPrefix(line, elfwriter.DelveHeaderTargetPidPrefix): + pid, err := strconv.ParseUint(line[len(elfwriter.DelveHeaderTargetPidPrefix):], 10, 64) + if err != nil { + return nil, fmt.Errorf("malformed delve header note (bad pid): %v", err) + } + p.pid = int(pid) + case strings.HasPrefix(line, elfwriter.DelveHeaderEntryPointPrefix): + entry, err := strconv.ParseUint(line[len(elfwriter.DelveHeaderEntryPointPrefix):], 0, 64) + if err != nil { + return nil, fmt.Errorf("malformed delve header note (bad entry point): %v", err) + } + p.entryPoint = entry + } + } + } + + if note.Type != elfwriter.DelveThreadNodeType { + continue + } + body := bytes.NewReader(note.Desc.([]byte)) + th := new(delveThread) + th.regs = new(delveRegisters) + + var readerr error + read := func(out interface{}) { + if readerr != nil { + return + } + readerr = binary.Read(body, binary.LittleEndian, out) + } + + read(&th.id) + + read(&th.regs.pc) + read(&th.regs.sp) + read(&th.regs.bp) + read(&th.regs.tls) + read(&th.regs.hasGAddr) + read(&th.regs.gaddr) + + var n uint32 + read(&n) + + if readerr != nil { + return nil, fmt.Errorf("error reading thread note header for thread %d: %v", th.id, readerr) + } + + th.regs.slice = make([]proc.Register, n) + + readBytes := func(maxlen uint16, kind string) []byte { + if readerr != nil { + return nil + } + var len uint16 + read(&len) + if maxlen > 0 && len > maxlen { + readerr = fmt.Errorf("maximum len exceeded (%d) reading %s", len, kind) + return nil + } + buf := make([]byte, len) + if readerr != nil { + return nil + } + _, readerr = body.Read(buf) + return buf + } + + for i := 0; i < int(n); i++ { + name := string(readBytes(20, "register name")) + value := readBytes(2048, "register value") + th.regs.slice[i] = proc.Register{Name: name, Reg: op.DwarfRegisterFromBytes(value)} + if readerr != nil { + return nil, fmt.Errorf("error reading thread note registers for thread %d: %v", th.id, readerr) + } + } + + p.Threads[int(th.id)] = &thread{th, p, proc.CommonThread{}} + if currentThread == nil { + currentThread = p.Threads[int(th.id)] + } + } + return currentThread, nil +} + +type delveThread struct { + id uint64 + regs *delveRegisters +} + +func (th *delveThread) pid() int { + return int(th.id) +} + +func (th *delveThread) registers() (proc.Registers, error) { + return th.regs, nil +} + +type delveRegisters struct { + pc, sp, bp, tls uint64 + hasGAddr bool + gaddr uint64 + slice []proc.Register +} + +func (regs *delveRegisters) PC() uint64 { return regs.pc } +func (regs *delveRegisters) BP() uint64 { return regs.bp } +func (regs *delveRegisters) SP() uint64 { return regs.sp } +func (regs *delveRegisters) TLS() uint64 { return regs.tls } +func (regs *delveRegisters) GAddr() (uint64, bool) { return regs.gaddr, regs.hasGAddr } + +func (regs *delveRegisters) Copy() (proc.Registers, error) { + return regs, nil +} + +func (regs *delveRegisters) Get(int) (uint64, error) { + return 0, errors.New("not supported") +} + +func (regs *delveRegisters) Slice(bool) ([]proc.Register, error) { + return regs.slice, nil +} diff --git a/pkg/proc/core/linux_core.go b/pkg/proc/core/linux_core.go index 5d758c65..25ebbbfa 100644 --- a/pkg/proc/core/linux_core.go +++ b/pkg/proc/core/linux_core.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "github.com/go-delve/delve/pkg/elfwriter" "github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/pkg/proc/amd64util" "github.com/go-delve/delve/pkg/proc/linutil" @@ -84,13 +85,14 @@ func linuxThreadsFromNotes(p *process, notes []*note, machineType elf.Machine) p return currentThread } -// readLinuxCore reads a core file from corePath corresponding to the executable at -// exePath. For details on the Linux ELF core format, see: +// readLinuxOrPlatformIndependentCore reads a core file from corePath +// corresponding to the executable at exePath. For details on the Linux ELF +// core format, see: // http://www.gabriel.urdhr.fr/2015/05/29/core-file/, // http://uhlo.blogspot.fr/2012/05/brief-look-into-core-dumps.html, // elf_core_dump in http://lxr.free-electrons.com/source/fs/binfmt_elf.c, // and, if absolutely desperate, readelf.c from the binutils source. -func readLinuxCore(corePath, exePath string) (*process, proc.Thread, error) { +func readLinuxOrPlatformIndependentCore(corePath, exePath string) (*process, proc.Thread, error) { coreFile, err := elf.Open(corePath) if err != nil { if _, isfmterr := err.(*elf.FormatError); isfmterr && (strings.Contains(err.Error(), elfErrorBadMagicNumber) || strings.Contains(err.Error(), " at offset 0x0: too short")) { @@ -99,38 +101,54 @@ func readLinuxCore(corePath, exePath string) (*process, proc.Thread, error) { } return nil, nil, err } + + if coreFile.Type != elf.ET_CORE { + return nil, nil, fmt.Errorf("%v is not a core file", coreFile) + } + + machineType := coreFile.Machine + notes, platformIndependentDelveCore, err := readNotes(coreFile, machineType) + if err != nil { + return nil, nil, err + } + exe, err := os.Open(exePath) if err != nil { return nil, nil, err } exeELF, err := elf.NewFile(exe) if err != nil { - return nil, nil, err + if !platformIndependentDelveCore { + return nil, nil, err + } + } else { + if exeELF.Machine != machineType { + return nil, nil, fmt.Errorf("architecture mismatch between core file (%#x) and executable file (%#x)", machineType, exeELF.Machine) + } + if exeELF.Type != elf.ET_EXEC && exeELF.Type != elf.ET_DYN { + return nil, nil, fmt.Errorf("%v is not an exe file", exeELF) + } } - if coreFile.Type != elf.ET_CORE { - return nil, nil, fmt.Errorf("%v is not a core file", coreFile) - } - if exeELF.Type != elf.ET_EXEC && exeELF.Type != elf.ET_DYN { - return nil, nil, fmt.Errorf("%v is not an exe file", exeELF) - } - - machineType := exeELF.Machine - notes, err := readNotes(coreFile, machineType) - if err != nil { - return nil, nil, err - } memory := buildMemory(coreFile, exeELF, exe, notes) // TODO support 386 var bi *proc.BinaryInfo - switch machineType { - case _EM_X86_64: - bi = proc.NewBinaryInfo("linux", "amd64") - case _EM_AARCH64: - bi = proc.NewBinaryInfo("linux", "arm64") - default: - return nil, nil, fmt.Errorf("unsupported machine type") + if platformIndependentDelveCore { + goos, goarch, err := platformFromNotes(notes) + if err != nil { + return nil, nil, err + } + bi = proc.NewBinaryInfo(goos, goarch) + } else { + switch machineType { + case _EM_X86_64: + bi = proc.NewBinaryInfo("linux", "amd64") + case _EM_AARCH64: + bi = proc.NewBinaryInfo("linux", "arm64") + default: + return nil, nil, fmt.Errorf("unsupported machine type") + } } entryPoint := findEntryPoint(notes, bi.Arch.PtrSize()) @@ -143,6 +161,11 @@ func readLinuxCore(corePath, exePath string) (*process, proc.Thread, error) { breakpoints: proc.NewBreakpointMap(), } + if platformIndependentDelveCore { + currentThread, err := threadsFromDelveNotes(p, notes) + return p, currentThread, err + } + currentThread := linuxThreadsFromNotes(p, notes, machineType) return p, currentThread, nil } @@ -193,7 +216,7 @@ type note struct { } // readNotes reads all the notes from the notes prog in core. -func readNotes(core *elf.File, machineType elf.Machine) ([]*note, error) { +func readNotes(core *elf.File, machineType elf.Machine) ([]*note, bool, error) { var notesProg *elf.Prog for _, prog := range core.Progs { if prog.Type == elf.PT_NOTE { @@ -203,6 +226,9 @@ func readNotes(core *elf.File, machineType elf.Machine) ([]*note, error) { } r := notesProg.Open() + hasDelveThread := false + hasDelveHeader := false + hasElfPrStatus := false notes := []*note{} for { note, err := readNote(r, machineType) @@ -210,12 +236,20 @@ func readNotes(core *elf.File, machineType elf.Machine) ([]*note, error) { break } if err != nil { - return nil, err + return nil, false, err + } + switch note.Type { + case elfwriter.DelveHeaderNoteType: + hasDelveHeader = true + case elfwriter.DelveThreadNodeType: + hasDelveThread = true + case elf.NT_PRSTATUS: + hasElfPrStatus = true } notes = append(notes, note) } - return notes, nil + return notes, hasDelveThread && hasDelveHeader && !hasElfPrStatus, nil } // readNote reads a single note from r, decoding the descriptor if possible. @@ -286,7 +320,7 @@ func readNote(r io.ReadSeeker, machineType elf.Machine) (*note, error) { } note.Desc = &fpregs } - case _NT_AUXV: + case _NT_AUXV, elfwriter.DelveHeaderNoteType, elfwriter.DelveThreadNodeType: note.Desc = desc case _NT_FPREGSET: if machineType == _EM_AARCH64 { @@ -340,6 +374,9 @@ func buildMemory(core, exeELF *elf.File, exe io.ReaderAt, notes []*note) proc.Me // Load memory segments from exe and then from the core file, // allowing the corefile to overwrite previously loaded segments for _, elfFile := range []*elf.File{exeELF, core} { + if elfFile == nil { + continue + } for _, prog := range elfFile.Progs { if prog.Type == elf.PT_LOAD { if prog.Filesz == 0 { diff --git a/pkg/proc/dump.go b/pkg/proc/dump.go new file mode 100644 index 00000000..0cd5dd39 --- /dev/null +++ b/pkg/proc/dump.go @@ -0,0 +1,420 @@ +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 platfrom-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 + 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 (regiter_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.Write([]byte(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 := "" + 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() +} diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index 5bf769fb..b997e478 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -80,6 +80,7 @@ import ( "golang.org/x/arch/arm64/arm64asm" "golang.org/x/arch/x86/x86asm" + "github.com/go-delve/delve/pkg/elfwriter" "github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/pkg/proc/linutil" @@ -650,7 +651,8 @@ func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason Path: path, DebugInfoDirs: debugInfoDirs, DisableAsyncPreempt: runtime.GOOS == "darwin", - StopReason: stopReason}) + StopReason: stopReason, + CanDump: runtime.GOOS == "darwin"}) if err != nil { p.conn.conn.Close() return nil, err @@ -1456,6 +1458,37 @@ func (p *gdbProcess) loadGInstr() []byte { return buf.Bytes() } +func (p *gdbProcess) MemoryMap() ([]proc.MemoryMapEntry, error) { + r := []proc.MemoryMapEntry{} + addr := uint64(0) + for addr != ^uint64(0) { + mri, err := p.conn.memoryRegionInfo(addr) + if err != nil { + return nil, err + } + if addr+mri.size <= addr { + return nil, errors.New("qMemoryRegionInfo response wrapped around the address space or stuck") + } + if mri.permissions != "" { + var mme proc.MemoryMapEntry + + mme.Addr = addr + mme.Size = mri.size + mme.Read = strings.Contains(mri.permissions, "r") + mme.Write = strings.Contains(mri.permissions, "w") + mme.Exec = strings.Contains(mri.permissions, "x") + + r = append(r, mme) + } + addr += mri.size + } + return r, nil +} + +func (p *gdbProcess) DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (threadsDone bool, out []elfwriter.Note, err error) { + return false, notes, nil +} + func (regs *gdbRegisters) init(regsInfo []gdbRegisterInfo, arch *proc.Arch) { regs.arch = arch regs.regs = make(map[string]gdbRegister) diff --git a/pkg/proc/gdbserial/gdbserver_conn.go b/pkg/proc/gdbserial/gdbserver_conn.go index d2936137..bf74a8c1 100644 --- a/pkg/proc/gdbserial/gdbserver_conn.go +++ b/pkg/proc/gdbserial/gdbserver_conn.go @@ -44,6 +44,7 @@ type gdbConn struct { maxTransmitAttempts int // maximum number of transmit or receive attempts when bad checksums are read threadSuffixSupported bool // thread suffix supported by stub isDebugserver bool // true if the stub is debugserver + xcmdok bool // x command can be used to transfer memory log *logrus.Entry } @@ -163,6 +164,10 @@ func (conn *gdbConn) handshake() error { } } + if resp, err := conn.exec([]byte("$x0,0"), "init"); err == nil && string(resp) == "OK" { + conn.xcmdok = true + } + return nil } @@ -871,8 +876,15 @@ func (conn *gdbConn) appendThreadSelector(threadID string) { fmt.Fprintf(&conn.outbuf, ";thread:%s;", threadID) } -// executes 'm' (read memory) command func (conn *gdbConn) readMemory(data []byte, addr uint64) error { + if conn.xcmdok && len(data) > conn.packetSize { + return conn.readMemoryBinary(data, addr) + } + return conn.readMemoryHex(data, addr) +} + +// executes 'm' (read memory) command +func (conn *gdbConn) readMemoryHex(data []byte, addr uint64) error { size := len(data) data = data[:0] @@ -900,6 +912,29 @@ func (conn *gdbConn) readMemory(data []byte, addr uint64) error { return nil } +// executes 'x' (binary read memory) command +func (conn *gdbConn) readMemoryBinary(data []byte, addr uint64) error { + size := len(data) + data = data[:0] + + for len(data) < size { + conn.outbuf.Reset() + + sz := size - len(data) + + fmt.Fprintf(&conn.outbuf, "$x%x,%x", addr+uint64(len(data)), sz) + if err := conn.send(conn.outbuf.Bytes()); err != nil { + return err + } + resp, err := conn.recv(conn.outbuf.Bytes(), "binary memory read", true) + if err != nil { + return err + } + data = append(data, resp...) + } + return nil +} + func writeAsciiBytes(w io.Writer, data []byte) { for _, b := range data { fmt.Fprintf(w, "%02x", b) @@ -1016,6 +1051,87 @@ func (conn *gdbConn) getLoadedDynamicLibraries() ([]imageDescription, error) { return images.Images, err } +type memoryRegionInfo struct { + start uint64 + size uint64 + permissions string + name string +} + +func decodeHexString(in []byte) (string, bool) { + out := make([]byte, 0, len(in)/2) + for i := 0; i < len(in); i += 2 { + v, err := strconv.ParseUint(string(in[i:i+2]), 16, 8) + if err != nil { + return "", false + } + out = append(out, byte(v)) + } + return string(out), true +} + +func (conn *gdbConn) memoryRegionInfo(addr uint64) (*memoryRegionInfo, error) { + conn.outbuf.Reset() + fmt.Fprintf(&conn.outbuf, "$qMemoryRegionInfo:%x", addr) + resp, err := conn.exec(conn.outbuf.Bytes(), "qMemoryRegionInfo") + if err != nil { + return nil, err + } + + mri := &memoryRegionInfo{} + + buf := resp + for len(buf) > 0 { + colon := bytes.Index(buf, []byte{':'}) + if colon < 0 { + break + } + key := buf[:colon] + buf = buf[colon+1:] + + semicolon := bytes.Index(buf, []byte{';'}) + var value []byte + if semicolon < 0 { + value = buf + buf = nil + } else { + value = buf[:semicolon] + buf = buf[semicolon+1:] + } + + switch string(key) { + case "start": + start, err := strconv.ParseUint(string(value), 16, 64) + if err != nil { + return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (start): %v in %s", err, string(resp)) + } + mri.start = start + case "size": + size, err := strconv.ParseUint(string(value), 16, 64) + if err != nil { + return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (size): %v in %s", err, string(resp)) + } + mri.size = size + case "permissions": + mri.permissions = string(value) + case "name": + namestr, ok := decodeHexString(value) + if !ok { + return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (name): %s", string(resp)) + } + mri.name = namestr + case "error": + errstr, ok := decodeHexString(value) + if !ok { + return nil, fmt.Errorf("malformed qMemoryRegionInfo response packet (error): %s", string(resp)) + } + return nil, fmt.Errorf("qMemoryRegionInfo error: %s", errstr) + } + } + + return mri, nil +} + // exec executes a message to the stub and reads a response. // The details of the wire protocol are described here: // https://sourceware.org/gdb/onlinedocs/gdb/Overview.html#Overview diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go index a5076736..3b8f17c6 100644 --- a/pkg/proc/interface.go +++ b/pkg/proc/interface.go @@ -1,5 +1,7 @@ package proc +import "github.com/go-delve/delve/pkg/elfwriter" + // Process represents the target of the debugger. This // target could be a system process, core file, etc. // @@ -35,6 +37,12 @@ type ProcessInternal interface { WriteBreakpoint(*Breakpoint) error EraseBreakpoint(*Breakpoint) error + + // DumpProcessNotes returns ELF core notes describing the process and its threads. + // Implementing this method is optional. + DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (bool, []elfwriter.Note, error) + // MemoryMap returns the memory map of the target process. This method must be implemented if CanDump is true. + MemoryMap() ([]MemoryMapEntry, error) } // RecordingManipulation is an interface for manipulating process recordings. diff --git a/pkg/proc/native/dump_linux.go b/pkg/proc/native/dump_linux.go new file mode 100644 index 00000000..dd38d68a --- /dev/null +++ b/pkg/proc/native/dump_linux.go @@ -0,0 +1,120 @@ +package native + +import ( + "fmt" + "io/ioutil" + "strconv" + "strings" + + "github.com/go-delve/delve/pkg/proc" +) + +func (p *nativeProcess) MemoryMap() ([]proc.MemoryMapEntry, error) { + const VmFlagsPrefix = "VmFlags:" + + smapsbuf, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/smaps", p.Pid())) + if err != nil { + // Older versions of Linux don't have smaps but have maps which is in a similar format. + smapsbuf, err = ioutil.ReadFile(fmt.Sprintf("/proc/%d/maps", p.Pid())) + if err != nil { + return nil, err + } + } + smapsLines := strings.Split(string(smapsbuf), "\n") + r := make([]proc.MemoryMapEntry, 0) + +smapsLinesLoop: + for i := 0; i < len(smapsLines); { + line := smapsLines[i] + if line == "" { + i++ + continue + } + start, end, perm, offset, dev, filename, err := parseSmapsHeaderLine(i+1, line) + if err != nil { + return nil, err + } + var vmflags []string + for i++; i < len(smapsLines); i++ { + line := smapsLines[i] + if line == "" || line[0] < 'A' || line[0] > 'Z' { + break + } + if strings.HasPrefix(line, VmFlagsPrefix) { + vmflags = strings.Split(strings.TrimSpace(line[len(VmFlagsPrefix):]), " ") + } + } + + for i := range vmflags { + switch vmflags[i] { + case "dd": + // "don't dump" + continue smapsLinesLoop + case "io": + continue smapsLinesLoop + } + } + if strings.HasPrefix(dev, "00:") { + filename = "" + offset = 0 + } + + r = append(r, proc.MemoryMapEntry{ + Addr: start, + Size: end - start, + + Read: perm[0] == 'r', + Write: perm[1] == 'w', + Exec: perm[2] == 'x', + + Filename: filename, + Offset: offset, + }) + + } + return r, nil +} + +func parseSmapsHeaderLine(lineno int, in string) (start, end uint64, perm string, offset uint64, dev, filename string, err error) { + fields := strings.SplitN(in, " ", 6) + if len(fields) != 6 { + err = fmt.Errorf("malformed /proc/pid/maps on line %d: %q (wrong number of fields)", lineno, in) + return + } + + v := strings.Split(fields[0], "-") + if len(v) != 2 { + err = fmt.Errorf("malformed /proc/pid/maps on line %d: %q (bad first field)", lineno, in) + return + } + start, err = strconv.ParseUint(v[0], 16, 64) + if err != nil { + err = fmt.Errorf("malformed /proc/pid/maps on line %d: %q (%v)", lineno, in, err) + return + } + end, err = strconv.ParseUint(v[1], 16, 64) + if err != nil { + err = fmt.Errorf("malformed /proc/pid/maps on line %d: %q (%v)", lineno, in, err) + return + } + + perm = fields[1] + if len(perm) < 4 { + err = fmt.Errorf("malformed /proc/pid/maps on line %d: %q (permissions column too short)", lineno, in) + return + } + + offset, err = strconv.ParseUint(fields[2], 16, 64) + if err != nil { + err = fmt.Errorf("malformed /proc/pid/maps on line %d: %q (%v)", lineno, in, err) + return + } + + dev = fields[3] + + // fields[4] -> inode + + filename = strings.TrimLeft(fields[5], " ") + return + +} diff --git a/pkg/proc/native/dump_linux_amd64.go b/pkg/proc/native/dump_linux_amd64.go new file mode 100644 index 00000000..0e36b3b4 --- /dev/null +++ b/pkg/proc/native/dump_linux_amd64.go @@ -0,0 +1,152 @@ +package native + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/go-delve/delve/pkg/elfwriter" + "github.com/go-delve/delve/pkg/proc/linutil" + "golang.org/x/sys/unix" +) + +const _NT_AUXV elf.NType = 0x6 + +type linuxPrPsInfo struct { + State uint8 + Sname int8 + Zomb uint8 + Nice int8 + _ [4]uint8 + Flag uint64 + Uid, Gid uint32 + Pid, Ppid, Pgrp, Sid int32 + Fname [16]uint8 + Args [80]uint8 +} + +func (p *nativeProcess) DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (threadsDone bool, out []elfwriter.Note, err error) { + tobytes := func(x interface{}) []byte { + out := new(bytes.Buffer) + _ = binary.Write(out, binary.LittleEndian, x) + return out.Bytes() + } + + prpsinfo := linuxPrPsInfo{ + Pid: int32(p.pid), + } + + fname := p.os.comm + if len(fname) > len(prpsinfo.Fname)-1 { + fname = fname[:len(prpsinfo.Fname)-1] + } + copy(prpsinfo.Fname[:], fname) + prpsinfo.Fname[len(fname)] = 0 + + if cmdline, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", p.pid)); err == nil { + for len(cmdline) > 0 && cmdline[len(cmdline)-1] == '\n' { + cmdline = cmdline[:len(cmdline)-1] + } + if zero := bytes.Index(cmdline, []byte{0}); zero >= 0 { + cmdline = cmdline[zero+1:] + } + path := p.BinInfo().Images[0].Path + if abs, err := filepath.Abs(path); err == nil { + path = abs + } + args := make([]byte, 0, len(path)+len(cmdline)+1) + args = append(args, []byte(path)...) + args = append(args, 0) + args = append(args, cmdline...) + if len(args) > len(prpsinfo.Args)-1 { + args = args[:len(prpsinfo.Args)-1] + } + copy(prpsinfo.Args[:], args) + prpsinfo.Args[len(args)] = 0 + } + notes = append(notes, elfwriter.Note{ + Type: elf.NT_PRPSINFO, + Data: tobytes(prpsinfo), + }) + + auxvbuf, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/auxv", p.pid)) + if err == nil { + notes = append(notes, elfwriter.Note{ + Type: _NT_AUXV, + Data: auxvbuf, + }) + } + + for _, th := range p.threads { + regs, err := th.Registers() + if err != nil { + return false, notes, err + } + + regs, err = regs.Copy() // triggers floating point register load + if err != nil { + return false, notes, err + } + + nregs := regs.(*linutil.AMD64Registers) + + var prstatus linuxPrStatusAMD64 + prstatus.Pid = int32(th.ID) + prstatus.Ppid = int32(p.pid) + prstatus.Pgrp = int32(p.pid) + prstatus.Sid = int32(p.pid) + prstatus.Reg = *(nregs.Regs) + notes = append(notes, elfwriter.Note{ + Type: elf.NT_PRSTATUS, + Data: tobytes(prstatus), + }) + + var xsave []byte + + if nregs.Fpregset != nil && nregs.Fpregset.Xsave != nil { + xsave = make([]byte, len(nregs.Fpregset.Xsave)) + copy(xsave, nregs.Fpregset.Xsave) + } else { + xsave = make([]byte, 512+64) // XSAVE header start + XSAVE header length + } + + // Even if we have the XSAVE area on some versions of linux (or some CPU + // models?) it won't contain the legacy x87 registers, so copy them over + // in case we got them from PTRACE_GETFPREGS. + buf := new(bytes.Buffer) + binary.Write(buf, binary.LittleEndian, &nregs.Fpregset.AMD64PtraceFpRegs) + copy(xsave, buf.Bytes()) + + notes = append(notes, elfwriter.Note{ + Type: _NT_X86_XSTATE, + Data: xsave, + }) + + threadDone() + } + + return true, notes, nil +} + +type linuxPrStatusAMD64 struct { + Siginfo linuxSiginfo + Cursig uint16 + _ [2]uint8 + Sigpend uint64 + Sighold uint64 + Pid, Ppid, Pgrp, Sid int32 + Utime, Stime, CUtime, CStime unix.Timeval + Reg linutil.AMD64PtraceRegs + Fpvalid int64 +} + +// LinuxSiginfo is a copy of the +// siginfo kernel struct. +type linuxSiginfo struct { + Signo int32 + Code int32 + Errno int32 +} diff --git a/pkg/proc/native/dump_linux_other.go b/pkg/proc/native/dump_linux_other.go new file mode 100644 index 00000000..9cd0b61d --- /dev/null +++ b/pkg/proc/native/dump_linux_other.go @@ -0,0 +1,11 @@ +//+build linux,!amd64 + +package native + +import ( + "github.com/go-delve/delve/pkg/elfwriter" +) + +func (p *nativeProcess) DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (threadsDone bool, out []elfwriter.Note, err error) { + return false, notes, nil +} diff --git a/pkg/proc/native/dump_other.go b/pkg/proc/native/dump_other.go new file mode 100644 index 00000000..5f92c20e --- /dev/null +++ b/pkg/proc/native/dump_other.go @@ -0,0 +1,16 @@ +//+build freebsd,amd64 darwin + +package native + +import ( + "github.com/go-delve/delve/pkg/elfwriter" + "github.com/go-delve/delve/pkg/proc" +) + +func (p *nativeProcess) MemoryMap() ([]proc.MemoryMapEntry, error) { + return nil, proc.ErrMemoryMapNotSupported +} + +func (p *nativeProcess) DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (threadsDone bool, notesout []elfwriter.Note, err error) { + return false, notes, nil +} diff --git a/pkg/proc/native/dump_windows_amd64.go b/pkg/proc/native/dump_windows_amd64.go new file mode 100644 index 00000000..084de02f --- /dev/null +++ b/pkg/proc/native/dump_windows_amd64.go @@ -0,0 +1,91 @@ +package native + +import ( + "errors" + "fmt" + "unsafe" + + "github.com/go-delve/delve/pkg/elfwriter" + "github.com/go-delve/delve/pkg/proc" +) + +func (p *nativeProcess) MemoryMap() ([]proc.MemoryMapEntry, error) { + var memoryMapError error + r := []proc.MemoryMapEntry{} + + p.execPtraceFunc(func() { + is64 := true + if isWow64 := uint32(0); _IsWow64Process(p.os.hProcess, &isWow64) != 0 { + if isWow64 != 0 { + is64 = false + } + } + + maxaddr := uint64(1 << 48) // windows64 uses only 48 bit addresses + if !is64 { + maxaddr = uint64(^uint32(0)) + } + + var meminfo _MEMORY_BASIC_INFORMATION + + for addr := uint64(0); addr < maxaddr; addr += meminfo.RegionSize { + size := _VirtualQueryEx(p.os.hProcess, uintptr(addr), &meminfo, unsafe.Sizeof(meminfo)) + if size == 0 { + // size == 0 is an error and the only error returned by VirtualQueryEx + // is when addr is above the highest address allocated for the + // application. + return + } + if size != unsafe.Sizeof(meminfo) { + memoryMapError = fmt.Errorf("bad size returned by _VirtualQueryEx: %d (expected %d)", size, unsafe.Sizeof(meminfo)) + return + } + if addr+meminfo.RegionSize <= addr { + // this shouldn't happen + memoryMapError = errors.New("VirtualQueryEx wrapped around the address space or stuck") + return + } + if meminfo.State == _MEM_FREE || meminfo.State == _MEM_RESERVE { + continue + } + if meminfo.Protect&_PAGE_GUARD != 0 { + // reading from this range will result in an error. + continue + } + + var mme proc.MemoryMapEntry + mme.Addr = addr + mme.Size = meminfo.RegionSize + + switch meminfo.Protect & 0xff { + case _PAGE_EXECUTE: + mme.Exec = true + case _PAGE_EXECUTE_READ: + mme.Exec = true + mme.Read = true + case _PAGE_EXECUTE_READWRITE: + mme.Exec = true + mme.Read = true + mme.Write = true + case _PAGE_EXECUTE_WRITECOPY: + mme.Exec = true + mme.Read = true + case _PAGE_NOACCESS: + case _PAGE_READONLY: + mme.Read = true + case _PAGE_READWRITE: + mme.Read = true + mme.Write = true + case _PAGE_WRITECOPY: + mme.Read = true + } + r = append(r, mme) + } + }) + + return r, memoryMapError +} + +func (p *nativeProcess) DumpProcessNotes(notes []elfwriter.Note, threadDone func()) (threadsDone bool, out []elfwriter.Note, err error) { + return false, notes, nil +} diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go index 1083c568..00db889a 100644 --- a/pkg/proc/native/proc.go +++ b/pkg/proc/native/proc.go @@ -25,13 +25,13 @@ type nativeProcess struct { // Thread used to read and write memory memthread *nativeThread - os *osProcessDetails - firstStart bool - resumeChan chan<- struct{} - ptraceChan chan func() - ptraceDoneChan chan interface{} - childProcess bool // this process was launched, not attached to - stopMu sync.Mutex // protects manualStopRequested + os *osProcessDetails + firstStart bool + resumeChan chan<- struct{} + ptraceChan chan func() + ptraceDoneChan chan interface{} + childProcess bool // this process was launched, not attached to + stopMu sync.Mutex // protects manualStopRequested // manualStopRequested is set if all the threads in the process were // signalled to stop as a result of a Halt API call. Used to disambiguate // why a thread is found to have stopped. @@ -285,7 +285,8 @@ func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc Path: path, DebugInfoDirs: debugInfoDirs, DisableAsyncPreempt: runtime.GOOS == "windows" || runtime.GOOS == "freebsd", - StopReason: stopReason}) + StopReason: stopReason, + CanDump: runtime.GOOS == "linux"}) } func (dbp *nativeProcess) handlePtraceFuncs() { diff --git a/pkg/proc/native/ptrace_linux_386.go b/pkg/proc/native/ptrace_linux_386.go index a0a64e30..7326107d 100644 --- a/pkg/proc/native/ptrace_linux_386.go +++ b/pkg/proc/native/ptrace_linux_386.go @@ -28,9 +28,10 @@ func ptraceGetRegset(tid int) (regset amd64util.AMD64Xstate, err error) { iov := sys.Iovec{Base: &xstateargs[0], Len: uint32(len(xstateargs))} _, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_GETREGSET, uintptr(tid), _NT_X86_XSTATE, uintptr(unsafe.Pointer(&iov)), 0, 0) if err != syscall.Errno(0) { - if err == syscall.ENODEV || err == syscall.EIO { + if err == syscall.ENODEV || err == syscall.EIO || err == syscall.EINVAL { // ignore ENODEV, it just means this CPU or kernel doesn't support XSTATE, see https://github.com/go-delve/delve/issues/1022 // also ignore EIO, it means that we are running on an old kernel (pre 2.6.34) and PTRACE_GETREGSET is not implemented + // also ignore EINVAL, it means the kernel itself does not support the NT_X86_XSTATE argument (but does support PTRACE_GETREGSET) err = nil } return diff --git a/pkg/proc/native/ptrace_linux_amd64.go b/pkg/proc/native/ptrace_linux_amd64.go index 5f5c99d5..b60c09c0 100644 --- a/pkg/proc/native/ptrace_linux_amd64.go +++ b/pkg/proc/native/ptrace_linux_amd64.go @@ -25,9 +25,10 @@ func ptraceGetRegset(tid int) (regset amd64util.AMD64Xstate, err error) { iov := sys.Iovec{Base: &xstateargs[0], Len: uint64(len(xstateargs))} _, _, err = syscall.Syscall6(syscall.SYS_PTRACE, sys.PTRACE_GETREGSET, uintptr(tid), _NT_X86_XSTATE, uintptr(unsafe.Pointer(&iov)), 0, 0) if err != syscall.Errno(0) { - if err == syscall.ENODEV || err == syscall.EIO { + if err == syscall.ENODEV || err == syscall.EIO || err == syscall.EINVAL { // ignore ENODEV, it just means this CPU or kernel doesn't support XSTATE, see https://github.com/go-delve/delve/issues/1022 // also ignore EIO, it means that we are running on an old kernel (pre 2.6.34) and PTRACE_GETREGSET is not implemented + // also ignore EINVAL, it means the kernel itself does not support the NT_X86_XSTATE argument (but does support PTRACE_GETREGSET) err = nil } return diff --git a/pkg/proc/native/syscall_windows.go b/pkg/proc/native/syscall_windows.go index b066a1bf..0cb622b3 100644 --- a/pkg/proc/native/syscall_windows.go +++ b/pkg/proc/native/syscall_windows.go @@ -70,6 +70,17 @@ type _EXCEPTION_RECORD struct { ExceptionInformation [_EXCEPTION_MAXIMUM_PARAMETERS]uintptr } +type _MEMORY_BASIC_INFORMATION struct { + BaseAddress uintptr + AllocationBase uintptr + AllocationProtect uint32 + PartitionId uint16 + RegionSize uint64 + State uint32 + Protect uint32 + Type uint32 +} + const ( _ThreadBasicInformation = 0 @@ -93,6 +104,20 @@ const ( _EXCEPTION_SINGLE_STEP = 0x80000004 _EXCEPTION_MAXIMUM_PARAMETERS = 15 + + _MEM_FREE = 0x10000 + _MEM_RESERVE = 0x2000 + + _PAGE_EXECUTE = 0x10 + _PAGE_EXECUTE_READ = 0x20 + _PAGE_EXECUTE_READWRITE = 0x40 + _PAGE_EXECUTE_WRITECOPY = 0x80 + _PAGE_NOACCESS = 0x01 + _PAGE_READONLY = 0x02 + _PAGE_READWRITE = 0x04 + _PAGE_WRITECOPY = 0x08 + + _PAGE_GUARD = 0x100 ) func _NT_SUCCESS(x _NTSTATUS) bool { @@ -117,3 +142,5 @@ type _CONTEXT = winutil.CONTEXT //sys _DebugActiveProcess(processid uint32) (err error) = kernel32.DebugActiveProcess //sys _DebugActiveProcessStop(processid uint32) (err error) = kernel32.DebugActiveProcessStop //sys _QueryFullProcessImageName(process syscall.Handle, flags uint32, exename *uint16, size *uint32) (err error) = kernel32.QueryFullProcessImageNameW +//sys _VirtualQueryEx(process syscall.Handle, addr uintptr, buffer *_MEMORY_BASIC_INFORMATION, length uintptr) (lengthOut uintptr) = kernel32.VirtualQueryEx +//sys _IsWow64Process(process syscall.Handle, wow64process *uint32) (ok uint32) = kernel32.IsWow64Process diff --git a/pkg/proc/native/zsyscall_windows.go b/pkg/proc/native/zsyscall_windows.go index a2950bcf..9476c2e9 100644 --- a/pkg/proc/native/zsyscall_windows.go +++ b/pkg/proc/native/zsyscall_windows.go @@ -1,17 +1,44 @@ -// MACHINE GENERATED BY 'go generate' COMMAND; DO NOT EDIT +// Code generated by 'go generate'; DO NOT EDIT. package native import ( "syscall" "unsafe" + + "golang.org/x/sys/windows" ) var _ unsafe.Pointer +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + var ( - modntdll = syscall.NewLazyDLL("ntdll.dll") - modkernel32 = syscall.NewLazyDLL("kernel32.dll") + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return nil + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modntdll = windows.NewLazySystemDLL("ntdll.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") procNtQueryInformationThread = modntdll.NewProc("NtQueryInformationThread") dbgUiRemoteBreakin = modntdll.NewProc("DbgUiRemoteBreakin") @@ -27,6 +54,8 @@ var ( procDebugActiveProcess = modkernel32.NewProc("DebugActiveProcess") procDebugActiveProcessStop = modkernel32.NewProc("DebugActiveProcessStop") procQueryFullProcessImageNameW = modkernel32.NewProc("QueryFullProcessImageNameW") + procVirtualQueryEx = modkernel32.NewProc("VirtualQueryEx") + procIsWow64Process = modkernel32.NewProc("IsWow64Process") ) func _NtQueryInformationThread(threadHandle syscall.Handle, infoclass int32, info uintptr, infolen uint32, retlen *uint32) (status _NTSTATUS) { @@ -39,7 +68,7 @@ func _GetThreadContext(thread syscall.Handle, context *_CONTEXT) (err error) { r1, _, e1 := syscall.Syscall(procGetThreadContext.Addr(), 2, uintptr(thread), uintptr(unsafe.Pointer(context)), 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -51,7 +80,7 @@ func _SetThreadContext(thread syscall.Handle, context *_CONTEXT) (err error) { r1, _, e1 := syscall.Syscall(procSetThreadContext.Addr(), 2, uintptr(thread), uintptr(unsafe.Pointer(context)), 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -64,7 +93,7 @@ func _SuspendThread(threadid syscall.Handle) (prevsuspcount uint32, err error) { prevsuspcount = uint32(r0) if prevsuspcount == 0xffffffff { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -77,7 +106,7 @@ func _ResumeThread(threadid syscall.Handle) (prevsuspcount uint32, err error) { prevsuspcount = uint32(r0) if prevsuspcount == 0xffffffff { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -89,7 +118,7 @@ func _ContinueDebugEvent(processid uint32, threadid uint32, continuestatus uint3 r1, _, e1 := syscall.Syscall(procContinueDebugEvent.Addr(), 3, uintptr(processid), uintptr(threadid), uintptr(continuestatus)) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -101,7 +130,7 @@ func _WriteProcessMemory(process syscall.Handle, baseaddr uintptr, buffer *byte, r1, _, e1 := syscall.Syscall6(procWriteProcessMemory.Addr(), 5, uintptr(process), uintptr(baseaddr), uintptr(unsafe.Pointer(buffer)), uintptr(size), uintptr(unsafe.Pointer(byteswritten)), 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -113,7 +142,7 @@ func _ReadProcessMemory(process syscall.Handle, baseaddr uintptr, buffer *byte, r1, _, e1 := syscall.Syscall6(procReadProcessMemory.Addr(), 5, uintptr(process), uintptr(baseaddr), uintptr(unsafe.Pointer(buffer)), uintptr(size), uintptr(unsafe.Pointer(bytesread)), 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -125,7 +154,7 @@ func _DebugBreakProcess(process syscall.Handle) (err error) { r1, _, e1 := syscall.Syscall(procDebugBreakProcess.Addr(), 1, uintptr(process), 0, 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -137,7 +166,7 @@ func _WaitForDebugEvent(debugevent *_DEBUG_EVENT, milliseconds uint32) (err erro r1, _, e1 := syscall.Syscall(procWaitForDebugEvent.Addr(), 2, uintptr(unsafe.Pointer(debugevent)), uintptr(milliseconds), 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -149,7 +178,7 @@ func _DebugActiveProcess(processid uint32) (err error) { r1, _, e1 := syscall.Syscall(procDebugActiveProcess.Addr(), 1, uintptr(processid), 0, 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -161,7 +190,7 @@ func _DebugActiveProcessStop(processid uint32) (err error) { r1, _, e1 := syscall.Syscall(procDebugActiveProcessStop.Addr(), 1, uintptr(processid), 0, 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } @@ -173,10 +202,22 @@ func _QueryFullProcessImageName(process syscall.Handle, flags uint32, exename *u r1, _, e1 := syscall.Syscall6(procQueryFullProcessImageNameW.Addr(), 4, uintptr(process), uintptr(flags), uintptr(unsafe.Pointer(exename)), uintptr(unsafe.Pointer(size)), 0, 0) if r1 == 0 { if e1 != 0 { - err = error(e1) + err = errnoErr(e1) } else { err = syscall.EINVAL } } return } + +func _VirtualQueryEx(process syscall.Handle, addr uintptr, buffer *_MEMORY_BASIC_INFORMATION, length uintptr) (lengthOut uintptr) { + r0, _, _ := syscall.Syscall6(procVirtualQueryEx.Addr(), 4, uintptr(process), uintptr(addr), uintptr(unsafe.Pointer(buffer)), uintptr(length), 0, 0) + lengthOut = uintptr(r0) + return +} + +func _IsWow64Process(process syscall.Handle, wow64process *uint32) (ok uint32) { + r0, _, _ := syscall.Syscall(procIsWow64Process.Addr(), 2, uintptr(process), uintptr(unsafe.Pointer(wow64process)), 0) + ok = uint32(r0) + return +} diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 8950adb1..4e7f0559 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -23,12 +23,15 @@ import ( "time" "github.com/go-delve/delve/pkg/dwarf/frame" + "github.com/go-delve/delve/pkg/dwarf/op" "github.com/go-delve/delve/pkg/goversion" "github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/proc" + "github.com/go-delve/delve/pkg/proc/core" "github.com/go-delve/delve/pkg/proc/gdbserial" "github.com/go-delve/delve/pkg/proc/native" protest "github.com/go-delve/delve/pkg/proc/test" + "github.com/go-delve/delve/service/api" ) var normalLoadConfig = proc.LoadConfig{true, 1, 64, 64, -1, 0} @@ -1302,9 +1305,23 @@ func TestFrameEvaluation(t *testing.T) { found[vval] = true } + firsterr := false + if goversion.VersionAfterOrEqual(runtime.Version(), 1, 14) { + // We try to make sure that all goroutines are stopped at a sensible place + // before reading their stacktrace, but due to the nature of the test + // program there is no guarantee that we always find them in a reasonable + // state. + // Asynchronous preemption in Go 1.14 exacerbates this problem, to avoid + // unnecessary flakiness allow a single goroutine to be in a bad state. + firsterr = true + } for i := range found { if !found[i] { - t.Fatalf("Goroutine %d not found\n", i) + if firsterr { + firsterr = false + } else { + t.Fatalf("Goroutine %d not found\n", i) + } } } @@ -4993,3 +5010,163 @@ func TestIssue2319(t *testing.T) { bi := proc.NewBinaryInfo("linux", "amd64") assertNoError(bi.LoadBinaryInfo(fixture.Path, 0, nil), t, "LoadBinaryInfo") } + +func TestDump(t *testing.T) { + if runtime.GOOS == "freebsd" || (runtime.GOOS == "darwin" && testBackend == "native") { + t.Skip("not supported") + } + + convertRegisters := func(arch *proc.Arch, dregs op.DwarfRegisters) string { + dregs.Reg(^uint64(0)) + buf := new(bytes.Buffer) + for i := 0; i < dregs.CurrentSize(); i++ { + reg := dregs.Reg(uint64(i)) + if reg == nil { + continue + } + name, _, repr := arch.DwarfRegisterToString(i, reg) + fmt.Fprintf(buf, " %s=%s", name, repr) + } + return buf.String() + } + + convertThread := func(thread proc.Thread) string { + regs, err := thread.Registers() + assertNoError(err, t, fmt.Sprintf("Thread registers %d", thread.ThreadID())) + arch := thread.BinInfo().Arch + dregs := arch.RegistersToDwarfRegisters(0, regs) + return fmt.Sprintf("%08d %s", thread.ThreadID(), convertRegisters(arch, dregs)) + } + + convertThreads := func(threads []proc.Thread) []string { + r := make([]string, len(threads)) + for i := range threads { + r[i] = convertThread(threads[i]) + } + sort.Strings(r) + return r + } + + convertGoroutine := func(g *proc.G) string { + threadID := 0 + if g.Thread != nil { + threadID = g.Thread.ThreadID() + } + return fmt.Sprintf("%d pc=%#x sp=%#x bp=%#x lr=%#x gopc=%#x startpc=%#x systemstack=%v thread=%d", g.ID, g.PC, g.SP, g.BP, g.LR, g.GoPC, g.StartPC, g.SystemStack, threadID) + } + + convertFrame := func(arch *proc.Arch, frame *proc.Stackframe) string { + return fmt.Sprintf("currentPC=%#x callPC=%#x frameOff=%#x\n", frame.Current.PC, frame.Call.PC, frame.FrameOffset()) + } + + makeDump := func(p *proc.Target, corePath, exePath string, flags proc.DumpFlags) *proc.Target { + fh, err := os.Create(corePath) + assertNoError(err, t, "Create()") + var state proc.DumpState + p.Dump(fh, flags, &state) + assertNoError(state.Err, t, "Dump()") + if state.ThreadsDone != state.ThreadsTotal || state.MemDone != state.MemTotal || !state.AllDone || state.Dumping || state.Canceled { + t.Fatalf("bad DumpState %#v", &state) + } + c, err := core.OpenCore(corePath, exePath, nil) + assertNoError(err, t, "OpenCore()") + return c + } + + testDump := func(p, c *proc.Target) { + if p.Pid() != c.Pid() { + t.Errorf("Pid mismatch %x %x", p.Pid(), c.Pid()) + } + + threads := convertThreads(p.ThreadList()) + cthreads := convertThreads(c.ThreadList()) + + if len(threads) != len(cthreads) { + t.Errorf("Thread number mismatch %d %d", len(threads), len(cthreads)) + } + + for i := range threads { + if threads[i] != cthreads[i] { + t.Errorf("Thread mismatch\nlive:\t%s\ncore:\t%s", threads[i], cthreads[i]) + } + } + + gos, _, err := proc.GoroutinesInfo(p, 0, 0) + assertNoError(err, t, "GoroutinesInfo() - live process") + cgos, _, err := proc.GoroutinesInfo(c, 0, 0) + assertNoError(err, t, "GoroutinesInfo() - core dump") + + if len(gos) != len(cgos) { + t.Errorf("Goroutine number mismatch %d %d", len(gos), len(cgos)) + } + + var scope, cscope *proc.EvalScope + + for i := range gos { + if convertGoroutine(gos[i]) != convertGoroutine(cgos[i]) { + t.Errorf("Goroutine mismatch\nlive:\t%s\ncore:\t%s", convertGoroutine(gos[i]), convertGoroutine(cgos[i])) + } + + frames, err := gos[i].Stacktrace(20, 0) + assertNoError(err, t, fmt.Sprintf("Stacktrace for goroutine %d - live process", gos[i].ID)) + cframes, err := cgos[i].Stacktrace(20, 0) + assertNoError(err, t, fmt.Sprintf("Stacktrace for goroutine %d - core dump", gos[i].ID)) + + if len(frames) != len(cframes) { + t.Errorf("Frame number mismatch for goroutine %d: %d %d", gos[i].ID, len(frames), len(cframes)) + } + + for j := range frames { + if convertFrame(p.BinInfo().Arch, &frames[j]) != convertFrame(p.BinInfo().Arch, &cframes[j]) { + t.Errorf("Frame mismatch %d.%d\nlive:\t%s\ncore:\t%s", gos[i].ID, j, convertFrame(p.BinInfo().Arch, &frames[j]), convertFrame(p.BinInfo().Arch, &cframes[j])) + } + if frames[j].Call.Fn != nil && frames[j].Call.Fn.Name == "main.main" { + scope = proc.FrameToScope(p.BinInfo(), p.Memory(), gos[i], frames[j:]...) + cscope = proc.FrameToScope(c.BinInfo(), c.Memory(), cgos[i], cframes[j:]...) + } + } + } + + vars, err := scope.LocalVariables(normalLoadConfig) + assertNoError(err, t, "LocalVariables - live process") + cvars, err := cscope.LocalVariables(normalLoadConfig) + assertNoError(err, t, "LocalVariables - core dump") + + if len(vars) != len(cvars) { + t.Errorf("Variable number mismatch %d %d", len(vars), len(cvars)) + } + + for i := range vars { + varstr := vars[i].Name + "=" + api.ConvertVar(vars[i]).SinglelineString() + cvarstr := cvars[i].Name + "=" + api.ConvertVar(cvars[i]).SinglelineString() + if strings.Contains(varstr, "(unreadable") { + // errors reading from unmapped memory differ between live process and core + continue + } + if varstr != cvarstr { + t.Errorf("Variable mismatch %s %s", varstr, cvarstr) + } + } + } + + withTestProcess("testvariables2", t, func(p *proc.Target, fixture protest.Fixture) { + assertNoError(p.Continue(), t, "Continue()") + corePath := filepath.Join(fixture.BuildDir, "coredump") + corePathPlatIndep := filepath.Join(fixture.BuildDir, "coredump-indep") + + t.Logf("testing normal dump") + + c := makeDump(p, corePath, fixture.Path, 0) + defer os.Remove(corePath) + testDump(p, c) + + if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" { + // No reason to do this test on other goos/goarch because they use the + // platform-independent format anyway. + t.Logf("testing platform-independent dump") + c2 := makeDump(p, corePathPlatIndep, fixture.Path, proc.DumpPlatformIndependent) + defer os.Remove(corePathPlatIndep) + testDump(p, c2) + } + }) +} diff --git a/pkg/proc/target.go b/pkg/proc/target.go index bef040e1..1bda30f3 100644 --- a/pkg/proc/target.go +++ b/pkg/proc/target.go @@ -41,6 +41,9 @@ type Target struct { // case only one will be reported. StopReason StopReason + // CanDump is true if core dumping is supported. + CanDump bool + // currentThread is the thread that will be used by next/step/stepout and to evaluate variables if no goroutine is selected. currentThread Thread @@ -120,6 +123,7 @@ type NewTargetConfig struct { DebugInfoDirs []string // Directories to search for split debug info DisableAsyncPreempt bool // Go 1.14 asynchronous preemption should be disabled StopReason StopReason // Initial stop reason + CanDump bool // Can create core dumps (must implement ProcessInternal.MemoryMap) } // DisableAsyncPreemptEnv returns a process environment (like os.Environ) @@ -159,6 +163,7 @@ func NewTarget(p Process, currentThread Thread, cfg NewTargetConfig) (*Target, e fncallForG: make(map[int]*callInjection), StopReason: cfg.StopReason, currentThread: currentThread, + CanDump: cfg.CanDump, } g, _ := GetG(currentThread) diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 81db7891..779cb009 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -410,6 +410,12 @@ For example: The '-a' option adds an expression to the list of expression printed every time the program stops. The '-d' option removes the specified expression from the list. If display is called without arguments it will print the value of all expression in the list.`}, + + {aliases: []string{"dump"}, cmdFn: dump, helpMsg: `Creates a core dump from the current process state + + dump + +The core dump is always written in ELF, even on systems (windows, macOS) where this is not customary. For environments other than linux/amd64 threads and registers are dumped in a format that only Delve can read back.`}, } addrecorded := client == nil @@ -2633,6 +2639,33 @@ func display(t *Term, ctx callContext, args string) error { return nil } +func dump(t *Term, ctx callContext, args string) error { + dumpState, err := t.client.CoreDumpStart(args) + if err != nil { + return err + } + for { + if dumpState.ThreadsDone != dumpState.ThreadsTotal { + fmt.Printf("\rDumping threads %d / %d...", dumpState.ThreadsDone, dumpState.ThreadsTotal) + } else { + fmt.Printf("\rDumping memory %d / %d...", dumpState.MemDone, dumpState.MemTotal) + } + if !dumpState.Dumping { + break + } + dumpState = t.client.CoreDumpWait(1000) + } + fmt.Printf("\n") + if dumpState.Err != "" { + fmt.Printf("error dumping: %s\n", dumpState.Err) + } else if !dumpState.AllDone { + fmt.Printf("canceled\n") + } else if dumpState.MemDone != dumpState.MemTotal { + fmt.Printf("Core dump could be incomplete\n") + } + return nil +} + func formatBreakpointName(bp *api.Breakpoint, upcase bool) string { thing := "breakpoint" if bp.Tracepoint { diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index b6855382..cb606bf8 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -399,6 +399,78 @@ func (env *Env) starlarkPredeclare() starlark.StringDict { } return env.interfaceToStarlarkValue(rpcRet), nil }) + r["dump_cancel"] = starlark.NewBuiltin("dump_cancel", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := isCancelled(thread); err != nil { + return starlark.None, decorateError(thread, err) + } + var rpcArgs rpc2.DumpCancelIn + var rpcRet rpc2.DumpCancelOut + err := env.ctx.Client().CallAPI("DumpCancel", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) + r["dump_start"] = starlark.NewBuiltin("dump_start", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := isCancelled(thread); err != nil { + return starlark.None, decorateError(thread, err) + } + var rpcArgs rpc2.DumpStartIn + var rpcRet rpc2.DumpStartOut + if len(args) > 0 && args[0] != starlark.None { + err := unmarshalStarlarkValue(args[0], &rpcArgs.Destination, "Destination") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + for _, kv := range kwargs { + var err error + switch kv[0].(starlark.String) { + case "Destination": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.Destination, "Destination") + default: + err = fmt.Errorf("unknown argument %q", kv[0]) + } + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + err := env.ctx.Client().CallAPI("DumpStart", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) + r["dump_wait"] = starlark.NewBuiltin("dump_wait", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + if err := isCancelled(thread); err != nil { + return starlark.None, decorateError(thread, err) + } + var rpcArgs rpc2.DumpWaitIn + var rpcRet rpc2.DumpWaitOut + if len(args) > 0 && args[0] != starlark.None { + err := unmarshalStarlarkValue(args[0], &rpcArgs.Wait, "Wait") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + for _, kv := range kwargs { + var err error + switch kv[0].(starlark.String) { + case "Wait": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.Wait, "Wait") + default: + err = fmt.Errorf("unknown argument %q", kv[0]) + } + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + err := env.ctx.Client().CallAPI("DumpWait", &rpcArgs, &rpcRet) + if err != nil { + return starlark.None, err + } + return env.interfaceToStarlarkValue(rpcRet), nil + }) r["eval"] = starlark.NewBuiltin("eval", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { if err := isCancelled(thread); err != nil { return starlark.None, decorateError(thread, err) diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go index e456a78e..fa00006d 100644 --- a/pkg/terminal/terminal.go +++ b/pkg/terminal/terminal.go @@ -149,6 +149,14 @@ func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) { } continue } + if err == nil && state.CoreDumping { + fmt.Printf("received SIGINT, stopping dump\n") + err := t.client.CoreDumpCancel() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + } + continue + } if multiClient { answer, err := t.line.Prompt("Would you like to [p]ause the target (returning to Delve's prompt) or [q]uit this client (leaving the target running) [p/q]? ") if err != nil { diff --git a/service/api/conversions.go b/service/api/conversions.go index 29a59f55..51b1c0c1 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -421,3 +421,20 @@ func ConvertRegisters(in *op.DwarfRegisters, dwarfRegisterToString func(int, *op func ConvertImage(image *proc.Image) Image { return Image{Path: image.Path, Address: image.StaticBase} } + +func ConvertDumpState(dumpState *proc.DumpState) *DumpState { + dumpState.Mutex.Lock() + defer dumpState.Mutex.Unlock() + r := &DumpState{ + Dumping: dumpState.Dumping, + AllDone: dumpState.AllDone, + ThreadsDone: dumpState.ThreadsDone, + ThreadsTotal: dumpState.ThreadsTotal, + MemDone: dumpState.MemDone, + MemTotal: dumpState.MemTotal, + } + if dumpState.Err != nil { + r.Err = dumpState.Err.Error() + } + return r +} diff --git a/service/api/types.go b/service/api/types.go index 7e4bdbd4..13b9218e 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -24,6 +24,8 @@ type DebuggerState struct { // sending a StopRecording request will halt the recording, every other // request will block until the process has been recorded. Recording bool + // Core dumping currently in progress. + CoreDumping bool // CurrentThread is the currently selected debugger thread. CurrentThread *Thread `json:"currentThread,omitempty"` // SelectedGoroutine is the currently selected goroutine @@ -544,3 +546,14 @@ type PackageBuildInfo struct { DirectoryPath string Files []string } + +// DumpState describes the state of a core dump in progress +type DumpState struct { + Dumping bool + AllDone bool + + ThreadsDone, ThreadsTotal int + MemDone, MemTotal uint64 + + Err string +} diff --git a/service/client.go b/service/client.go index 296317c1..62e0be0a 100644 --- a/service/client.go +++ b/service/client.go @@ -165,6 +165,13 @@ type Client interface { // StopRecording stops a recording if one is in progress. StopRecording() error + // CoreDumpStart starts creating a core dump to the specified file + CoreDumpStart(dest string) (api.DumpState, error) + // CoreDumpWait waits for the core dump to finish, or for the specified amount of milliseconds + CoreDumpWait(msec int) api.DumpState + // CoreDumpCancel cancels a core dump in progress + CoreDumpCancel() error + // Disconnect closes the connection to the server without sending a Detach request first. // If cont is true a continue command will be sent instead. Disconnect(cont bool) error diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index f7ae53ad..6994f363 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -37,6 +37,12 @@ var ( // ErrNotRecording is returned when StopRecording is called while the // debugger is not recording the target. ErrNotRecording = errors.New("debugger is not recording") + + // ErrCoreDumpInProgress is returned when a core dump is already in progress. + ErrCoreDumpInProgress = errors.New("core dump in progress") + + // ErrCoreDumpNotSupported is returned when core dumping is not supported + ErrCoreDumpNotSupported = errors.New("core dumping not supported") ) // Debugger service. @@ -62,6 +68,8 @@ type Debugger struct { stopRecording func() error recordMutex sync.Mutex + + dumpState proc.DumpState } type ExecuteKind int @@ -532,6 +540,12 @@ func (d *Debugger) State(nowait bool) (*api.DebuggerState, error) { return &api.DebuggerState{Recording: true}, nil } + d.dumpState.Mutex.Lock() + if d.dumpState.Dumping && nowait { + return &api.DebuggerState{CoreDumping: true}, nil + } + d.dumpState.Mutex.Unlock() + d.targetMutex.Lock() defer d.targetMutex.Unlock() return d.state(nil) @@ -1653,6 +1667,77 @@ func (d *Debugger) UnlockTarget() { d.targetMutex.Unlock() } +// DumpStart starts a core dump to dest. +func (d *Debugger) DumpStart(dest string) error { + d.targetMutex.Lock() + // targetMutex will only be unlocked when the dump is done + + if !d.target.CanDump { + d.targetMutex.Unlock() + return ErrCoreDumpNotSupported + } + + d.dumpState.Mutex.Lock() + defer d.dumpState.Mutex.Unlock() + + if d.dumpState.Dumping { + d.targetMutex.Unlock() + return ErrCoreDumpInProgress + } + + fh, err := os.Create(dest) + if err != nil { + d.targetMutex.Unlock() + return err + } + + d.dumpState.Dumping = true + d.dumpState.AllDone = false + d.dumpState.Canceled = false + d.dumpState.DoneChan = make(chan struct{}) + d.dumpState.ThreadsDone = 0 + d.dumpState.ThreadsTotal = 0 + d.dumpState.MemDone = 0 + d.dumpState.MemTotal = 0 + d.dumpState.Err = nil + go func() { + defer d.targetMutex.Unlock() + d.target.Dump(fh, 0, &d.dumpState) + }() + + return nil +} + +// DumpWait waits for the dump to finish, or for the duration of wait. +// Returns the state of the dump. +// If wait == 0 returns immediately. +func (d *Debugger) DumpWait(wait time.Duration) *proc.DumpState { + d.dumpState.Mutex.Lock() + if !d.dumpState.Dumping { + d.dumpState.Mutex.Unlock() + return &d.dumpState + } + d.dumpState.Mutex.Unlock() + + if wait > 0 { + alarm := time.After(wait) + select { + case <-alarm: + case <-d.dumpState.DoneChan: + } + } + + return &d.dumpState +} + +// DumpCancel canels a dump in progress +func (d *Debugger) DumpCancel() error { + d.dumpState.Mutex.Lock() + d.dumpState.Canceled = true + d.dumpState.Mutex.Unlock() + return nil +} + func go11DecodeErrorCheck(err error) error { if _, isdecodeerr := err.(dwarf.DecodeError); !isdecodeerr { return err diff --git a/service/rpc2/client.go b/service/rpc2/client.go index fc95a3c9..ba506161 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -455,6 +455,23 @@ func (c *RPCClient) StopRecording() error { return c.call("StopRecording", StopRecordingIn{}, &StopRecordingOut{}) } +func (c *RPCClient) CoreDumpStart(dest string) (api.DumpState, error) { + out := &DumpStartOut{} + err := c.call("DumpStart", DumpStartIn{Destination: dest}, out) + return out.State, err +} + +func (c *RPCClient) CoreDumpWait(msec int) api.DumpState { + out := &DumpWaitOut{} + _ = c.call("DumpWait", DumpWaitIn{Wait: msec}, out) + return out.State +} + +func (c *RPCClient) CoreDumpCancel() error { + out := &DumpCancelOut{} + return c.call("DumpCancel", DumpCancelIn{}, out) +} + func (c *RPCClient) call(method string, args, reply interface{}) error { return c.client.Call("RPCServer."+method, args, reply) } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index b467b828..a27eae0c 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -864,3 +864,48 @@ func (s *RPCServer) StopRecording(arg StopRecordingIn, cb service.RPCCallback) { } cb.Return(out, nil) } + +type DumpStartIn struct { + Destination string +} + +type DumpStartOut struct { + State api.DumpState +} + +// DumpStart starts a core dump to arg.Destination. +func (s *RPCServer) DumpStart(arg DumpStartIn, out *DumpStartOut) error { + err := s.debugger.DumpStart(arg.Destination) + if err != nil { + return err + } + out.State = *api.ConvertDumpState(s.debugger.DumpWait(0)) + return nil +} + +type DumpWaitIn struct { + Wait int +} + +type DumpWaitOut struct { + State api.DumpState +} + +// DumpWait waits for the core dump to finish or for arg.Wait milliseconds. +// Wait == 0 means return immediately. +// Returns the core dump status +func (s *RPCServer) DumpWait(arg DumpWaitIn, out *DumpWaitOut) error { + out.State = *api.ConvertDumpState(s.debugger.DumpWait(time.Duration(arg.Wait) * time.Millisecond)) + return nil +} + +type DumpCancelIn struct { +} + +type DumpCancelOut struct { +} + +// DumpCancel cancels the core dump. +func (s *RPCServer) DumpCancel(arg DumpCancelIn, out *DumpCancelOut) error { + return s.debugger.DumpCancel() +} diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 4ac36268..64ec5dc9 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -1548,6 +1548,8 @@ func TestPluginVariables(t *testing.T) { } func TestCgoEval(t *testing.T) { + protest.MustHaveCgo(t) + testcases := []varTest{ {"s", true, `"a string"`, `"a string"`, "*char", nil}, {"longstring", true, `"averylongstring0123456789a0123456789b0123456789c0123456789d01234...+1 more"`, `"averylongstring0123456789a0123456789b0123456789c0123456789d01234...+1 more"`, "*const char", nil},