unsafe.Pointer其实就是类似C的void *,在golang中是用于各种指针相互转换的桥梁。uintptr是golang的内置类型,是能存储指针的整型,uintptr的底层类型是int,它和unsafe.Pointer可相互转换。uintptr和unsafe.Pointer的区别就是:unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象,uintptr类型的目标会被回收。golang的unsafe包很强大,基本上很少会去用它。它可以像C一样去操作内存,但由于golang不支持直接进行指针运算,所以用起来稍显麻烦。

切入正题。利用unsafe包,可操作私有变量(在golang中称为“未导出变量”,变量名以小写字母开始),下面是具体例子。

注:以下代码是在32位机器上编译的,如果是用64位机器编译,int64类型的alignof应该是8。

以下是v.go的代码:

package p

import (
"fmt"
) type V struct {
i int32
j int64
} func (this V) PutI() {
fmt.Printf("i=%d\n", this.i)
} func (this V) PutJ() {
fmt.Printf("j=%d\n", this.j)
}

 

意图很明显,我是想通过unsafe包来实现对V的成员i和j赋值,然后通过PutI()和PutJ()来打印观察输出结果。

以下是main.go源代码:

package main

import (
"poit/p"
"unsafe"
) func main() {
var v *p.V = new(p.V)
var i *int32 = (*int32)(unsafe.Pointer(v))
*i = int32(98)
var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))
*j = int64(763)
v.PutI()
v.PutJ()
}

  

当然会有些限制,比如需要知道结构体V的成员布局,要修改的成员大小以及成员的偏移量。我们的核心思想就是:结构体的成员在内存中的分配是一段连续的内存,结构体中第一个成员的地址就是这个结构体的地址,您也可以认为是相对于这个结构体偏移了0。相同的,这个结构体中的任一成员都可以相对于这个结构体的偏移来计算出它在内存中的绝对地址。

具体来讲解下main方法的实现:

var v *p.V = new(p.V)

  

new是golang的内置方法,用来分配一段内存(会按类型的零值来清零),并返回一个指针。所以v就是类型为p.V的一个指针。

var i *int32 = (*int32)(unsafe.Pointer(v))

  

将指针v转成通用指针,再转成int32指针。这里就看到了unsafe.Pointer的作用了,您不能直接将v转成int32类型的指针,那样将会panic。刚才说了v的地址其实就是它的第一个成员的地址,所以这个i就很显然指向了v的成员i,通过给i赋值就相当于给v.i赋值了,但是别忘了i只是个指针,要赋值得解引用。

*i = int32(98)

  

现在已经成功的改变了v的私有成员i的值,好开心^_^

但是对于v.j来说,怎么来得到它在内存中的地址呢?其实我们可以获取它相对于v的偏移量(unsafe.Sizeof可以为我们做这个事),但我上面的代码并没有这样去实现。各位别急,一步步来。

var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))

  

其实我们已经知道v是有两个成员的,包括i和j,并且在定义中,i位于j的前面,而i是int32类型,也就是说i占4个字节。所以j是相对于v偏移了4个字节。您可以用uintptr(4)或uintptr(unsafe.Sizeof(int32(0)))来做这个事。unsafe.Sizeof方法用来得到一个值应该占用多少个字节空间。注意这里跟C的用法不一样,C是直接传入类型,而golang是传入值。之所以转成uintptr类型是因为需要做指针运算。v的地址加上j相对于v的偏移地址,也就得到了v.j在内存中的绝对地址,别忘了j的类型是int64,所以现在的j就是一个指向v.j的指针,接下来给它赋值:

*j = int64(763)

  

好吧,现在貌视一切就绪了,来打印下:

v.PutI()
v.PutJ()

  

如果您看到了正确的输出,那恭喜您,您做到了!

但是,别忘了上面的代码其实是有一些问题的,您发现了吗?

在p目录下新建w.go文件,代码如下:

package p

import (
"fmt"
"unsafe"
) type W struct {
b byte
i int32
j int64
} func init() {
var w *W = new(W)
fmt.Printf("size=%d\n", unsafe.Sizeof(*w))
}

  

需要修改main.go的代码吗?不需要,我们只是来测试一下。w.go里定义了一个特殊方法init,它会在导入p包时自动执行,别忘了我们有在main.go里导入p包。每个包都可定义多个init方法,它们会在包被导入时自动执行(在执行main方法前被执行,通常用于初始化工作),但是,最好在一个包中只定义一个init方法,否则您或许会很难预期它的行为)。我们来看下它的输出:

size=16

  

等等,好像跟我们想像的不一致。来手动计算一下:b是byte类型,占1个字节;i是int32类型,占4个字节;j是int64类型,占8个字节,1+4+8=13。这是怎么回事呢?这是因为发生了对齐。在struct中,它的对齐值是它的成员中的最大对齐值。每个成员类型都有它的对齐值,可以用unsafe.Alignof方法来计算,比如unsafe.Alignof(w.b)就可以得到b在w中的对齐值。同理,我们可以计算出w.b的对齐值是1,w.i的对齐值是4,w.j的对齐值也是4。如果您认为w.j的对齐值是8那就错了,所以我们前面的代码能正确执行(试想一下,如果w.j的对齐值是8,那前面的赋值代码就有问题了。也就是说前面的赋值中,如果v.j的对齐值是8,那么v.i跟v.j之间应该有4个字节的填充。所以得到正确的对齐值是很重要的)。对齐值最小是1,这是因为存储单元是以字节为单位。所以b就在w的首地址,而i的对齐值是4,它的存储地址必须是4的倍数,因此,在b和i的中间有3个填充,同理j也需要对齐,但因为i和j之间不需要填充,所以w的Sizeof值应该是13+3=16。如果要通过unsafe来对w的三个私有成员赋值,b的赋值同前,而i的赋值则需要跳过3个字节,也就是计算偏移量的时候多跳过3个字节,同理j的偏移可以通过简单的数学运算就能得到。

