ホームページ  >  記事  >  バックエンド開発  >  Golang の安全でないパッケージについて学ぶ

Golang の安全でないパッケージについて学ぶ

青灯夜游
青灯夜游転載
2023-04-02 08:30:021113ブラウズ

一部の低レベル ライブラリでは、安全でないパッケージが使用されていることがよくあります。この記事では、Golang の unsafe パッケージについて理解し、unsafe パッケージの役割と Pointer の使い方を紹介します。

Golang の安全でないパッケージについて学ぶ

unsafe パッケージは、go の型安全性チェックをバイパスできるいくつかの操作を提供し、それによってメモリ アドレスを直接操作し、いくつかのトリッキーな操作を実行します。サンプルコードの実行環境は go バージョン go1.18 darwin/amd64

メモリアライメント

安全でないパッケージは、Sizeof メソッドを提供します。変数が占有するメモリ サイズを取得します。「ポインタが指す変数のメモリ サイズは含まれません」、Alignof はメモリ アライメント係数を取得します。具体的なメモリ アライメント ルールは自分でググることができます。

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}复制代码

上記のケースから、demo1 と Demon2 には同じ属性が含まれており、定義された属性の順序が異なるだけで、変数のメモリ サイズが異なることがわかります。これは、メモリのアライメントが発生するためです。

コンピュータはタスクを処理する際、特定のワード長の単位でデータを処理します。「例: 32 ビット オペレーティング システムではワード長は 4、64 ビット オペレーティング システムではワード長は 8」。そして、データを読み取るときも、単位はワード長に基づきます。例: 64 ビット オペレーティング システムの場合、プログラムによって一度に読み取られるバイト数は 8 の倍数です。以下は、非メモリ アライメントとメモリ アライメントでのデモ 1 のレイアウトです。

非メモリ アライメント:

変数 c は異なる語長で配置され、CPU は読み取る必要があります。同時に 2 回読み取り、両方の結果を同時に処理して c の値を取得します。この方法ではメモリ領域は節約されますが、処理時間は増加します。

メモリ アライメント:

メモリ アライメントは、同じ非メモリ アライメントの状況を回避できるスキームを採用していますが、「時間のためのスペース」として余分なスペースが必要になります。特定のメモリ調整ルールについては、Google で検索できます。

Golang の安全でないパッケージについて学ぶ

Unsafe Pointer

Go ではポインタ型を宣言できます。ここでの型は安全なポインタです。ポインタが Type を指していることを明確にする必要があります。型が一致しない場合、コンパイル中にエラーが発生します。次の例のように、コンパイラは MyString と string は異なる型であり、割り当てることができないと判断します。

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

任意の型の変数を指すことができる型はありますか?任意のタイプの変数を指すことができる unsfe.Pointer を使用できます。 Pointer の宣言により、それが変数のアドレスを指すポインター型であることがわかります。特定のアドレスに対応する値は、uinptr を通じて変換できます。ポインタには、次の 4 つの特別な操作があります。

  • #任意のタイプのポインタをポインタ型に変換できます
  • ポインタ型の変数を任意のタイプのポインタに変換できます
  • uintptr 型の変数を Pointer 型に変換できます
  • Pointer 型の変数を 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

}

Pointer の 6 つの使い方

公式ドキュメントでは、Pointer の 6 つの使用姿勢が説明されています。

  1. ポインターを介して *T1 を *T2 に変換します

ポインターはメモリの一部を直接ポイントしているため、この部分はメモリのアドレスを任意の型に変換できます。ここで、T1 と T2 は同じメモリ レイアウトである必要があり、異常なデータが存在することに注意してください。

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)
}

Golang の安全でないパッケージについて学ぶ

T1 と T2 のメモリ レイアウトが異なる場合はどうなりますか?以下の例では、demo1 と Demon2 には同じ構造が含まれていますが、メモリの配置によりメモリ レイアウトが異なります。 Pointer を変換するとき、demo1 のアドレスから 24 "sizeof" バイトが読み取られ、demo2 のメモリ アライメント ルールに従って変換が実行されます。最初のバイトは a:true に変換され、8 ~ 16 バイトが変換されます。 :2 に変換されます。16 ~ 24 バイトはデモ 1 の範囲を超えていますが、直接読み取ることはでき、予期しない値 b:17368000 が取得されます。

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, 
}

Golang の安全でないパッケージについて学ぶ

  1. ポインタ型を uintptr 型に変換する "Uinptr をポインタに変換しないでください。"

Pointer は任意の変数を指すことができるポインタ型で、Pointer を uintptr に変換すると、Pointer が指す変数のアドレスを出力できます。さらに: uintptr は Pointer に変換しないでください。次の例を考えてみましょう。GC が発生すると、d のアドレスが変更される可能性があり、更新が同期されていないため、up は間違ったメモリをポイントします。

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

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教程

以上がGolang の安全でないパッケージについて学ぶの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。