Page 14 - 《软件学报》2024年第6期
P. 14
2590 软件学报 2024 年第 35 卷第 6 期
4. return p
5. }
6. func g2s() {
7. i := 1 //i 应该堆分配, 却被错误地栈分配
8. j := global2stack(& i)
9. _ = j
10. }
• 逃逸分析和后续编译优化的错误配合. 有时虽然逃逸分析没有出错, 但后续的一系列的编译优化却可能造成
错误. 一个例子是社区 issue#54247 [14] . 其一个简化版的实例如代码 4 所示. 在该例中, obj1 是栈对象, 但逃逸分析
后续编译流程中对 defer 的处理导致函数 Recover 的参数 objs 逃逸到堆上, 这就导致在第 4 行, 栈对象 obj1 的地
址被堆对象 objs 获取, 违反逃逸不变式, 导致错误.
代码 4. issue54247 简化版示例.
Go 语言官方对逃逸分析正确性的验证手段也较为有限, 目前在 Go 语言官方给出的逃逸分析的测试中对逃
1. func Escape(task func()) {
2. var obj1 obj
3. defer Recover(
4. & obj1,
5. ) //obj1 应该堆分配, 却被错误地栈分配
6. task()
7. }
8. func Recover(objs ...*obj) {
9. use(objs)
10. }
由于这些对象分配位置出错, 因此引用这些对象的堆和全局对象极易出现悬挂指针. Go 向程序员保证, 程序
执行的任何时刻中, 任意可达的对象都处于生命周期内, 即, 不可能出现悬挂指针. 当 Go 的 GC 遇到悬挂指针时会
直接在运行时 panic, 使得整个 Go 程序异常终止. 若在实际生产环境中出现该问题, 则很有可能带来不可挽回的损
失. 同时, GC 不是发生这种错误的第一现场, 以代码 3 为例, 其发生错误的第一现场应是第 4 行的 return p, 在该处
栈对象的地址被传递给堆对象. 因此, GC 的 panic 具有延后性和不可预知性. 由于 GC 不是错误的第一现场, 其崩
溃输出多和 GC 相关而和事发的第一现场无关. 后文图 4 所示为 issue#44614 和 issue#54247 中的崩溃输出. 其多
为显示 GC 的工作流程和状态信息, 不能准确地显示栈对象是在什么位置被传递给了全局或堆对象. 不完整的信
息也给排查问题带来了困难, issue#44614 中逃逸分析的漏洞从 Go1.14 开始出现, 直至 Go1.17 发布前才被修复,
影响了 Go1.14 至 Go1.16 这 3 个大版本.
1.2.2 逃逸分析正确性验证的现状
逸分析正确性的检验存在比较函数返回值和比较 DeBug 信息这两种途径.
途径一 [26] 是通过检查两次调用同一个函数的返回值是否相同来判断逃逸分析正确性的. 在这个函数内部会
为一个对象分配一块空间, 这部分空间理应堆分配, 这样在两次函数调用中将该空间的地址返回时为不同的地址,
这就可以通过测试. 但是如果该空间由于错误的逃逸分析导致被栈分配, 因为栈帧的布局在编译完成后已经确定,
故将导致在同一个栈帧下两次调用该函数返回的该变量的地址将为相同的地址, 这样就不能通过测试. 这种情况
只能适用于小规模测例, 而且要求在同一个栈帧下调用相同函数进行测试, 较难推广.