Go - 将字节从 uintptr 复制到偏移处的字节切片

Go - Copy bytes from uintptr to byte slice at offset

提问人:pepperoni 提问时间:8/22/2023 更新时间:8/23/2023 访问量:160

问:

type MINIDUMP_IO_CALLBACK struct {
    Handle      uintptr //not relevant
    Offset      uint64  //The offset for the write operation from the start of the minidump data.
    Buffer      uintptr //A pointer to a buffer that contains the data to be written.
    BufferBytes uint32  //The size of the data buffer, in bytes
}

// https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_io_callback

var dumpBuffer []byte //all bytes should be copied here

func copyDumpBytes(ioStruct *MINIDUMP_IO_CALLBACK) {
    size := unsafe.Sizeof(ioStruct.Buffer)
    dumpData := make([]byte, size)
    binary.LittleEndian.PutUint64(dumpData, uint64(ioStruct.Buffer)) // getting byte slice from pointer
    requiredSize := ioStruct.Offset + uint64(ioStruct.BufferBytes) // calculating required size to write it at the required offset
    if requiredSize > uint64(len(dumpBuffer)) {
        padding := make([]byte, requiredSize)
        dumpBuffer = append(dumpBuffer, padding...) // allocating new space in buffer, if existing is not enough
    }
    copy(dumpBuffer[ioStruct.Offset:], dumpData) //copying bytes itself into the buffer at the required offset
}

copyDumpBytes() 函数在执行过程中由回调函数多次调用。它需要将接收到的所有数据存储到 dumpBuffer 字节片中。

字节切片应该表示一个文件(Windows 进程小型转储文件),但当存储到文件中并使用相关程序 (WinDbg) 打开时,它无法识别文件结构。

ioutil.WriteFile("test.dmp", dumpBuffer, 0644)

我的猜测是我在复制字节时搞砸了这段代码的某个地方。

Windows Go WinAPI SDK 切片

评论

0赞 Torrecto - MSFT 8/22/2023
您使用的是哪个 IDE?VC6.0 的 Link 选项需要将 /pdbtype:sept 更改为 /pdbtype:con,否则生成的 pdb 文件将不包含自定义结构、类等信息。
0赞 Torrecto - MSFT 8/22/2023
能否提供一个最小的演示来生成 WinDbg 无法打开的转储文件?
2赞 Luke 8/22/2023
当你这样做时,你会得到成员的大小,这将是 4 或 8 个字节。我想你想要。同样,您也希望将 替换为 .unsafe.Sizeof(ioStruct.Buffer)BufferioStruct.BufferBytesbinary.LittleEndian.PutUint64copy

答:

0赞 kostix 8/22/2023 #1

这是一个不完整的答案(正如其结尾所解释的那样),但在这一点上,我想说你的方法的主要问题是

size := unsafe.Sizeof(ioStruct.Buffer)
dumpData := make([]byte, size)

使用此代码,您可以获得指针大小的变量的大小,该变量在给定平台上是恒定的 - 在 i386 上为 32 位,在 amd64 上为 64 位(64 位 Windows 是 LLP64 平台)。ioStruct.Buffer

相反,该类型的字段是您应该复制的内存块的指针(地址),并且该字段包含该字段指向的数据大小(以字节为单位)。BufferuintptrBufferBytesBuffer

因此,为了正确复制该数据,您应该从原始内存指针中生成一个适当的 Go 切片。

在 Go ≥ 1.17 中,您可以使用 unsafe。切片¹:

func copyDumpBytes(iocb *MINIDUMP_IO_CALLBACK) {
    src := unsafe.Slice((*byte)(unsafe.Pointer(iocb.Buffer)), iocb.BufferBytes)

    dumpBuffer = append(dumpBuffer, src...)
}

这里:

  1. 表达式生成一个“适当的”指针,该指针可以参与对指针进行操作的 Go 表达式的某些位置(不能 - 它是一个整数类型)。unsafe.Pointer(iocb.Buffer)uintptr
  2. 然后将结果类型转换为生成指向字节的指针(实际上 - 指向零到任意数量的相邻字节的内存块)。*byte
  3. 最后,对内部函数的调用会产生一个适当的 Go 切片,其后备数组是指向 的内存块,其中包含元素(字节)。unsafe.Slice[]byteBufferBufferBytes

然后,您可以在所有常用的 Go 表达式和函数调用中自由使用生成的切片,包括 .append()

⚠ 请注意,虽然您已经从系统传递给我们的内存中创建了一个 Go 切片,但您并不拥有该内存,因此您必须确保没有指向该内存的引用(指针),包括我们的切片,在处理程序的代码中幸存下来,否则您将遇到一个经典的“释放后使用”问题。 复制源的内存,所以在我的玩具示例中不存在这个问题。append()


一个让我担心的问题是那个被记录为的成员Offset

从小型转储数据开始的写入操作的偏移量。

这可能意味着无法保证回调将“以串行方式”调用,也就是说,数据片段按顺序跟随,中间没有间隙。相反,手册页的措辞表明,可以使用要写入生成的小型转储文件的随机位置的数据块来调用回调。

为了理解我的意思,请考虑 Win32 API 函数向我们传递生成的小型转储的数据已决定将其分解为四个块,如下所示:

|offset_1            |offset_2          |offset_3             |offset_4
+--------------------+------------------+---------------------+---------------------+
|          1         |        2         |          3          |          4          |
+--------------------+------------------+---------------------+---------------------+
 <-    size_1      -> <-    size_2    -> <-      size_3     -> <-      size_4     ->

我想说的是,文档不保证您将按照与上面块标记的顺序相同的顺序收到这四个回调;取而代之的是,文档的表述方式允许以任何顺序使用块调用回调,例如,如下所示:

  1. offset_4,size_4;
  2. offset_2,size_2;
  3. offset_1,size_1;
  4. offset_3, .size_3

如果这是真的,你不能忽略和依赖 -ing 数据,而应该以某种方式预分配生成的缓冲区并使用生成的数据“修补它”,或者将数据直接写入文件(每次在写入下一个接收的块之前使用 seek 操作定位文件指针)。Offsetappend

无论如何,这是一个完全不同的问题,与围棋无关。


¹ 对于旧版本的 Go,您通常会使用一个相当标准的技巧来做同样的事情:

func copyDumpBytes(iocb *MINIDUMP_IO_CALLBACK) {
    src := (*[1<<31 - 1]byte)(unsafe.Pointer(iocb.Buffer))[:iocb.BufferBytes]

    dumpBuffer = append(dumpBuffer, src...)
}

让我们分解一下:

  1. 如上所述,该表达式从类型化值中生成一个“正确的”Go 指针。unsafe.Pointer(iocb.Buffer)uintptr
  2. 然后,生成的指针被类型转换为类型,该类型是指向()大小的字节数组的指针(这是Go数组在i386架构上可以具有的最大大小)。
    这两个表达式的结果是指向字节数组的指针。它的长度是假的(“无限”),但这在表达式的最后一部分处理。
    *[1<<31 - 1]byte*1<<32 - 1
  3. 这是一个切片表达式,它从源字节数组中生成一个字节切片 ,并使切片引用数组的元素,从索引 0 处的元素到索引处的元素,包括索引处的元素。[:iocb.BufferBytes][]byteiocb.BufferSize-1