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

kd3sttzy  于 2023-09-28  发布在  Go
关注(0)|答案(1)|浏览(129)
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()函数在执行过程中被callback函数多次 * 调用。它需要将接收到的所有数据存储到dumpBuffer**字节片中。

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

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

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

83qze16e

83qze16e1#

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

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

通过这段代码,您可以获得指针大小的变量ioStruct.Buffer的大小,在给定的平台上 * 是常量 *-i386上是32位,amd 64上是64位(64位Windows是LLP 64平台)。
相反,uintptr类型的Buffer字段是您应该复制的内存块的 * 指针 *(地址),BufferBytes字段包含Buffer字段所指向的数据大小(以字节为单位)。
所以,为了正确地复制数据,你应该从原始内存指针中产生一个正确的Go切片。
当Go ≥ 1.17时,您可以使用unsafe.Slice来实现¹:

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

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

这里:
1.表达式unsafe.Pointer(iocb.Buffer)产生了一个“适当的”指针,它可以参与Go表达式对指针的某些操作(uintptr不能-它是一个整数类型)。
1.然后将结果类型转换为*byte,以生成指向byte的指针(实际上是指向零到任意数量相邻字节的内存块)。
1.最后,对intrinsic functionunsafe.Slice的调用产生一个正确的Go切片[]byte,其支持数组是Buffer指向的内存块,并且包含BufferBytes元素(字节)。
然后,你可以在所有常用的Go表达式和函数调用中自由地使用结果切片,包括append()
请注意,虽然你已经从系统传递给我们的内存中创建了一个Go切片,但你并不拥有该内存,所以你必须确保没有对该内存的引用(指针),包括我们的切片,在处理程序的代码中生存下来,否则你会遇到一个经典的“释放后使用”问题。append()复制源的内存,因此在我的示例中不存在这个问题。
一个让我担心的问题是,Offset成员被记录为
从小型转储数据开始的写入操作偏移量。
它 * 可能 * 意味着不能保证回调将被“以串行方式”调用-也就是说,数据片段顺序地彼此跟随,之间没有间隙。相反,手册页面的措辞表明,可以调用回调,其中数据块将被写入结果minidump文件的 * 随机 * 位置。
为了理解我的意思,考虑Win32 API函数将结果minidump的数据传递给我们,并决定将其分为四个块,如下所示:

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

我要说的是,文档并不能保证你会按照上面块的标记顺序收到这四个回调;相反,文档是以一种允许以任何顺序调用回调的方式制定的,比如说,像这样:

  1. offset_4size_4 ;
  2. offset_2size_2 ;
  3. offset_1size_1 ;
  4. offset_3size_3
    如果这是真的,你就不能忽略Offset而依赖append-ing数据,而是应该以某种方式预分配结果缓冲区并用结果数据“修补”,或者直接将数据写入文件(每次在写入下一个接收到的块之前使用seek操作定位文件指针)。
    不管怎么说,这是一个完全不同的问题,与围棋无关。
    ¹在旧版本的围棋中,你通常会使用一个相当标准的技巧来做同样的事情:
func copyDumpBytes(iocb *MINIDUMP_IO_CALLBACK) {
    src := (*[1<<31 - 1]byte)(unsafe.Pointer(iocb.Buffer))[:iocb.BufferBytes]

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

让我们来分解一下:
1.如上所述,表达式unsafe.Pointer(iocb.Buffer)uintptr类型的值中生成一个“正确的”Go指针。
1.然后,生成的指针被类型转换为类型*[1<<31 - 1]byte,这是一个指向(*)大小为1<<32 - 1(这是Go数组在i386架构上的最大大小)的字节数组的指针。
这两个表达式的结果是一个指向字节数组的指针。它的长度是假的(“无限”),但这是由表达式的最后一部分处理的。

  1. [:iocb.BufferBytes]是一个切片表达式,它从源字节数组中产生一个字节切片[]byte,并使切片引用数组中从索引0到索引iocb.BufferSize-1的元素。

相关问题