比如也可以通过unsafe来灵活取值:

package main

import (
"fmt"
"unsafe"
) func main() {
var b []byte = []byte{'a', 'b', 'c'}
var c *byte = &b[0]
fmt.Println(*(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(c)) + uintptr(1))))
}

  

关于填充,FastCGI协议就用到了。

golang: 利用unsafe操作未导出变量的更多相关文章

  1. 利用pdb获取未导出符号

      BOOL InitSymHandler(HANDLE hProc)   {   CHAR SymPath[MAX_PATH], CurDir[MAX_PATH];       GetCurrent ...

  2. MySql 利用mysql&mysqldum导入导出数据

    MySql 利用mysql&mysqldum导入导出数据 by:授客 QQ:1033553122   测试环境 Linux下测试,数据库MySql 工具 mysqldump,该命令位于mysq ...

  3. vue springboot利用easypoi实现简单导出

    vue springboot利用easypoi实现简单导出 前言 一.easypoi是什么? 二.使用步骤 1.传送门 2.前端vue 3.后端springboot 3.1编写实体类(我这里是dto, ...

  4. 利用 Java 操作 Jenkins API 实现对 Jenkins 的控制详解

    本文转载自利用 Java 操作 Jenkins API 实现对 Jenkins 的控制详解 导语 由于最近工作需要利用 Jenkins 远程 API 操作 Jenkins 来完成一些列操作,就抽空研究 ...

  5. PHP数组/Hash表的实现/操作、PHP变量内核实现、PHP常量内核实现 - [ PHP内核学习 ]

    catalogue . PHP Hash表 . PHP数组定义 . PHP变量实现 . PHP常量实现 1. PHP Hash表 0x1: 基本概念 哈希表在实践中使用的非常广泛,例如编译器通常会维护 ...

  6. 告别硬编码-发个获取未导出函数地址的Dll及源码

    还在为找内核未导出函数地址而苦恼嘛? 还在为硬编码通用性差而不爽吗? 还在为暴搜内核老蓝屏而痛苦吗? 请看这里: 最近老要用到内核未导出的函数及一些结构,不想再找特征码了,准备到网上找点符号文件解析的 ...

  7. dll的概念 dll导出变量 函数 类

    1. DLL的概念 DLL(Dynamic Linkable Library),动态链接库,可以向程序提供一些函数.变量或类.这些可以直接拿来使用. 静态链接库与动态链接库的区别:   (1)静态链接 ...

  8. windows下Redis安装及利用java操作Redis

    一.windows下Redis安装 1.Redis下载 下载地址:https://github.com/MicrosoftArchive/redis 打开下载地址后,选择版本 然后选择压缩包 下载 R ...

  9. Redis入门和Java利用jedis操作redis

    Redis入门和Java利用jedis操作redis Redis介绍 Redis 是完全开源的,遵守 BSD 协议,是一个高性能的 key-value 数据库. Redis 与其他 key - val ...

随机推荐

  1. react-native android/ios 根据配置文件编译时自动修改版本号

    开发react-native时大都有过这个操作,当版本迭代时候要修改app版本号时,一般都这样做 Android: 的要修改build.gradle文件的versionName ios: 打开xcod ...

  2. 力扣(LeetCode)561. 数组拆分 I

    给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大. 示例 ...

  3. 盛最多水的容器(java实现)

    题目: 给定 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) .在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0).找出其中的 ...

  4. Spring中使用Redis

    普通使用Redis的方法很简单,前面的文章也有讲解,这篇文章主要就是讲解通过注解的方式实现Spring和Redis的整合.这里我们创建了3个类:1.Config 全局配置类,相当于xml配置文件2.R ...

  5. node.js调用google翻译api

    源码下载:https://pan.baidu.com/s/1nxoodst 使用:(只支持get) http://39.106.33.56:3001/translate?text=Failure is ...

  6. Confluence 6 安装指南

    在你开始之前 在你开始安装 Confluence 之前,请确定你的安装环境满足 最小系统安装要求和支持的平台. 如果你计划将你的 Confluence 运行到虚拟环境下,请参考 Running Con ...

  7. Luffy之课程详情页

    Luffy之课程详情页 提前写好课程详情的template,并放入到Vue中 注册路由 import CourseDetail from "../components/CourseDetai ...

  8. Police Stations CodeForces - 796D (bfs)

    大意: 给定树, 有k个黑点, 初始满足条件:所有点到最近黑点距离不超过d, 求最多删除多少条边后, 使得原图仍满足条件. 所有黑点开始bfs, 贪心删边. #include <iostream ...

  9. 1018. Binary Prefix Divisible By 5可被 5 整除的二进制前缀

    网址:https://leetcode.com/problems/binary-prefix-divisible-by-5/ 一次for循环遍历数组,在上次计算的基础上得到本次的结果! class S ...

  10. CopyOnWriteArrayList&Collections.synchronizedList()

    1.ArrayList ArrayList是非线性安全,此类的 iterator() 和 listIterator() 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remov ...