- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
距离golang 1.23发布还有两个月不到,按照惯例很快要进入1.23的功能冻结期了。在冻结期间不会再添加新功能,已经添加的功能不出大的意外一般也不会被移除。这正好可以让我们提前尝鲜这些即将到来的新特性.
今天要说的就是1.23中对//go:linkname指令的变更。这个新特性可以说和我的一次失误息息相关.
重要的事情得先写在前面://go:linkname指令官方并不推荐使用,且不保证任何向前或者向后兼容性,因此明智的做法是尽量别用 。
牢记这一点之后,我们可以接着往下看了。至于为啥和“我”也就是本文的作者有关,我们先看完新版本带来的新变化再说.
简单的说,linkname指令用于向编译器和链接器传递信息。具体的含义根据用法可以分为三类.
第一类叫做“pull”,意思是拉取,使用方式如下:
import _ "unsafe" // 必须有这行才能用linkname
import _ "fmt" // 被拉取的包需要显式导入(除了runtime包)
//go:linkname my_func fmt.Println
func my_func(...any) (n int, err error)
这种用法的指令格式是//go:linkname <指令下方的只有声明的函数或包级别变量名> <本包或者其他包中的有完整定义的函数或变量>.
这个指令的作用就是告诉编译器和连接器,my_func的函数体直接使用fmt.Println的,my_func类似fmt.Println的别名,和它共享同一份代码,就像把指令第二个参数指定的函数和变量拉取下来给第一个参数使用一样.
正因如此,指令下方给出的声明必须和被拉取的函数/变量完全一致,否则很容易因为类型不匹配导致panic(是的没错,除非拉取的对象不存在,否则都不会出现编译错误).
这个指令最恐怖的地方在于它能无视函数或者变量是否是export的,包私有的东西也能被拉取出来使用。因为这一点这种用法在早期的社区中很常见,比如很多人喜欢这么干://go:linkname myRand runtime.fastrand,因为runtime提供了一个性能还不错的随机数实现,但没有公开出来,所以有人会用linkname指令把它导出为己所用,当然随着1.21的发布这种用法不再有任何意义了,请永远都不要去模仿.
第二种用法叫做“push”,即推送。形式上是下面这样:
import _ "unsafe" // 必须有这行才能用linkname
//go:linkname main.fastHandle
func fastHandle(input io.Writer) error {
...
}
// package main
func fastHandle(input io.Writer) error
// 后面main包中可以直接使用fastHandle
// 这种情况下需要在main包下创建一个空的asm文件(通常以.s作为扩展名),以告诉编译器fastHandle的定义在别处
在这种用法中,我们只需要把函数/变量名当作第一个参数传给指令,注意需要给出想用这个函数/变量的包的名字,这里是main。同时在指令下方的函数/变量必须有完整的定义.
这种用法是告诉编译器和链接器这个函数/变量的名字就是xxx.yyy,如果遇到这个函数就使用linkname指定的函数/变量的代码,这个模式下甚至能在本包定义别的包里的函数.
当然这种用法的语义作用更明显,它意味着这个函数会在任何地方被使用,修改它需要小心,因为改变了函数的行为可能会让其他调用它的代码出bug;修改了函数的签名则很可能导致运行时panic;删除了这个函数则会导致代码无法编译.
最后一类叫做“handshake”,即握手。他是把第一类和第二类方法结合使用:
package mypkg
import _ "unsafe" // 必须有这行才能用linkname
//go:linkname fastHandle
func fastHandle(input io.Writer) error {
...
}
package main
import _ "unsafe" // 必须有这行才能用linkname
//go:linkname fastHandle mypkg.fastHandle
func fastHandle(input io.Writer) error
“pull”的一方没什么区别,但“push”的一方不用再写包名,同时用来告诉编译器函数定义在别的地方的空的asm文件也不需要了。这种就像通讯协议中的“握手”,一方告诉编译器这边允许某个函数/变量被linkname操作,另一边则明确像编译器要求它要使用某个包的某个函数/变量.
通常“pull”和“push”应该成对出现,也就是你只应该使用“handshake”模式.
然而不幸的是,当前(1.22)的go语言支持“pull-only”的用法,即可以随便拉取任何包里的任何函数/变量,但不需要被拉取的对象使用“push”标记自己。而被linkname拉取的一方是完全无感知的.
这就导致了非常大的隐患.
最大的隐患在于这个指令可以在不通知被拉取的packages的情况下随意使用包中私有的函数/变量.
举个例子:
// pkg/mymath/mymath.go
package mymath
func uintPow(n uint) uint {
return n*n
}
// main.go
package main
import (
"fmt"
_ "linkname/pkg/mymath"
_ "unsafe"
)
//go:linkname pow linkname/pkg/mymath.uintPow
func pow(n uint) uint
func main() {
fmt.Println(pow(6)) // 36
}
正常来说,uintPow是不可能被外部使用的,然而通过linkname指令我们直接无视了接口的公开和私有,有什么就能用什么了.
这当然是非常危险的,比如我们把uintPow的参数类型改成string:
package mymath
func uintPow(n string) string {
return n + n
}
这时候编译还是能正常编译,但运行的时候就会出现各种bug,在我的机器上表现是卡死和段错误。为什么呢?因为我们把uint强行传递了过去,但参数需要是string,类型对不上,自然会出现稀奇古怪的bug。这种在别的语言里是严重的类型相关的内存错误.
另外如果我们直接删了uintPow或者给他改个名,链接器会在编译期间报错:
$ go build
# linkname
main.main: relocation target linkname/pkg/mymath.uintPow not defined
而且我们导出的是私有函数,通常没人会认为自己写的私有级别的帮助函数会被导出到包外并被使用,因此在开发时大家都是保证公开接口的稳定性,私有的函数/变量是随时可以被大规模修改甚至删除的.
而linkname将这种在别的语言里最基本的规矩给粉碎了.
而且事实上也是如此,从1.18开始几乎每个版本都有因为编译器或者标准库内部的私有函数被修改/删除从而导致某些第三方库在新版本无法使用的问题,因为这些库在内部悄悄用//go:linkname用了一些未公开的功能。最近一次发生在广泛使用的知名json库上类似的问题可以在这里看到.
既然这个指令如此危险,为什么还一直存在呢?答案是有不得不用的理由,其中一个就在启动go程序的时候.
我们来看下go的runtime里是怎么用linkname的:
// runtime/proc.go
//go:linkname main_main main.main
func main_main()
// runtime.main
// 所有go程序的入口
func main() {
// 初始化runtime
// 调用main.main
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
// main退出后做清理工作
}
因为程序的入口在runtime里(要初始化runtime,比如gc等),所以入口函数必须在runtime包里。而我们又需要调用用户定义在main包里的main函数,但main包不能被import,因此只能靠linkname指令让链接器绕过所有编译器附加的限制来调用main函数.
这是目前在go自身的源代码里看到的唯一一处不得不使用“pull-only”模式的地方.
另外“handshake”模式也有存在的必要性,因为像runtime和reflect需要共享很多实现上的细节,因此reflect作为pull的一方,runtime作为push的一方,可以极大减少代码维护的复杂度.
除了上述这些情况,绝大数linkname的使用都可以算作abuse.
鉴于上述情况,golang核心团队决定限制linkname的使用.
第一个改动是标准库里新添加的包全部禁止使用linkname导出其中的内容,目前是通过黑名单实现的,1.23中新添加的几个包以及它们的internal依赖都在名单上,这样可以防止已有的linkname问题继续扩大。这对已有的代码也是完全无害的.
第二个变更时添加了新的ldflags: -checklinkname=1。1代表开启对linkname的限制,0代表维持1.22的行为不变。目前默认是0,但官方决定在1.23发布时默认值为1开启限制。个人建议尽量不要关闭这个限制。这个限制眼下只针对标准库,但按官方的说法效果好的话以后所有的代码不管标准库还是第三方都会启用限制.
最后也是最大的变动,禁止对标准库的 “pull-only” linkname指令,但允许“handshake”模式.
虽然go从来不保证linkname的向后兼容性,但这样还是会大量较大的破坏,因此官方已经对常见的go第三方库做了扫描,会把一些经常被人用linkname拉取的接口改成符合“handshake”模式的形式,这种改动只用加一行指令即可。而且该限制目前只针对标准库,其他第三方库暂时不受影响.
因为这个变更,下面的代码在1.23是无法编译通过的:
package main
import _ "unsafe"
//go:linkname corostart runtime.corostart
func corostart()
func main() {
corostart()
}
因为runtime.corostart并不符合handshake模式,所以对它的linkname被禁止了:
$ go version
go version devel go1.23-13d36a9b46 Wed May 15 21:51:49 2024 +0000 windows/amd64
$ go build -ldflags=-checklinkname=1
# linkname
link: main: invalid reference to runtime.corostart
大趋势肯定是以后只允许handshake模式。不过作为过渡目前还是允许push模式的,并且官方应该会在进入功能冻结后把之前说的扫描到的常用的内部函数添加上linkname指令.
这里比较重要的是作为开发者的我们应该怎么办:
-checklinkname=1
排查代码,及时清除不必要的linkname指令。-ldflags=-checklinkname=0
关闭限制,这样也许能岁月静好几个版本,直到某一天程序突然没法编译或者运行了一半被莫名其妙的panic打断。4是万不得已时的保底方案,按优先度我推荐1 > 3 > 2的顺序去适配go1.23。2和3不仅仅适用于go标准库,常用的第三方库也可以。通过这些适配工作说不定也有机会让你成为go或者知名第三方库的贡献者.
从现在开始完全是来得及的,毕竟离1.23的第一个测试版发布还有一个月左右,离正式版发布还有两个月。而且方案2的修改并不算作新功能,不受功能冻结的影响.
当然,大部分开发者应该不用担心,比较linkname的使用是少数,一些主动使用linkname的库比如quic-go也知道兼容性问题,很小心地做了不同版本的适配,加上官方承诺的兜底这一对linkname指令的改动的影响应该比想象中小,但是是提高代码安全性的一大步.
那肯定有关系,老丢人了.
其实之所以会在开发窗口的中后期有这样大的变动,多半是因为我捅的篓子:前面也说过以前也有不少linkname引用的私有api变化导致的兼容问题,但要么影响范围很小要么作者及时适配使得这些问题没引起太大的波澜;但这次我的改动影响到了某个广泛应用的基础库,这个库用linkname指令引用了大量的内部api,更恐怖的是k8s也在用它,有人用master分支的go编译了一下k8s问题才被发现,要是没能及时发现的话会有一大堆软件在1.23测试版发布的时候出现兼容问题。其实在我的提交之前这些内部api已经变得面目全非了,但因为函数名字和字段类型没怎么变所以库的代码还能接着跑,直到我的提交打破了这一切.
当然问题要说大其实也不大,像那样大量使用linkname且没怎么适配版本的第三方库本身就不多,其次把变更的内部函数的签名还原之后问题很快就解决了,因此除了核心开发者和谷歌内部之外应该没多少人发觉这个问题。这也充分体现了linkname的危险性:在不算缺乏经验的我以及至少三位经验丰富的审核者的review下也没预料到这样功能简单且使用面极窄的内部私有函数会被linkname指令拉取出来使用.
后续库作者也说这些linkname引用的内部api其实很早之前就已经没啥用处了,他会尽快删除;实际上我跟踪了一下库代码发现这些被linkname导出的内部api除了设置了一些简单的flag值之外也确实没啥用处,flag值有些也没用上.
认识到这样的危险性后go官方自然不会坐视不管,官方以前应该也有类似想做限制的想法,这次也算是找到了合情合理的理由了,所以这回行动也意外的快,不到一星期从黑名单禁止导出新的库到linkname指令的检查都实现了。不出意外的话我们应该能在1.23看到一个更健壮的go以及它的标准库.
这样的问题怎么避免?答案是不可能,因为linkname能无视几乎一切限制私有函数/变量的办法,而且你也很难知道有哪些代码通过linkname访问了你写的函数/变量,因此只要一天不做限制类似这次问题的事故就会越来越多,总不可能让开发者每次改完代码都扫描一遍go语言编写的常见的项目吧。而且go的兼容性保证的是公开的接口和语法,内部实现的细节从来都不是也不应该是保证的对象.
我捅的这个篓子现在作为example被放在新提案里呢,虽说本质上用日本话讲叫“お互い様”(大家都有不对的地方),但作为广泛应用的编程语言也确实有需求和义务要兼容那些作为生态基石的应用广泛的第三方库,作为go的贡献者之一却忽视了这一点被结结实实地被上了一课也是应该的,算是经验教训了。。.
最后总结就一句话:没事别用//go:linkname。。。。。.
想跟进这一变更的进展的话,可以看这个issue:https://github.com/golang/go/issues/67401 。
最后此篇关于因为我的一次疏忽而带来的golang1.23新特性的文章就讲到这里了,如果你想了解更多关于因为我的一次疏忽而带来的golang1.23新特性的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
目录 C#特性 1. 概括 2. 语法 定义特性类 应用特性
是否有关于 Python 语言必须提供哪些功能的文章/论文?为什么应该使用 Python 而不是任何其他语言? Python 的优点和缺点是什么? 最佳答案 Why Python和 Why Pytho
我想弄清楚为什么 .x比*.x具有更高的特异性当后者有望获胜时。 不是*.x应该具有 0-0-1-1 的特异性(1 个类,1 个标签)而 .x只是一个类(class) 0-0-1-0 ? 考虑以下基本
在尝试评估非 instanceof 条件时,我发现了我在 Groovy 2.4.7、1.6.0 JVM 中没有预料到的行为。 总之: class Foo { static Boolean
当使用可变结构和属性时,编译器可以解决一些问题,但不能对其他相当明显的事情做同样的事情,这对我来说似乎很奇怪。 以下面的自动属性为例: Vector2 Vector { get; set; } 而不是
我对 ES3 有一定的了解,但我对 ES5 的特性不是很了解。我 - 或多或少 - 知道: Object.create(), Object.freeze() “使用严格” getter 和 sette
我最近开始将 Django 1.5.4 用于带有 MySQL 后端的 Web 应用程序。就在一开始,我遇到了某些限制,这让我想知道 Django 是否是继续前进的正确方法。 一些明显的缺点是: 缺少复
在函数模板的定义中,模板参数的实例化一般是未知的。类型特征可用于在编译时获取一些信息。例如,这是 is_pointer 的一个简单应用: template void foo(T p) { cout
我正在设计页面的样式,该页面具有除最后一个框外的带底部边框的连续框。我为所有框应用类 .box 并添加 .box_last 以仅隐藏最后一个框的边框。 .box { border-bottom-s
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
我有兴趣编制一份不建议在嵌入式系统中使用的 c++ 功能列表(这可能会导致人们推荐使用 c 而不是 c++)。如果您知道,请尝试添加原因,或者将您的原因添加到其他人的答案中。 这是一个开始(我知道的唯
作为一名 Ruby 程序员,您是否曾觉得任何使用起来有点冒险的功能,可能是因为它的奇怪行为?它可能有很好的文档记录,但在调试时很难找到,或者难以记住? 我通常尽量远离 String#gsub!。文档说
我正在开发一个连接到健身 watch 的蓝牙应用程序。这是我第一次使用蓝牙。我设法使用出色的 FlutterBlue 将我的应用程序与设备连接起来图书馆。 但是我无法理解我从阅读中获得的结果。这是我阅
1. 迭代器(Iterator)的介绍 背景:指针可以用来遍历存储空间连续的数据结构,但是对于存储空间费连续的,就需要寻找一个行为类似指针的类,来对非数组的数据结构进行遍历。 定义:迭代器是一种检
嗨,伙计们,有没有动态更改 Spring 属性文件内容的好例子?如果您能给我一些示例或链接,我将不胜感激。 非常感谢 最佳答案 我想你可以使用 ReloadableResourceBundleMess
SystemVerilog 引入了一些非常有用的结构来改进编码风格。然而,正如我的一位同事经常说的,“你不是在写软件,你是在描述硬件。”考虑到这一点,当最终结果需要合成时,应该避免语言的哪些特征?这个
我定义了这些测试依赖项 / Test Dependencies lazy val wiremock = "com.github.tomakehurst" % "wir
我正在为 Android 手机和 Android watch (wearOS) 编写应用程序。这些应用程序将通过蓝牙相互通信。基本上,Android 手机上的应用程序将与 WearOS 设备绑定(bi
我正在为 Android 手机和 Android watch (wearOS) 编写应用程序。这些应用程序将通过蓝牙相互通信。基本上,Android 手机上的应用程序将与 WearOS 设备绑定(bi
我有兴趣提高我的设计能力(设计具有属性、方法等的类)。即如何决定类、方法和属性应该是什么? 你们能建议我改进这个的好 Material 吗? 最佳答案 请看: Any source of good o
我是一名优秀的程序员,十分优秀!