1. 缘起

这几天调gcc 7.5.0 +glibc 2.23的交叉编译工具链,由于gcc 7.5.0的默认打开Werr,偶然发现了glibc一个隐藏了二十年的世纪大bug。

这个bug在glibc 2.0版本刚开始就引入了,但直到2.25版本才最终解决,即使按glibc-2.0.1.bin.alpha-linux.tar.gz 版本的发布时间(04-Feb-1997)到glibc-2.25.tar.bz2 的发布时间(05-Feb-2017),也持续了20年加一天。

用gcc 7.5编译的时候如果使能-Wall -Werror这2个选项(-Wall 英文说明是Enable most warning messages,表示使能大多数告警上报;-Werror表示所有告警都当错误来上报,不可忽略),会报下面的错误:

nss_nisplus/nisplus-alias.c: In function '_nss_nisplus_getaliasbyname_r':
nss_nisplus/nisplus-alias.c:300:12: error: argument 1 null where non-null expected [-Werror=nonnull]
char buf[strlen (name) + 9 + tablename_len];
^~~~~~~~~~~~~
In file included from ../include/string.h:54:0,
from ../sysdeps/generic/hp-timing-common.h:40,
from ../sysdeps/x86_64/hp-timing.h:38,
from ../include/libc-internal.h:7,
from ../sysdeps/x86_64/nptl/tls.h:29,
from ../sysdeps/x86_64/atomic-machine.h:20,
from ../include/atomic.h:50,
from nss_nisplus/nisplus-alias.c:19:
../string/string.h:394:15: note: in a call to function 'strlen' declared here
extern size_t strlen (const char *__s)
^~~~~~
nss_nisplus/nisplus-alias.c:303:39: error: '%s' directive argument is null [-Werror=format-truncation=]
snprintf (buf, sizeof (buf), "[name=%s],%s", name, tablename_val);
^~
cc1: all warnings being treated as errors

如果不使能-Werror,编译器最多会上报告警,程序还是能正常编译通过。上面2个告警分别对strlen的入参和snprintf的字符串格式化参数做了非空检查,根据代码逻辑判断,两处代码如果执行到,调用的入参确实都必然是空指针。

源代码如下:

