Programmer

Will Change The World

Go读取任意内存地址的任意字节数据

在介绍读取方法之前,首先介绍两种类型:unsafe.Pointer 和uintptr。

unsafe.Pointer代表的是任意类型的指针,它支持四种特有的操作:

  1. 任意类型的指针变量都可以转换为unsafe.Pointer
  2. unsafe.Pointer可以转换为任意类型的指针变量
  3. uintptr可以转换为unsafe.Pointer
  4. unsafe.Pointer可以转换为uintptr

我们知道在Go中,不同类型的指针是不可以相互转换的,但是根据以上的特性,就可以借助unsafe.Pointer作为跳板实现不同类型的指针之间的相互转换

同时,Go中的指针是不能进行运算的,包括unsafe.Pointer,但是uintptr是可以的,而uintptr和unsafe.Pointer可以互相转换,因此可以用来实现指针的运算

接下来介绍一些变量及其在内存中的存储方法。

例如以下结构:

type T struct {
	F1 int32
	F2 rune
	F3 int64
	F4 *string
}

var t T

则变量t在内存中的布局可见下图:

《Go读取任意内存地址的任意字节数据》

其中变量t的指针&t指向的是t在内存中的起始地址。字段F1为int32类型,占4个字节;F2为rune,是int32的别名,也占4个字节;F3为int64,占用8个字节,最后是F4是*string类型,也是个指针,所以也占用8个字节。

在内存存储中,是没有变量类型信息的,其内部存放的内容到底如何处理,由变量的类型来决定。比如说上图中的F1字段,假设存储的内容为0x01,作为int32值解析时为1,作为bool值解析时就为true。也就是说,只要我知道内存地址,然后任意给定一个类型,就能得到这个地址里的内容作为这个类型处理时得到的值。

func main() {
	t := T{
		F1: 1,
		F2: 2,
	}
	println(t.F1)

	b := *(*bool)(unsafe.Pointer(&t))
	println(b)

}
// output:
1
true

既然任意类型都可以,那么我们可以以字节数组类型来解析,这样就可以拿到内存中的每个字节的数据了。

func main() {
	t := T{
		F1: 1,
		F2: 2,
	}
	fmt.Println(t.F1)

	b := *(*bool)(unsafe.Pointer(&t))
	fmt.Println(b)

	var arr [4]byte
	arr = *(*[4]byte)(unsafe.Pointer(&t))
	fmt.Println(arr)
}
// output:
1
true
[1 0 0 0]

由上面图中可以看到,F2字段的地址起始是在F1字段地址(也即变量t的起始地址)后偏移4个字节,那么可以通过指针运算的方式得到F2字段的地址及内部存储的内容:

func main() {
	t := T{
		F1: 1,
		F2: 2,
	}

	// 此处只作为演示,用法有不妥的地方
	// 具体正确用法请见unsafe.Pointer的源码注释
	p2 := uintptr(unsafe.Pointer(&t)) + 4

	fmt.Println(*(*[4]byte)(unsafe.Pointer(p2)))
}

// output:
[2 0 0 0]

uintptr其实就是个整数,所以我们可以实现以下的方法来读取任意地址(当然地址是否合法另说)的4个字节的数据:

func readMemory(addr uintptr) [4]byte {
	return *(*[4]byte)(unsafe.Pointer(addr))
}

现在我们已经实现了读取任意地址的4个字节的数据,那有什么办法来读取任意字节的数据呢?Go中的数组结构长度是固定的,并且无法通过变量来指定长度。但是我们有切片(slice)啊!

都知道切片的底层结构是由数据地址、长度、容量三个字段组成的结构体,其与reflect.SliceHeader保持一致,那么我们只要将上面代码做如下改动即可:

func readMemory(addr uintptr, size int) []byte {
	return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
		Data: addr,
		Len:  size,
		Cap:  size,
	}))
}

这样,就实现了读取任意内存地址任意字节数据的功能。刚才只是读了单个字段、4个字节的数据,现在可以尝试直接读取前两个字段总共8个字节的数据了:

func main() {
	t := T{
		F1: 1,
		F2: 2,
	}

	fmt.Println(readMemory(uintptr(unsafe.Pointer(&t)), 8))
}
// output:
[1 0 0 0 2 0 0 0]

完美!

点赞

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注