cgo - 绕过指针检查

更新日志

2021年9月22日 - 上传

关于 cgo 中的指针检查

官方文档 给出的说明如下:

原文:

Go is a garbage collected language, and the garbage collector needs to know the location of every pointer to Go memory. Because of this, there are restrictions on passing pointers between Go and C.

翻译:

Go 是一种垃圾回收语言,GC 需要知道每个 GO 指针所指向内存位置。因此 GO 与 C 之间传递指针会受到限制。

具体的限制参见官方文档。

大意就是说,GO 的 GC 机制需要了解到 GO 所引用内存的使用情况,因此没有办法自由的在 GO 与 C 之间传递指针。也因此将 GO 指针传递给 C 是危险的,因为该指针随时可能被 GO 的 GC 机制回收,在 C 中使用该指针将导致无法预期的问题发生。相反的 C 指针被允许自由的传递,但 C 指针指向的内存必须人为管理,因为 GO 的 GC 机制无法了解到 C 指针的使用情况。

如何绕过指针检查

了解到相关的机制之后,引入我们的问题,那就是如何绕过相关的指针检查?

在 GO 中使用 GO 指针作为参数或者返回值传递给 C。示例如下:

示例-1

package main

// #include <stdio.h>
/*
void print_go_ptr(void *go_ptr) {
  printf("0x%.16x", go_ptr);
}
*/
import "C"

import (
  "unsafe"
)

func main() {
  var v struct{ f1, f2 int }
  C.print_go_ptr(unsafe.Pointer(&v))
}

输出:

0x0000000000016040

通过这个示例可以发现,实际上有时 GO 指针也是可以作为参数传递给 C 的。那为什么说不可以将 GO 指针传递给 C 呢?看下面这个示例:

示例-2

package main

// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
/*
void print_go_ptr(void *go_ptr) {
  printf("0x%.16x", go_ptr);
}
*/
import "C"

import (
  "unsafe"
)

func main() {
  var a, b int
  var v struct{ f1, f2 *int }
  v.f1 = &a
  v.f2 = &b
  C.print_go_ptr(unsafe.Pointer(&v))
}

输出:

runtime error: cgo argument has Go pointer to Go pointer

示例-2触发 runtime error。

两个示例都同样向 C 传递了 GO 指针,区别在于示例-2中 GO 指针指向的内存区域包含了其他的 GO 指针(结构 v 的成员 f1,f2 分别被指向了 变量 a,b)。

GODEBUG 环境变量设置为 GODEBUG=cgocheck=0 关闭指针检查,再运行示例-2

输出:

0x0000000000040070

可以看到 GO 在运行时进行 GO 指针检查,当检查到 GO 指针指向的内存区域包含 GO 指针时,抛出异常。

假如我们有办法确保 GO 指针在被传递到 C 中时的有效性,那如何避免 GO 指针检查呢?上面通过环境变量关闭指针检查是一种解决方案,但是这将暴露程序出错的风险,因为有时我们并无法确保 GO 指针的有效性。

我这里想到的解决方案之一是:可以将指针转换为数值类型。

C99 中定义了 intptr_tuintptr_t 类型(stdint.h),可以用作指针与数值类型的安全转换(32 位系统与 64 系统中指针长度不同)。示例如下:

示例-3

package main

// #include <stdio.h>
// #include <stdint.h>
/*
void print_go_ptr(uintptr_t go_ptr) {
  printf("0x%.16x", (void *) go_ptr);
}
*/
import "C"

import (
  "unsafe"
)

func main() {
  var a, b int
  var v struct{ f1, f2 *int }
  v.f1 = &a
  v.f2 = &b
  C.print_go_ptr(C.uintptr_t(uintptr(unsafe.Pointer(&v))))
}

输出:

0x000000000003bf68

成功执行。

需要注意的是这么做是危险的,这绕开了 GO 指针检查,因此在使用时需要注意 GO 指针需要确保在 C 执行时必须是有效的。

golang runtime 包下 KeepAlive 函数可以确保在该函数被调用之前 GO 指针的有效性,相关的介绍参见官方文档

知识共享许可协议 [cgo - 绕过指针检查] 由 [洋灰] 采用 知识共享署名-相同方式共享 4.0 国际许可协议 (CC BY-SA 4.0) 进行许可。