• golang之不安全编程


    楔子

    不安全编程?用golang以来也没发现有啥不安全的啊,而且golang有垃圾回收,也不需要我们来管理内存。当听到不安全编程这几个字,唯一能想到的也就是指针了,只有指针才可能导致不安全问题。我们知道golang中是有指针的,但是golang的指针并不能像C语言中的指针一样,可以进行运算,所以golang中的指针既提供了指针的便利性,又保证了安全。但是在golang中,可以通过一个叫做unsafe的包让指针突破限制,从而进行运算,一旦用不好会导致很严重的问题,但是用好了在某些场景下能够带来很大的便利,所以我们说这是不安全编程。但即便如此,我们还是可以使用的,而且golang的内部也在大量的使用unsafe这个包。

    golang中的指针

    尽管golang的指针没有C的指针那么强大,但是能够获取一个变量的地址,并且能通过地址来改变存储的值,我个人认为已经足够了。

    package main
    
    import "fmt"
    
    func pass_by_value(num int){
    	num = 3
    }
    
    func pass_by_pointer(num *int){
    	*num = 3
    	num = nil
    }
    
    func main() {
    
    	num := 1
    	pass_by_value(num)
    	fmt.Println("传递值:", num)  //传递值: 1
    	pass_by_pointer(&num)
    	fmt.Println("传递指针:", num)  //传递指针: 3
    

    我们知道golang的函数传递方式是值传递,不管传递什么,都是拷贝一份出来。而且函数里面形参叫什么是无所谓,这里我们函数的形参不叫num,叫其他的也无所谓。

    pass_by_value中接收一个整型,当我们传递num的时候,会把num的值拷贝一份出来传进去,此时函数里面无论做什么修改,都不会影响外面的num,因为不是一个东西。

    pass_by_pointer中接收一个指针,那么传递&num的时候,依旧会把指针拷贝一份,我们说golang只有值传递,传递指针的话也是把指针拷贝一份。由于是拷贝,所以两者没有任何关系,只不过它们存储的地址是一样的,但就pass_by_pointer来说,里面的num这个* int类型的变量和我们传递的&num来说是没有关系的。由于存储的地址是一样的,所以两者操作的都是同一片内存,因此* num = 3之后是会影响外面的num的,但是指针也是拷贝,所以函数里面的num = nil跟外面没关系。

    所以golang中的指针在改变内存的值的时候和C是一样的,但是它和C中的指针相比,又弱化了许多。

    弱化一:golang中的指针不能进行数学运算。

    package main
    
    func main() {
    	arr := [...]int{1, 2, 3}
    	//获取数组首元素的地址
    	p := &arr[0]
    	//如果是C中,我们可以通过p++,获取当前元素的下一个元素的地址
    	//但是在golang中不行,p++这种做法是不会通过编译的
    	p++
    }
    
    //invalid operation: p++ (non-numeric type *int)
    

    弱化二:golang中不同类型的指针不能进行转化或者赋值。

    package main
    
    func main() {
    	var a int
    	var b *float64
    	b = &a
    	
    	//cannot use &a (type *int) as type *float64 in assignment
    }
    

    弱化三:golang中不同类型的指针不能进行比较。

    只有在两个指针类型相同或者可以相互转化的情况下才可以比较,比较两个地址是否一样。另外所有指针都可以通过==!=和nil进行比较,来判断这个指针是否为空。

    package main
    
    import "fmt"
    
    func main() {
    	var a = 1
    	var b = 1
    	//值相同,为true
    	fmt.Println(a == b)  // true
    	//但是地址不一样,为false
    	fmt.Println(&a == &b)  // false
    
    	//但是不同类型的指针是无法比较的,别说指针了,就是值也是无法比较的
    	//golang对于类型的要求是非常严格的
    	var c float64
    	/*
    	fmt.Println(&a == &c)
    
    	//invalid operation: &a == &c (mismatched types *int and *float64)
    	*/
    
    	//但是它们都可以和nil进行比较
    	fmt.Println(&c == nil, &c != nil)  // false true
    
    	var m map[int]int
    	//map是指针类型,对于slice、channel、map这种数据结构,我们一般使用make创建,会申请对应的内存,然后返回指针
    	//如果只是这种声明的话,那么是没有所谓的0值的,直接会返回一个空指针
    	//别看打印m的结果是map[],但是这只是表示m是一个map类型的数据,但是它还没有被分配内存,所以结果nil
    	fmt.Println(m == nil, m) // true map[]
    }
    

    unsafe:不安全编程

    什么是unsafe

    通过以上我们看到golang中的指针实际上是类型安全的,因为golang对类型的检测是十分的严格,让你在享受指针带来的便利时,又给指针施加了很多制约来保证安全。但是保证安全是需要以牺牲效率为代价的,如果你能保证写出的程序就是安全的,那么你可以使用golang中的不安全指针,从而绕过类型系统的检测,让你的程序运行的更快。

    如果是一个高阶golang程序员的话,怎么能不会unsafe包呢?它可以绕过golang的类型系统的检测,直接访问内存,增加效率。golang中的很多限制,比如不能操作结构体中的未导出成员等等,但是有了unsafe包,就可以直接突破这些限制。所以这个包叫做unsafe,我们称使用unsafe为不安全编程,因为它很危险,官方也不推荐使用,估计正因为如此也设计了这么个名字吧。但是你底层都在大量使用,那我们为什么不能用。

    unsafe实现原理

    我们刚才提到了不安全指针,那么我们先来看看什么是不安全指针。

    package unsafe
    type ArbitraryType int
    type Pointer *ArbitraryType
    
    func Sizeof(x ArbitraryType) uintptr
    func Offsetof(x ArbitraryType) uintptr
    func Alignof(x ArbitraryType) uintptr
    

    unsafe包下面只有一个unsafe.go文件,这个文件里面把注释去掉就上面6行代码,是的你没有看错。当然功能肯定都内嵌在编译器里面,至于怎么实现的我们就不管啦,看看怎么用就行了。我们先来看看这两行:

    type ArbitraryType int
    type Pointer *ArbitraryType
    

    Arbitrary表示任意的,所以这个Pointer可以是任何类型的指针,比如:*int、*string、*float64等等。也就是说任何类型的指针都可以传递给它。

    package main
    
    import (
    	"fmt"
    	"unsafe"
    )
    
    func main() {
    	var a int
    	var s string
    	var f float64
    	fmt.Println(unsafe.Pointer(&a))  //0xc000062080
    	fmt.Println(unsafe.Pointer(&s))  //0xc00004e1c0
    	fmt.Println(unsafe.Pointer(&f))  //0xc000062088
    }
    

    unsafe.Pointer()是有返回值的,返回的当然也是一个指针,但是这个指针同样是无法进行运算的。如果无法运算,那么我们还是无法实现通过指针自增的方式,访问数组的下一个元素啊。别急,所以还有一个整数类型:uintptr,我们unsafe.Pointer()是可以和uintptr互相转化的,而这个uintptr是可以运算的,并且它还足够大。因此我们目前看到了两个功能:

    1.任何类型的指针都可以和unsafe.Pointer相互转化

    2.unsafe.Pointer可以和uintptr互相转化

    但是需要注意的是,uintptr并没有指针的含义,所以它指向的内存是会被回收的,而unsafe.Pointer有指针的含义,可以确保其指向的对象不会被回收。

    使用unsafe带你突破限制

    像C语言一样访问数组或切片

    package main
    
    import (
    	"fmt"
    	"unsafe"
    )
    
    func main() {
    	//这里把数字弄成没有规律的,就不用1 2 3 4 5 6了
    	var arr = []int{177, 123, 3, 221, 5, 1211}
    
    	//获取第二个元素的指针,我们也不从头获取
    	//因为从中间获取都可以的话,那么从头获取肯定可以
    	//然后传给unsafe.Pointer(),将*int转化成Pointer类型
    	pointer := unsafe.Pointer(&arr[1])
    
    	//注意了:下面要将Pointer转成uintptr,因为Pointer是不能运算的
    	u_pointer := uintptr(pointer)
    	//此时的u_pointer就相当于C中的指针了,但是还有一点不同
    	//C中的指针直接++即可,指针会自动移到到下一个元素的位置
    	//而golang中的uintptr相当于一个整型,我们不能++,而是需要+8,因为一个int占8个字节,所以golang中需要加上元素所占的大小
    	//所以我们发现C中的+n是从当前元素开始,移动n个元素,不管元素是什么类型。
    	//但是golang的+n是移动n个字节。
    	//所以C中的指针+2 等于 golang中uintptr + 2 * (元素类型所占的字节)
    	u_pointer += 16 //移动两个元素
    
    	//然后再转回来,要先转成Pointer,再转成对应的指针类型
    	pointer = unsafe.Pointer(u_pointer)
    
    	//这个pointer是我们通过&arr[1]也就是*int类型的指针得到的,那么结果也要转成*int
    	int_pointer := (*int)(pointer)
    	// 打印了221,结果是正确的
    	fmt.Println(*int_pointer) // 221
    
    	//这里也可以转成*string,即便我们的pointer是通过*int得到的
    	//因为Pointer可以是任何指针类型
    	string_pointer := (*string)(pointer)
    	//也是可以打印的,但是通过*来访问内存的话就会报错,panic: runtime error: invalid memory address or nil pointer dereference
    	fmt.Println(string_pointer) //0xc00008c048
    
    	//这里我们再加上1,不加8,那么会出现什么后果
    	//我们知道再加上8,就会访问221后面的5
    	u_pointer += 1
    	fmt.Println(*(*int)(unsafe.Pointer(u_pointer))) // 360287970189639680
    	//我们看到此时得到的是一个我们也不知道从哪里来的脏数据,所以一定要加上对应的字节
    }
    

    所以我们发现unsafe.Pointer就类似于一座桥,*T通过Pointer转成uintptr,然后进行指针运算,运算完成之后,再通过Pointer转回*T,此时的*T就是我们想要的了。

    指针访问结构体

    我们知道结构体是可以有字段的,那么我们也可以把结构体想象成数组,字段想象成数组的元素。

    package main
    
    import (
    	"fmt"
    	"unsafe"
    )
    
    type score struct {
    	math    int
    	english int
    	history int
    }
    
    func main() {
    	s := score{math: 90, english: 92, history: 85}
    
    	//我们看到通过unsafe.Pointer的方式,获取结构体的指针,可以直接转换为结构体第一个字段的指针
    	p := unsafe.Pointer(&s)
    	fmt.Println(*(*int)(p))
    	//math字段是一个整型,那么p转为uintptr之后加上8,就可以转换成第二个字段的指针
    	fmt.Println(*(*int)(unsafe.Pointer(uintptr(p) + 8))) //92
    	//同理加上16就是第三个
    	fmt.Println(*(*int)(unsafe.Pointer(uintptr(p) + 16))) //85
    	
    	//这里显然就是一个乱七八糟的值了
    	fmt.Println(*(*int)(unsafe.Pointer(uintptr(p) + 29))) //70709489434624
    }
    

    我们知道切片是一个结构体,有三个字段,分别是指向底层数组的指针,以及大小和容量。

    package main
    
    import (
    	"fmt"
    	"unsafe"
    )
    
    func main() {
    	//申请大小为5,容量为10的切片
    	s := make([]int, 5, 10)
    
    	//第一个元素显然是指向底层数组的指针,大小也是8个字节。我们来看第二个和第三个
    	//虽然有些长,但是从内往外的话,还是很好看懂的。如果不习惯的话可以多写几行
    	fmt.Printf("长度:%d
    ", *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + 8)))  //长度:5
    	fmt.Printf("容量:%d
    ", *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + 16)))  //容量:10
    }
    

    我们看到unsafe包还是很强大的,之所以叫unsafe是因为如果用不好后果会很严重。但是如果能正确使用的话,能够做到很多之前做不到的事情。

    获取对象的大小

    我们目前可以使用unsafe做很多事情了,但是还不够,我们看到unsafe这个包除了给我们提供了Pointer这个类型之外,还给我们提供了三个函数。

    func Sizeof(x ArbitraryType) uintptr
    func Offsetof(x ArbitraryType) uintptr
    func Alignof(x ArbitraryType) uintptr
    

    这三个函数返回的都是uintptr类型,这个类型你就看成是整型即可,它是可以和数字进行运算的,可以转为int。我们先来看看Sizeof

    package main
    
    import (
    	"fmt"
    	"unsafe"
    )
    
    func main() {
    	a := 123
    	b := "h"
    	c := []int{1, 2, 3}
    	fmt.Println(unsafe.Sizeof(a)) //8
    	//关于字符串为什么是16
    	//golang中的字符串在底层是一个结构体,这个结构体有两个元素
    	//一个是字符串的首地址,一个是字符串的长度
    	//所以是16,因为golang的字符串底层对应的是一个字符数组
    	fmt.Println(unsafe.Sizeof(b)) //16
    
    	//切片我们说过底层也是一个结构体,有三个字段,指向底层数组的指针、大小、容量,所以是24个字节
    	fmt.Println(unsafe.Sizeof(c)) //24
    }
    

    golang中的Sizeof和C中的sizeof还是比较类似的,但是golang中的Sizeof不能接收类型本身, 比如你可以传入一个123,但是你不能传入一个int,这是不行的。至于获取一个字符串的大小结果是16,这个是由golang底层字符串的结构决定的。对了,当我们获取一个结构体的大小的时候,我们看到貌似是将结构体中的每一个字段的值的大小进行相加,至少目前看来是这样的。

    获取结构体成员的偏移量

    对于一个结构体来说,可以使用Offsetof来获取结构体成员的偏移量,进而获取成员的地址,从而改变内存的值。这里提一句:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。但是你懂的,我们不可能直接通过对结构体的地址加上*来获取第一个成员的值,只能通过unsafe.Pointer转化,然后再转化成对应类型的指针,才能获取。

    package main
    
    import (
    	"fmt"
    	"unsafe"
    )
    
    type girl struct {
    	//对应的字节数
    	name string  // 16
    	age int  // 8
    	gender string  //16
    	hobby []string //24
    }
    
    func main() {
    	g := girl{"mashiro", 17, "f", []string{"画画", "开车"}}
    	//首先这几步操作应该不需要解释了,直接想象成数组即可
    	fmt.Println(*(*string)(unsafe.Pointer(&g))) // mashiro
    	fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16))) // 17
    	fmt.Println(*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8)))  // f
    	fmt.Println(*(*[]string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8 + 16)))  // [画画 开车]
    
    	//我们看到即使对具有不同字段类型的结构体,依旧可以自由操作,只要搞清楚每个字段的大小即可
    	*(*[]string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8 + 16)) =
    		append(*(*[]string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + 16 + 8 + 16)), "料理")
    	fmt.Println(g) // {mashiro 17 f [画画 开车 料理]}
    
    	//我们看到,即便操作起来没有问题,但是有一个缺陷,就是我们必须要事先计算好每一个字段占多少个字节,尽管我们可以通过unsafe.Sizeof可以很方便的计算。
    	//但是有没有不用计算的方法呢?显然有,就是我们说的Offsetof。但是这个Offsetof又有点特殊,它表示的是偏移量
    	//比如我想访问hobby这个字段,那么这么做可以,直接以&g为起点,此时偏移量为0,加上unsafe.Offsetof(g.hobby),直接偏移到hobby
    	fmt.Println(*(*[]string)(unsafe.Pointer(   uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.hobby)     ))) // [画画 开车 料理]
    	
    	//其余的也是一样,获取哪个字段,直接传入哪个字段即可,个人觉得这个Offsetof比自己计算要方便一些
    	fmt.Println(*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.name)))) // mashiro
    	fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.age)))) // 17
    	fmt.Println(*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&g)) + unsafe.Offsetof(g.gender)))) // f
    }
    

    而且我们知道,如果在别的包里面,结构体里的字段没有大写,那么是无法导出的,然鹅即便如此,我们依旧可以通过unsafe包绕过这些限制。

    package hahaha
    
    type OverWatch struct {
    	name   string
    	age    int
    	Gender string
    	weapon string
    }
    

    这些字段有三个没有大写,理论上是无法导出的,因为golang会进行检测,但是使用unsafe就可以绕过这些检测。

    package main
    
    import (
    	"fmt"
    	"hahaha"
    	"unsafe"
    )
    
    func main() {
    	hero := new(hahaha.OverWatch)
    	//设置name
    	*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(hero)))) = "麦克雷"
    	//设置age,因为Offsetof需要指定访问的字段,而字段又没有被导出,所以无法通过Offsetof的方式
    	//因此需要手动计算对应类型的偏移量,因为是string类型,所以加上一个Sizeof(""),当然也可以手动填上16
    	*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(hero)) + unsafe.Sizeof(""))) = 37
    	//这个就可以直接设置了,因为被导出了
    	hero.Gender = "男"
    	//老规矩,这里是两个string加上一个int的大小
    	*(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(hero)) + unsafe.Sizeof("") * 2 + unsafe.Sizeof(123))) = "维和者"
    	fmt.Println(*hero)  // {麦克雷 37 男 维和者}
    }
    

    字段对齐

    通过unsafe.Alignof可以获取字段的对齐值,不过这里用不上,可以自己尝试一下,上面的用的比较多。

    参考于:https://qcrao.com/2019/06/03/dive-into-go-unsafe/,用原文作者的话来说就是:

    unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。

    uintptr 可以和 unsafe.Pointer 进行相互转换,uintptr 可以进行数学运算。这样,通过 uintptr 和 unsafe.Pointer 的结合就解决了 Go 指针不能进行数学运算的限制。通过 unsafe 相关函数,可以获取结构体私有成员的地址,进而对其做进一步的读写操作,突破 Go 的类型安全限制。关于 unsafe 包,我们更多关注它的用法。

    顺便说一句,unsafe 包用多了之后,也不觉得它的名字有多么地不“美观”了。相反,因为使用了官方并不提倡的东西,反而觉得有点酷炫。这就是叛逆的感觉吧。个人非常赞同,觉得真的很酷。

  • 相关阅读:
    实践:VIM深入研究(20135301 && 20135337)
    信息安全系统设计基础第十二周学习总结
    信息安全系统设计基础第五次实验报告 20135201&&20135306&&20135307
    信息安全系统设计基础第四次实验报告 20135201&&20135306&&20135307
    信息安全系统设计基础第三次实验报告 20135201&&20135306&&20135307
    信息安全系统设计基础第二次实验报告 20135201&&20135306&&20135307
    深入理解计算机系统家庭作业汇总 20135301&&20135328
    java 基本理论知识点
    使用DAO模式开发宠物管理系统
    java编程常用的快捷键
  • 原文地址:https://www.cnblogs.com/traditional/p/12210822.html
Copyright © 2020-2023  润新知