现在的编译器还需要手动展开循环吗_一例 Go 编译器代码优化 bug 定位和修复解析...

本文深入剖析了一个Go编译器优化错误导致的循环无限执行bug,通过分析编译器流程、静态单赋值形式(SSA),定位到prove pass的问题,最终修复bug。文章旨在帮助读者理解Go编译器工作原理和优化过程。
摘要由CSDN通过智能技术生成

摘要

本文中介绍了 Go 编译器的整体编译流程脉络和一个编译优化错误导致数据越界访问的 bug,并分析了对这个 bug 的排查和修复过程,希望能够借此让大家对 Go 编译器有更多的了解,在遇到类似问题时有排查思路。

缘起

某日,一位友人在群里招呼我,“看到有人给 Go 提了个编译器的 bug,挺有意思,感觉还挺严重的,要不要来看看?”于是我打开了 issue 40367[1] 。彼时,最新一条评论是 这条[2]

26abe2d70528c41b72d222c57131194a.png

提到将循环体中的一个常数从 1 改成 2 就无法复现问题,这顿时勾起了我的兴趣,于是我准备研究一番。

bug 代码跟现象如下图,正常来看,代码应该在输出 "5 6" 后停止,然而实际上却无限执行了下去,只能强行终止或等待程序触碰到无权限内存地址之后崩溃。

f17539824284a23eec1569b154f08af1.png

首先,我们要定位到这个问题具体的直接原因。简单来说,这个 bug 是 for-range loop 越界,原本循环应该在循环次数到达数组长度后终止,但是这个复现程序中的循环无限执行了下去。乍一看,问题像是有 bound check 被优化掉了,那么我们来实锤一下。有一个方便的网站,可以在线观察给定程序编译产出的汇编结果,我用 这个网站[3] 分别生成了原复现程序和将第六行的 +1 改为 +2 后不复现程序的汇编,供大家对比。抛开无关细节不提,可以很容易地看到前者的汇编相较于后者的确少了一次判断,导致循环无法终止,具体的位置是第二段代码的 105 行:

96822fb42c83be932a2d7c4cad2683c1.png

既然直接原因已经定位到了,那接下来我们就要想办法追进编译器来查看为什么汇编结果有问题了。对很多同学来说,追进编译器查问题的过程可能比较陌生,听起来就令人望而却步,那么我们如何来排查这个问题呢?

背景知识

在追踪这个具体问题之前,我们需要先了解一些相关知识背景。

Go 编译器的大体运行流程

想要追查 Go 编译器的问题,首先就需要了解 Go 编译器的大致运行流程。其实 Go 的编译器的实现中规中矩,相比于 GCC/Clang 等老牌编译器甚至有些简陋,许多优化并未实现。一个 Go 程序在生成汇编前的工作大概分为这几步:

  1. 语法解析。由于 Go 语言语法相当简单,所以 Go 编译器使用的是一个手写的 LALR (1) 解析器,这部分跟今天的 bug 无关,细节略过不提。
  2. 类型检查。Go 是强类型静态类型语言,在编译期会对赋值、函数调用等过程做类型检查,判断程序是否合法。另外,这个步骤会将一些 Go 自带的泛型函数变换成具体类型的函数调用,比方说 make 函数,在类型检查阶段会根据类型检查的结果变换成具体的 makeslice/makemap 等。这部分也跟今天的 bug 无关。
  3. 中间代码 (IR)生成。为方便做跨平台代码生成,也为方便做编译优化,现代编译器通常会将语法树变成一个中间代码表示
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值