"*(*uintptr)"和"**(**uintptr)"之间有什么区别?

7
在Go语言的`runtime/proc.go`文件中,有一段如下所示的代码:
``` // funcPC返回函数f的入口点PC。 // 它假设f是一个func值。否则行为未定义。 // 注意:在带插件的程序中,funcPC可以返回相同函数的不同值(因为实际上在地址空间中有多个相同的函数副本)。 // 为了安全起见,请勿在任何==表达式中使用此函数的结果。只能将结果用作开始执行代码的地址。 ```
//go:nosplit
func funcPC(f interface{}) uintptr {
    return **(**uintptr)(add(unsafe.Pointer(&f), sys.PtrSize))
}

我不明白的是为什么不使用*(*uintptr)而是要使用**(**uintptr)?
所以我编写了下面的测试程序来弄清楚。
package main

import (
    "fmt"
    "unsafe"
)


func main(){
    fmt.Println()

    p := funcPC(test)

    fmt.Println(p)

    p1 := funcPC1(test)

    fmt.Println(p1)

    p2 := funcPC(test)

    fmt.Println(p2)
}

func test(){
    fmt.Println("hello")
}

func funcPC(f func()) uintptr {
    return **(**uintptr)(unsafe.Pointer(&f))
}

func funcPC1(f func()) uintptr {
    return *(*uintptr)(unsafe.Pointer(&f))
}

p不等于p1的结果让我感到困惑。虽然它们的类型相同,但为什么p的值不等于p1的值呢?

输入图像描述


1
因为它是指向指针的指针? - zerkms
不了解Go语言,但我想知道 funcPC(p) 的值是多少。反正拥有指向指针的指针有什么意义呢? - LukStorms
@LukStorms: 指向指针的指针的重点在于指向指针。如果pp指向p,那么写入*pp将写入p,并且从*pp读取的是p的值。当然,如果p在作用域内,这有点傻,因为您可以直接从或向p读取或写入。但是,如果p不在作用域内,或者如果pp指向pq中的任一个(取决于先前的逻辑),并且您想要使用或更新pp指向的任何指针,则该怎么办? - torek
@torek 哦,好的,也许这样做并不是那么毫无意义。 - LukStorms
2个回答

7

介绍

在Go中,函数值表示函数的代码。从远处看,它是函数代码的指针。它像一个指针。

更近一步地看,它是类似于这样的结构体(取自runtime/runtime2.go):

type funcval struct {
    fn uintptr
    // variable-size, fn-specific data here
}

函数值将指向函数代码的指针作为其第一个字段保存,我们可以解引用该指针以访问函数代码。

解释您的示例

要获取函数的地址(即函数代码),可以使用反射:

fmt.Println("test() address:", reflect.ValueOf(test).Pointer())

为了确认我们获取到正确的地址,我们可以使用runtime.FuncForPC()。这将返回与您的funcPC()函数相同的值。参见下面的例子:
fmt.Println("reflection test() address:", reflect.ValueOf(test).Pointer())
fmt.Println("funcPC(test):", funcPC(test))
fmt.Println("funcPC1(test):", funcPC1(test))

fmt.Println("func name for reflect ptr:",
    runtime.FuncForPC(reflect.ValueOf(test).Pointer()).Name())

它输出的结果如下(在Go Playground中尝试):
reflection test() address: 919136
funcPC(test): 919136
funcPC1(test): 1357256
func name for reflect ptr: main.test

为什么?因为函数值本身是一个指针(它只是具有不同于指针的类型,但它存储的值是一个指针),需要对其进行解引用以获取代码地址。
所以你需要在funcPC()内部获得这个uintptr(代码地址)的简单方法是:
func funcPC(f func()) uintptr {
    return *(*uintptr)(f) // Compiler error!
}

当然它不能编译,转换规则不允许将函数值转换为*uintptr。另一种尝试是先将其转换为unsafe.Pointer,然后再转换为*uintptr
func funcPC(f func()) uintptr {
    return *(*uintptr)(unsafe.Pointer(f)) // Compiler error!
}

再次强调:转换规则不允许将函数值转换为unsafe.Pointer。任何指针类型和uintptr值都可以相互转换,但不包括函数值。
因此,我们必须先有一个指针值。那么我们能有哪些指针值呢?是的,f的地址:&f。但这不是函数值,而是f参数(本地变量)的地址。因此,&f在概念上不是(仅仅是)一个指针,而是一个指向指针的指针(需要对两者进行解引用)。我们仍然可以将其转换为unsafe.Pointer(因为任何指针值都符合要求),但它不是函数值(作为指针),而是指向它的指针。
因此,我们需要从函数值获取代码地址,所以我们必须使用**uintptr来转换unsafe.Pointer值,并且我们必须使用2个解应用操作来获取地址(而不仅仅是f中的指针)。
这正是为什么funcPC1()会给出不同、意外的、不正确的结果的原因。
func funcPC1(f func()) uintptr {
    return *(*uintptr)(unsafe.Pointer(&f))
}

它返回的是指向的指针,而不是实际的代码地址。

1
我在“转换规则”中找不到任何关于不允许将“函数值”转换为“uintptr”的信息。 - polar9527
3
正是因为没有其他内容。规范中明确列出了允许的内容,未列出的内容则不被允许。请查找以以下文本开头的列表:“非常量值 x 可以在以下情况下转换为类型 T…” - icza

0

它返回不同的值,因为(**uintptr)与*(*uintptr)不同。前者是双重间接,后者是简单间接。

在前一种情况下,该值是指向指向指向uint的指针。


2
您能解释一下 **(**uintptr) 和仅有的 (uintptr) 有什么区别吗?为什么这里要使用 **(**uintptr) 语法? - minitauros

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接