记一次gcc升级暴露的memcpy/memmove的程序bug
- 背景
项目升级了gcc版本从gcc-4.1.2升级到了gcc-4.8.5
产生一个结果是程序有crash,通过分析发现好像是内存数据结构被弄乱了,正常不应该出现这样的数据值,应该是未知操作破坏了正常的内存数据。
- 故障排除
因为用gcc-4.1.2编译是没有问题的,采用gcc-4.8.5才引发的问题,所以逐步对照编译所用的源文件,包括库文件,头文件,以及编译器开关的差异,发现均无差异。
此时开始怀疑编译器bug,开始逐步验证编译器发行版,到底是哪一个版本引入的bug,挨个验证:
https://ftp.gnu.org/gnu/gcc/
gcc-4.1.2 => PASS
gcc-4.2.0 => FAIL
gcc-4.2.4 => FAIL
gcc-4.8.5 => FAIL
gcc-4.9.0 => FAIL
gcc-4.9.4 => FAIL
gcc-5.5.0 => FAIL
gcc-12.1.0 => FAIL
发现从4.1.2之后就一直有这个bug,直到最新的版本12.1.0这个bug还是有。难道真的被我碰到这种神级的bug了吗,得好好研究一下了。
比较4.1.2和4.2.0,他们中间没再发布其他版本,试图了解一下release note和gcc-4.1.2-4.2.0.diff.bz2,感觉还是吃不消,内容太细阅读性咱跟不上。
怎么办?应该是gcc的bug,因为两个版本之间产品代码没有变化,编译配置也没有变化,唯一不同的就是gcc编译器版本,但想想又不应该啊,这么多年这么多个release都发布了,如果真是gcc bug应该早被暴露出来早被修复了啊。
还得深挖一下。
- 一步一步来
既然有参照对象,那咱们就一个一个来:
- 咱先不用4.8.5了,先选用4.2.0,这样两者差异会小点,反正问题是一样的。
- 把源文件一个一个排除,试图找出到底是哪一个源文件引发的问题。
这其中涉及把编译过程分开,.c -> .i -> .s -> .o -> .exe,其中要分开哪一步哪一个文件用4.1.2编译,哪些用4.2.0编译。这一步最终定义一个文件从.i -> .s的时候出现了问题,说明还是编译器cc1的问题。 - 把函数以一个排除,到底是哪一个函数引发的问题。
这里要把前面定位的.i文件拆分成两个.i文件编译,再链接,为的是把函数一个一个提取出来,确定是哪一个函数生成的汇编代码不一致。
这里选取4.2.0作为对照的好处是,生成的汇编代码差异相对小些,不然如果用4.8.5版本跨越太大,生成的汇编码无关差异太大,不好比较。
通过比较发现主要差一点就是对memcpy的调用方式不一致。
gcc-4.1.2 | gcc-4.2.0
----------------+-------------------
mov <DST> %rdi | mov <DST> %rdi
mov <SRC> %rsi | mov <SRC> %rsi
mov <LEN> %rcx | mov <LEN> %rdx
cld | call memcpy@PLT
rep |
movsb |
可见:
- gcc-4.1.2采用指令的方式实现memcpy
- gcc-4.2.0采用库函数调用的方式实现memcpy
难道是库函数memcpy出问题了。这可是libc的函数啊,成千上万无时无刻不在使用的函数啊,不应该出问题的。
也许是偶然,也是是灵光一现,在反复观看函数具体实现时,发现有些地方使用memcpy有些地方使用memmove,而4.1.2的处理方式是不一样的,对memmove调用,gcc-4.1.2也采用和gcc-4.2.0一样的方式即函数调用(call memmove),而只对memcpy采用指令循环的方式。
立马想到这是不是memcpy和memmove引发的bug,之前我可是常用它来面试候选人的哦。
- 粗暴试错
怀疑是memcpy和memmove的问题,先把函数里的所有memcpy改成memmove,重新编译运行,竟然成功了。就它了!
下面就具体定位:是哪一个memcpy引发问题,是否真的有memcpy地址重叠的问题?答案是还真是。
至此,问题水落石出,还是产品代码本身的问题,真不能轻易赖在成千上万人每时每刻都是使用的主流产品上去。