Rumah  >  Artikel  >  pembangunan bahagian belakang  >  Ketahui tentang pakej tidak selamat di Golang

Ketahui tentang pakej tidak selamat di Golang

青灯夜游
青灯夜游ke hadapan
2023-04-02 08:30:021065semak imbas

Di sesetengah perpustakaan peringkat rendah, anda sering melihat penggunaan pakej yang tidak selamat. Artikel ini akan membawa anda memahami pakej tidak selamat di Golang, memperkenalkan peranan pakej tidak selamat dan cara menggunakan Pointer saya harap ia akan membantu anda!

Ketahui tentang pakej tidak selamat di Golang

Pakej tidak selamat menyediakan beberapa operasi yang boleh memintas pemeriksaan keselamatan jenis go, dengan itu mengendalikan alamat memori secara langsung dan melakukan beberapa operasi rumit. Persekitaran berjalan bagi kod sampel ialah go version go1.18 darwin/amd64

Penjajaran memori

Pakej tidak selamat menyediakan kaedah Sizeof untuk mendapatkan saiz memori yang diduduki oleh pembolehubah "tidak termasuk memori saiz ditunjuk oleh penunjuk", Alignof mendapat pekali penjajaran memori. Anda boleh google sendiri peraturan penjajaran memori khusus.

type demo1 struct {
   a bool  // 1
   b int32 // 4
   c int64 // 8
}

type demo2 struct {
   a bool  // 1
   c int64 // 8
   b int32 // 4
}

type demo3 struct { // 64 位操作系统, 字长 8
   a *demo1 // 8
   b *demo2 // 8
}

func MemAlign() {
   fmt.Println(unsafe.Sizeof(demo1{}), unsafe.Alignof(demo1{}), unsafe.Alignof(demo1{}.a), unsafe.Alignof(demo1{}.b), unsafe.Alignof(demo1{}.c)) // 16,8,1,4,8
   fmt.Println(unsafe.Sizeof(demo2{}), unsafe.Alignof(demo2{}), unsafe.Alignof(demo2{}.a), unsafe.Alignof(demo2{}.b), unsafe.Alignof(demo2{}.c)) // 24,8,1,4,8
   fmt.Println(unsafe.Sizeof(demo3{}))                                                                                                           // 16
}                                                                                                         // 16}复制代码

Daripada kes di atas, anda boleh melihat demo1 itu dan demo2 mengandungi atribut yang sama, tetapi susunan atribut yang ditentukan adalah berbeza tetapi ia menghasilkan saiz memori yang berbeza. Ini kerana penjajaran memori berlaku.

Apabila komputer sedang memproses tugas, ia akan memproses data dalam unit panjang perkataan tertentu "Contohnya: sistem pengendalian 32-bit, panjang perkataan ialah 4; sistem pengendalian 64-bit, panjang perkataan ialah 8". Kemudian, apabila membaca data, unit juga berdasarkan panjang perkataan. Contohnya: Untuk sistem pengendalian 64-bit, bilangan bait yang dibaca oleh program pada satu masa ialah gandaan 8. Berikut ialah susun atur demo1 di bawah penjajaran bukan memori dan penjajaran memori:

Penjajaran bukan memori:

Pembolehubah c akan diletakkan dalam panjang perkataan yang berbeza dan CPU perlu membaca pada masa yang sama Baca dua kali dan proses keputusan kedua-dua masa pada masa yang sama untuk mendapatkan nilai c. Walaupun kaedah ini menjimatkan ruang memori, ia akan meningkatkan masa pemprosesan.

Penjajaran memori:

Penjajaran memori menggunakan skema yang boleh mengelakkan situasi penjajaran bukan memori yang sama, tetapi ia akan mengambil sedikit ruang tambahan "ruang untuk masa". Anda boleh google untuk peraturan penjajaran memori tertentu.

Ketahui tentang pakej tidak selamat di Golang

Penunjuk Tidak Selamat

Anda boleh mengisytiharkan jenis penunjuk dalam go, jenis di sini ialah penunjuk selamat, iaitu, anda perlu untuk menjelaskan di mana penunjuk menunjukkan Jenis, jika jenis tidak sepadan, ralat akan berlaku semasa penyusunan. Seperti dalam contoh berikut, pengkompil akan berfikir bahawa MyString dan rentetan adalah jenis yang berbeza dan tidak boleh diberikan.

func main() {
   type MyString string
   s := "test"
   var ms MyString = s // Cannot use 's' (type string) as the type MyString
   fmt.Println(ms)
}

Adakah terdapat jenis yang boleh menunjuk kepada pembolehubah dari sebarang jenis? Anda boleh menggunakan unsfe.Pointer, yang boleh menunjuk kepada sebarang jenis pembolehubah. Melalui pengisytiharan Pointer, kita boleh tahu bahawa ia adalah jenis penunjuk, menunjuk ke alamat pembolehubah. Nilai yang sepadan dengan alamat tertentu boleh ditukar melalui uinptr. Penunjuk mempunyai empat operasi khas berikut:

  • Sebarang jenis penuding boleh ditukar menjadi jenis Penunjuk
  • Pembolehubah jenis penuding boleh ditukar kepada sebarang jenis penuding
  • Pembolehubah jenis Uintptr boleh ditukar kepada jenis Penunjuk
  • Pembolehubah jenis penuding boleh ditukar kepada jenis uintprt