276 enum nss_status
277 _nss_nisplus_getaliasbyname_r (const char *name, struct aliasent *alias,
278 char *buffer, size_t buflen, int *errnop)
279 {
280 int parse_res;
281
282 if (tablename_val == NULL)
283 {
284 __libc_lock_lock (lock);
285
286 enum nss_status status = _nss_create_tablename (errnop);
287
288 __libc_lock_unlock (lock);
289
290 if (status != NSS_STATUS_SUCCESS)
291 return status;
292 }
293
294 if (name != NULL)
295 {
296 *errnop = EINVAL;
297 return NSS_STATUS_UNAVAIL;
298 }
299
300 char buf[strlen (name) + 9 + tablename_len];
301 int olderr = errno;
302
303 snprintf (buf, sizeof (buf), "[name=%s],%s", name, tablename_val);
304
305 nis_result *result = nis_list (buf, FOLLOW_PATH | FOLLOW_LINKS, NULL, NULL);

可以看出300行对应的strlen函数的入参要求非空,但由于294行做了一个非空的判断并返回,也就是说如果294行的if判断为非,那说明name指针必然为空,这时strlen来获取字符串长度就会异常。

具体会怎么异常?我们可以写个简单的例子:

1 #include <stdio.h>
2 #include <string.h>
3 int main()
4 {
5 printf("%d", strlen(NULL));
6 return 0;
7 }

默认不带任何参数的情况下,gcc会上报告警,但仍然可以编译通过,执行后会出现Segmentation fault:

 1 gcc  test1.c
2 test1.c: In function 'main':
3 test1.c:5:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
4 printf("%d", strlen(NULL));
5 ^
6 test1.c:5:12: warning: format '%d' expects argument of type 'int', but argument 2 has type 'size_t {aka long unsigned int}' [-Wformat=]
7 printf("%d", strlen(NULL));
8 ^
9
10 ./a.out
11 Segmentation fault

编译如果加上-Wall -Werror选项会直接报error编译失败:

1 gcc -Wall -Werror test1.c
2 test1.c: In function 'main':
3 test1.c:5:5: error: null argument where non-null required (argument 1) [-Werror=nonnull]
4 printf("%d", strlen(NULL));
5 ^
6 test1.c:5:12: error: format '%d' expects argument of type 'int', but argument 2 has type 'size_t {aka long unsigned int}' [-Werror=format=]
7 printf("%d", strlen(NULL));
8 ^
9 cc1: all warnings being treated as errors

问题的直接原因还是因为libc库里面的strlen没有做空指针保护,直接访问入参对应的内存了,所以实际上就会出现空指针访问,程序异常退出。

同样的303行的snprintf也要求%s对应的参数不能是空指针,否则也会出现Segmentation fault。

从上面的分析可以看出,有一些warning实际上本身就是错误,应该作为error来处理,在glibc的漫长进化过程中,有很多执行路径可能真的没走到(如果没有100%覆盖率的单元测试,也没有完善的代码review机制,可能永远也没人会发现),或者确实不影响功能的正常发布。但这些告警指向的代码,一旦走到就会出现致命错误。

最终glibc修正代码其实也很简单,就是将294行的“if (name != NULL)”修改成了“if (name != NULL)”,一个运算符用反了。

很多影响非常大的bug,定位之后的实际修改都是简单的一两行代码的事情,但问题的关键是要发现bug并定位bug,并且在bug修正之后的波及测试工作。

这个bug之所有能持续20年没人发现,只能说明glibc中应该还有很多代码在实际场景中没有用到。

2. 编译器的进化

下面这个表格给出了不同clang或者gcc版本新增的代码静态检查的告警计数,为了显得简洁一点,clang7或者更老的clang的所有告警做了一下汇总,gcc 4或者更老的gcc版本的所有告警也做了一下汇总,从中可以看出每次大版本升级,编译器团队都给开发团队提供了一些新的工具能更多的发现自己代码bug的神器。

下面汇总的1204个告警中,有119个告警是clang和gcc都提供的,其他966个告警至少从名称上看是gcc或者clang特有的。其中clang(以clang 12来算)特有的告警检查项有803个,gcc(以gcc 9来算)有178个,单从这个指标看clang在静态检查方面是远胜于gcc的,"2012 ACM Software System Award"大奖实至名归。

不过clang本身是为了支撑llvm的,所以很多与llvm不相关的功能都是直接调用的gcc的库接口,可以认为clang是站在gcc的巨人肩膀上来发布的自己的产品。

当前各个公司都引入了很多静态检查的工具来完善代码质量,但第一步还是要把静态检查工具的老祖宗,也就是编译器,自带的静态检查功能用足用好,再考虑消除其他静态检查工具的问题比较靠谱。走好这一步,引入clang非常必要。

first introduced compiler version

Count of new warning options

clang7 or older 584
clang8 12
clang9 223
clang10 55
clang11 33
clang12 15
gcc 4 or older 172
gcc 5 26
gcc 6 24
gcc 7 35
gcc 8 16
gcc 9 24
Grand Total 1204

最新文章

  1. js 中histroy.back()与history.go()的区别
  2. [转]Unity 延迟执行一段代码的较为优雅的方式
  3. Android设置TextView显示一行或多行
  4. AngularJS测试框架 karma备忘
  5. Bootstrap 手风琴搭配导航条实现常用菜单栏
  6. Redis基本数据类型以及String(一)
  7. C语言之浮点数
  8. Java框架之Spring(五)
  9. c# tolist() 浅析
  10. ●poj 1474 Video Surveillance
  11. java控制台输入带空格的字符串
  12. sql中count(*)、count(col)、count(1)区别
  13. DDD - 概述 - 聚合 (三)
  14. LVS中Windows作为真实主机(RealServer)时的设置方法
  15. 正则表达式之 \b
  16. python 文件读写方式
  17. [WF2012]infiltration
  18. eclipse以O开头的版本安装tomcat插件
  19. Python 读取大文件的方式
  20. Unity3d修炼之路:GUIbutton简单使用,完毕对一个简单对象Cube的移动,旋转

热门文章

  1. react踩坑笔记
  2. 两万字长文,彻底搞懂Kafka!
  3. 跟我一起写 Makefile(二)
  4. 解析java源文件
  5. Golang语言系列-06-map数据类型和指针
  6. Git(6)-- 记录每次更新到仓库(git clone、status、add、diff、commit、rm、mv命令详解)
  7. kali 免杀工具shellter安装以及使用
  8. vivo商城计价中心 - 从容应对复杂场景价格计算
  9. CentOS7 安装Oracle12c数据库
  10. NOIP 模拟 6 考试总结