go-delve / delve

Delve is a debugger for the Go programming language.
MIT License
22.38k stars 2.13k forks source link

Feature request: dump byte slice memory contents into file #3706

Open stapelberg opened 2 months ago

stapelberg commented 2 months ago

[Using dlv 1.22.1]

Recently, I had to debug a number of failing Go batch pipelines. Because of how these pipelines are structured, it is prohibitively complicated to run them locally, so all I had was a core dump.

With a running program, I would expect call os.WriteFile("/tmp/b.bin", b, 0600) to write the contents of the byte slice b to /tmp/b.bin. Unfortunately this didn’t actually work in my test, but that’s probably worth a separate issue:

(dlv) call os.WriteFile("/tmp/b.bin", b, 0644)
> [runtime-fatal-throw] runtime.fatalsignal() third_party/go/gc/src/runtime/signal_unix.go:795 (hits goroutine(1):1 total:1) (PC: 0x558a764f39ae)
Warning: debugging optimized function
Command failed: could not find symbol value for os

Anyway, because I only have a core dump, I needed to find another way to dump the contents.

Luckily, I found https://www.youtube.com/watch?v=sV5f1dF8ZU0, which explains how to display the Go slice header:

(dlv) x -size 8 -count 3 -x &b
0xc004406e30:   0x000000c005c80000   0x0000000000001d6c   0x000000000002ca2b

With the memory address and element count, we can try to display the slice contents:

(dlv) x -size 1 -count 0x1d6c 0x000000c005c80000
Command failed: count/len must be a positive integer
(dlv) x -size 1 -count 7532 0x000000c005c80000
Command failed: read memory range (count*size) must be less than or equal to 1000 bytes

I removed the 1000 bytes check from the code:

diff --git i/pkg/terminal/command.go w/pkg/terminal/command.go
index dc629437..4c0c19d6 100644
--- i/pkg/terminal/command.go
+++ w/pkg/terminal/command.go
@@ -2131,9 +2131,9 @@ loop:
    }

    // TODO, maybe configured by user.
-   if count*size > 1000 {
-       return fmt.Errorf("read memory range (count*size) must be less than or equal to 1000 bytes")
-   }
+   // if count*size > 1000 {
+   //  return fmt.Errorf("read memory range (count*size) must be less than or equal to 1000 bytes")
+   // }

    if len(args) == 0 {
        return fmt.Errorf("no address specified")

diff --git i/service/rpc2/server.go w/service/rpc2/server.go
index f806873b..cbcd4d18 100644
--- i/service/rpc2/server.go
+++ w/service/rpc2/server.go
@@ -977,9 +977,9 @@ type ExaminedMemoryOut struct {
 }

 func (s *RPCServer) ExamineMemory(arg ExamineMemoryIn, out *ExaminedMemoryOut) error {
-   if arg.Length > 1000 {
-       return fmt.Errorf("len must be less than or equal to 1000")
-   }
+   // if arg.Length > 1000 {
+   //  return fmt.Errorf("len must be less than or equal to 1000")
+   // }
    Mem, err := s.debugger.ExamineMemory(arg.Address, arg.Length)
    if err != nil {
        return err

Now, dlv lets me print the memory contents, which I can redirect into a file using the transcript command:

(dlv) transcript /tmp/b.hex
(dlv) x -size 1 -count 7532 0xc005c80000
0xc005d80000:   0x68  0x65   0x6c   0x6c   0x6f   0x20   0x63   0x75   
[…]
(dlv) transcript -off

However, dlv’s current format is not compatible with xxd -r, so would require further work to turn from hex back to binary.

I found it easier to modify dlv’s printing:

diff --git i/service/api/prettyprint.go w/service/api/prettyprint.go
index 001ab17f..8d98d7e9 100644
--- i/service/api/prettyprint.go
+++ w/service/api/prettyprint.go
@@ -511,12 +511,12 @@ func PrettyExamineMemory(address uintptr, memArea []byte, isLittleEndian bool, f
        cols = 8
        colFormat = fmt.Sprintf("%%0%dd", colBytes*3)
    case 'x':
-       cols = 8
-       colFormat = fmt.Sprintf("0x%%0%dx", colBytes*2) // Always keep one leading '0x' for hex.
+       cols = 16
+       colFormat = "%02x"
    default:
        return fmt.Sprintf("not supported format %q\n", string(format))
    }
-   colFormat += "\t"
+   colFormat += " "

    l := len(memArea)
    rows := l / (cols * colBytes)
@@ -528,13 +528,14 @@ func PrettyExamineMemory(address uintptr, memArea []byte, isLittleEndian bool, f
    if l != 0 {
        addrLen = len(fmt.Sprintf("%x", uint64(address)+uint64(l)))
    }
-   addrFmt = "0x%0" + strconv.Itoa(addrLen) + "x:\t"
+   addrFmt = "%0" + strconv.Itoa(addrLen) + "x: "

    var b strings.Builder
    w := tabwriter.NewWriter(&b, 0, 0, 3, ' ', 0)

+   startAddress := address
    for i := 0; i < rows; i++ {
-       fmt.Fprintf(w, addrFmt, address)
+       fmt.Fprintf(w, addrFmt, address-startAddress)

        for j := 0; j < cols; j++ {
            offset := i*(cols*colBytes) + j*colBytes

With this modification, dlv will print like so:

(dlv) transcript /tmp/b.hex
(dlv) x -size 1 -count 7532 0xc005c80000
0000000000: 68 65 6c 6c 6f 20 63 75  72 69 6f 75 73 20 67 6f
[…]
(dlv) transcript -off

…and then I can use xxd -r /tmp/b.hex > /tmp/b.bin (after deleting the command itself from /tmp/b.hex).


My feature request is for a command built into delve which can dump memory into a file. It would be tremendously useful and is rather hard to accomplish today (see above).

Thanks