type Pointer *ArbitraryType

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

func main() {
   d := demo1{true, 1, 2}
   p := unsafe.Pointer(&d)                // 任意类型的指针可以转换为 Pointer 类型
   pa := (*demo1)(p)                      // Pointer 类型变量可以转换成 demo1 类型的指针
   up := uintptr(p)                       // Pointer 类型的变量可以转换成 uintprt 类型
   pu := unsafe.Pointer(up)               // uintptr 类型的变量可以转换成 Pointer 类型; 当 GC 时, d 的地址可能会发生变更, 因此, 这里的 up 可能会失效
   fmt.Println(d.a, pa.a, (*demo1)(pu).a) // true true true

}

Enam cara untuk menggunakan Penunjuk

Dokumentasi rasmi menyediakan enam postur penggunaan Pointer.

  1. Tukar *T1 kepada *T2 melalui Penunjuk

Penunjuk menghala terus ke sekeping memori, jadi bahagian ini boleh Tukar alamat memori kepada apa-apa jenis. Perlu diingatkan di sini bahawa T1 dan T2 perlu mempunyai susun atur memori yang sama dan akan ada data yang tidak normal.

func main() {
   type myStr string
   ms := []myStr{"1", "2"}
   //ss := ([]string)(ms) Cannot convert an expression of the type '[]myStr' to the type '[]string'
   ss := *(*[]string)(unsafe.Pointer(&ms)) // 将 pointer 指向的内存地址直接转换成 *[]string
   fmt.Println(ms, ss)
}

Ketahui tentang pakej tidak selamat di Golang

Apakah yang berlaku jika susun atur memori T1 dan T2 berbeza? Dalam contoh di bawah, walaupun demo1 dan demo2 mengandungi struktur yang sama, disebabkan penjajaran memori, mereka mempunyai susun atur memori yang berbeza. Apabila menukar Penunjuk, 24 "sizeof" bait akan dibaca bermula dari alamat demo1, dan penukaran akan dilakukan mengikut peraturan penjajaran memori demo2 bait pertama akan ditukar kepada a:true dan 8-16 bait akan ditukar kepada c. :2, 16-24 bait berada di luar julat demo1, tetapi masih boleh dibaca secara langsung dan nilai yang tidak dijangka b:17368000 diperolehi.

type demo1 struct {
   a bool  // 1
   b int32 // 4
   c int64 // 8
}

type demo2 struct {
   a bool  // 1
   c int64 // 8
   b int32 // 4
}

func main() {
   d := demo1{true, 1, 2}
   pa := (*demo2)(unsafe.Pointer(&d)) // Pointer 类型变量可以转换成 demo2 类型的指针
   fmt.Println(pa.a, pa.b, pa.c) // true, 17368000, 2, 
}

Ketahui tentang pakej tidak selamat di Golang

  1. Tukar jenis Penunjuk kepada jenis uintptr "Anda tidak sepatutnya menukar uinptr kepada Penunjuk"

Penunjuk ialah jenis penunjuk yang boleh menunjuk kepada mana-mana pembolehubah Anda boleh mencetak alamat pembolehubah yang ditunjuk oleh Penunjuk dengan menukar Penunjuk kepada uintptr. Selain itu: uintptr tidak boleh ditukar kepada Penunjuk. Ambil contoh berikut: Apabila GC berlaku, alamat d mungkin berubah, kemudian naik menunjukkan ke memori yang salah disebabkan kemas kini yang tidak disegerakkan.

func main() {
   d := demo1{true, 1, 2}
   p := unsafe.Pointer(&d)
   up := uintptr(p)
   fmt.Printf("uintptr: %x, ptr: %p \n", up, &d) // uintptr: c00010c010, ptr: 0xc00010c010
   fmt.Println(*(*demo1)(unsafe.Pointer(up)))    // 不允许
}
  1. 通过算数计算将 Pointer 转换为 uinptr 再转换回 Pointer

当 Piointer 指向一个结构体时, 可以通过此方式获取到结构体内部特定属性的 Pointer。

func main() {
   d := demo1{true, 1, 2}
   // 等同于 unsafe.Pointer(&d.b); unsafe.Add(unsafe.Pointer(&d), unsafe.Offsetof(d.b))
   pb := unsafe.Pointer(uintptr(unsafe.Pointer(&d)) + unsafe.Offsetof(d.b))
   fmt.Println(pb)
}
  1. 当调用 syscall.Syscall 的时候, 可以讲 Pointer 转换为 uintptr

前面说过, 由于 GC 会导致变量的地址发生变更, 因此不可以直接处理 uintptr。但是, 在调用 syscall.Syscall 时候可以允许传递一个 uintptr, 这里可以简单理解为是编译器做了特殊处理, 来保证 uintptr 是安全的。

  • 调用方式:
  •   syscall.Syscall(SYS_READ, uintptr( fd ), uintptr(unsafe.Pointer(p)), uintptr(n))

下面这种方式是不允许的:

u := uintptr(unsafe.Pointer(p)) // 不应该保存到一个变量上 syscall.Syscall(SYS_READ, uintptr( fd ), u, uintptr(n))

  1. 可以将 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 的结果「uintptr」转换为 Pointer

在 reflect 包中的 Value.Pointer 和 Value.UnsafeAddr 直接返回了地址对应的值「uintptr」, 可以直接将其结果转为 Pointer

func main() {
   d := demo1{true, 1, 2}
   // 等同于 unsafe.Pointer(&d.b); unsafe.Add(unsafe.Pointer(&d), unsafe.Offsetof(d.b))
   pb := unsafe.Pointer(uintptr(unsafe.Pointer(&d)) + unsafe.Offsetof(d.b))
   // up := reflect.ValueOf(&d.b).Pointer(), pc := unsafe.Pointer(up); 不安全, 不应存储到变量中
   pc := unsafe.Pointer(reflect.ValueOf(&d.b).Pointer())
   fmt.Println(pb, pc)
}
  1. 可以将 reflect.SliceHeader 或者 reflect.StringHeader 的 Data 字段与 Pointer 相互转换

SliceHeader 和 StringHeader 其实是 slice 和 string 的内部实现, 里面都包含了一个字段 Data「uintptr」, 存储的是指向 []T 的地址, 这里之所以使用 uinptr 是为了不依赖 unsafe 包。

func main() {
   s := "a"
   hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // *string to *StringHeader
   fmt.Println(*(*[1]byte)(unsafe.Pointer(hdr.Data))) // 底层存储的是 utf 编码后的 byte 数组
   arr := [1]byte{65}
   hdr.Data = uintptr(unsafe.Pointer(&arr))
   hdr.Len = len(arr)
   ss := *(*string)(unsafe.Pointer(hdr))
   fmt.Println(ss) // A
   arr[0] = 66
   fmt.Println(ss) //B
}

应用

string、byte 转换

在业务上, 经常遇到 string 和 []byte 的相互转换。我们知道, string 底层其实也是存储的一个 byte 数组, 可以通过 reflect 直接获取 string 指向的 byte 数组, 赋值给 byte 切片, 避免内存拷贝。

func StrToByte(str string) []byte {
   return []byte(str)
}

func StrToByteV2(str string) (b []byte) {
   bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
   sh := (*reflect.StringHeader)(unsafe.Pointer(&str))
   bh.Data = sh.Data
   bh.Cap = sh.Len
   bh.Len = sh.Len
   return b
}

// go test -bench .
func BenchmarkStrToArr(b *testing.B) {
   for i := 0; i < b.N; i++ {
      StrToByte(`{"f": "v"}`)
   }
}

func BenchmarkStrToArrV2(b *testing.B) {
   for i := 0; i < b.N; i++ {
      StrToByteV2(`{"f": "v"}`)
   }
}

//goos: darwin
//goarch: amd64
//pkg: github.com/demo/lsafe
//cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
//BenchmarkStrToArr-12            264733503                4.311 ns/op
//BenchmarkStrToArrV2-12          1000000000               0.2528 ns/op

通过观察 string 和 byte 的内存布局我们可以知道, 无法直接将 string 转为 []byte 「确实 cap 字段」, 但是可以直接将 []byte 转为 string

Ketahui tentang pakej tidak selamat di Golang

func ByteToStr(b []byte) string {
   return string(b)
}

func ByteToStrV2(b []byte) string {
   return *(*string)(unsafe.Pointer(&b))
}

// go test -bench .
func BenchmarkArrToStr(b *testing.B) {
   for i := 0; i < b.N; i++ {
      ByteToStr([]byte{65})
   }
}

func BenchmarkArrToStrV2(b *testing.B) {
   for i := 0; i < b.N; i++ {
      ByteToStrV2([]byte{65})
   }
}

//goos: darwin
//goarch: amd64
//pkg: github.com/demo/lsafe
//cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
//BenchmarkArrToStr-12            536188455                2.180 ns/op
//BenchmarkArrToStrV2-12          1000000000               0.2526 ns/op

总结

本文介绍了如何使用 unsafe 包绕过类型检查, 直接操作内存。正如 go 作者对包的命名一样, 它是 unsafe 的, 随着 go 版本的迭代, 有些机制可能会发生变更。如无必要, 不应使用这个包。如果要使用 unsafe 包, 一定要理解清楚Pointer、uinptr、对齐系数等概念。

推荐学习:Golang教程

Atas ialah kandungan terperinci Ketahui tentang pakej tidak selamat di Golang. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:juejin.cn. Jika ada pelanggaran, sila hubungi admin@php.cn Padam