当前位置:首页 > 公众号精选 > 程序员写个解
[导读]在我看来最不值得一提的BUG是那种可以重复复现的,他的稳定复现通常排查起来没啥技术含量, 早些年我处理一个不值得一提的BUG,BUG也很好复现,难点是复现时间固定在4小时左右,BUG由于文件资源未释放引起进程访问文件数目受限而崩溃,早期Android系统用该BUG获取到root权限, 本文向你分享,如何根据错误提示和参考手册找到故障点,指导新码农如何正确阅读Linux帮助手册(man page), 最后总结我的排查过程给小白一点实用的建议。好下面开始不如步入正题。需要调试的是一个监控程序,代码非常简单,2个线程执行不同的任务,每个任务都是间隔15秒执行一次,程序固定在大约4小时后崩溃。代码简单到用不着任何同步机制、没有任何通信,极少的内存访问,按理来说他就不应该存在BUG,然而还是发生了。

最不值得一提的BUG

在我看来最不值得一提的BUG是那种可以重复复现的,他的稳定复现通常排查起来没啥技术含量, 早些年我处理一个不值得一提的BUG,BUG也很好复现,难点是复现时间固定在4小时左右,BUG由于文件资源未释放引起进程访问文件数目受限而崩溃,早期Android系统用该BUG获取到root权限, 本文向你分享,如何根据错误提示和参考手册找到故障点,指导新码农如何正确阅读Linux帮助手册(man page), 最后总结我的排查过程给小白一点实用的建议。好下面开始不如步入正题。需要调试的是一个监控程序,代码非常简单,2个线程执行不同的任务,每个任务都是间隔15秒执行一次,程序固定在大约4小时后崩溃。代码简单到用不着任何同步机制、没有任何通信,极少的内存访问,按理来说他就不应该存在BUG,然而还是发生了。

第1个4小时:缩小排查范围,是什么引起段错误

在源码若干位置加上打印执行的函数、行号, 打开调试选项重新编译应用程序,开启coredump选项,耐心等待4小时后故障复现。gdb打开coredump 确认段错误(Segmentation fault),栈溯确认崩溃现场调用栈。段错误位于ti_ck_mutil函数第266行之后。
TickStatusIO():105ti_ck_mutil():266Segmentation fault (core dumped) (gdb) bt#0  0x401b28e0 in vfwprintf () from /lib/libc.so.6#1  0x00009d10 in ti_ck_mutil (cmdstr=0xbebffa4c, len=1) at src/ti.c:268#2  0x00008e2c in TickStatusIO () at src/initgpio.c:106#3  0x00009238 in main (argc=1, argv=0xbebffbf4) at src/initgpio.c:304

审查ti_ck_mutil函数内226行之后的代码,结合栈底位置是vfwprintf函数入口,基本可以确定导致崩溃位置是fread函数,fread可能会有什么错误呢?
int ti_ck_mutil(char *cmdstr, int count){ FILE *stream; char strout[256]; int ret, failcount = 0;  for (int i = 0; i < count; i++) { printf("%s()%d\n", __FUNCTION__, __LINE__);//226行 stream = popen(cmdstr, "r");//未检查文件是否成功 ret = fread(strout, sizeof(char), sizeof(strout), stream); // 228行  strout[ret] = '\0'; pclose(stream); // ... } return failcount;}
fread输入参数只有4个,猜测可能存在的失败原因有3点:
1、被编译器优化后strout的缓存不是256

但后面用的是算数表达式sizeof,就算被优化也不会造成错误。
观点:暂时不去瞎想2、fread写入最后一个字符时溢出。

strout后第256地址也被填写了,实际我读写的文件不超过64byte,不应该超过256。
即使第256地址被fread写了,相当于内存访问越接。访问越接发生什么错误都不奇怪,轻微越接会影响附近变量的值,比如ret和stream的值改变,大范围越界破坏调用栈。观点:猜测fread可能访问越限,但绝对没破坏调用栈。若破坏调用栈,那么栈不会是整整齐齐打印4个函数,而是输出若干问号(“?? ()”),找不到函数名称标签。
#0  0x000028e0 in ?? () #1  0x000038e8 in ?? () #2  0x000048ec in ?? () #3  0x000068e0 in ?? () #4  0x00009d10 in ti_ck_mutil (cmdstr=0xbebffa4c, len=1) at src/ti.c:268#5  0x00008e2c in TickStatusIO () at src/initgpio.c:106#6  0x00009238 in main (argc=1, argv=0xbebffbf4) at src/initgpio.c:304

3、stream文件描述符无效观点:有可能,源码未对popen返回结果做判断。

第2个4小时:是内存越界?还是资源不足?

于是结合猜测2和3,对源码做2处理修改:1、不向fread传递完整内存长度,保证最后一个字符不被fread填写 2、判断popen返回值
stream = popen(cmdstr, "r");ret = fread(strout, sizeof(char), sizeof(strout), stream); 修改后 stream = popen(cmdstr, "r");if (stream == 0) { perror("popen error:");}ret = fread(strout, sizeof(char), sizeof(strout) - 1, stream);

继续等待4小时,程序依旧崩溃,输出崩溃前提示执行popen失败,返回值0,错误原因记录在errno里,errno指示打开太多文件,资源不足。
	
popen error:: Too many open files

机理分析:为什么文件打开太多?

进一步定位到故障点在popen函数上,问题是:啥叫文件打开太多?查看popen帮助介绍:man popen。或许能给我解释
RETURN VALUEThe popen() function returns NULL if the fork(2) or pipe(2) calls fail, or if it cannot allocate memory.
本质上popen是个“壳",它返回0的原因有两个:1、它间接调用fork()创建子进程执行脚本,间接调用pipe()创建管道,子进程输出信息从管道传递到父进程。2、没有足够的内存分配。从第2点:没有足够的内存方向去排查,无非是内存泄漏咯,通常是申请内存有释放干净导致。c语言标准内存分配函数有malloc、calloc、realloc、reallocarray,对应的释放函数只有free。我应该在源码上搜索,是否所有“分配函数和释放函数都一一配对”,哦~别忘了,小白可能还不清楚,除了常用的malloc外,还有像mmap这样的内存分配函数,它有专用的释放函数munmap从搜索结果上看,数目是能对得上的,暂且粗略的判定不存内存泄漏。更仔细的排查方向应该是:确定代码执行流真的执行到释放函数,而不是单纯地看数目是否匹配



在继续阅读popen的errors段落描述。
ERRORSThe popen() function does not set errno if memory allocation fails. If the underlying fork(2) or pipe(2) fails, errno is set appropriately. If the type argument is invalid, and this condition is detected, errno is set to EINVAL.
popen不会因为内存分配失败而在errno记录错误码,如果是fork()或pipe()函数执行失败则在errno设置相应错误码。忙半天忙个寂寞,年轻人,别学会写一、二、三,就自以为无师自通懂得写四、写一万。读完man全文再入手好不好!既然errno提示具体错误信息,就不可能是内存泄漏,执行失败原因一定是Too many open files的字面意思。回想以前初学Linux时有个知识点:为了防止某用户打开过多的文件,系统对进程件访问数目有限制,默认是10242016年4月参加宋宝华的线下培训,他说Android刚出来时有个提权的方法(root权限):创建1024个无用子进程资源且不释放,第1025个进程就能得到root权限命令查看应用程序运行一段时间后,有多少文件描述符号(file descriptor)没有释放。果然,每间隔15秒文件描述符就多一个。256分钟后达到1024个文件描述符,时间上和软件4小时崩溃很接近。
watch -n 1 ls -l /proc/PID/fd

再用之前的筛选方法:排查open和close的函数是一一匹配。发现open关键词筛选出
6行,close作为关键词筛出5行。opendir没有对应的close。


捂脸!!!“Linux下一切皆是文件”我还没理解透彻,没意识到打开目录(opendir)也是文件资源,应用程序某线程每间隔15秒就访问一次目录。man opendir确认closedir是它的配对关闭函数。
SEE ALSOopen(2), closedir(3), dirfd(3), readdir(3), rewinddir(3), scandir(3), seekdir(3), telldir(3)
添加上closedir后故障得以修复。

顺带提一下

贴图用的搜索工具不是grep而是我自己写的脚本jgrep,它的用法和grep完全一样,输入前面的数字能打开对于文件所在行,对于搜索源码、系统配置文件检索、跳转特别适用。如果你对jgrep感兴趣的话,在我的公众号“程序员写个解”发送 “20220411” 可获取。




总结建议

BUG成功得以修复,它本是不应该犯的错误,在这里我给自己和读者建议:1、以后使用不熟悉的API,首先查阅他的帮助手册2、对于内存分配函数有相互独立的API,比如malloc对应free、mmap对应munmap。跟着手册建议的API去掉释放资源,避免不可预知的故障发生。最后,如果你觉得文章对你有所帮助,有启发作用。欢迎点击,把今天的内容分享给你的好友,和他一起讨论学习。
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

日前Intel的12代酷睿处理器BIOS源码泄露引发网络热议,网上信息显示文件容量高达6GB,主要用于创建及优化12代酷睿的UEFI BIOS。考虑到BIOS的重要性,这次泄露引发了很多人担心,不过Intel官方已经出来...

关键字: Intel 源码 酷睿 BIOS

作者:vivo互联网服务器团队-ZhangZhenglin一、简介RocketMQ是阿里巴巴开源的分布式消息中间件,它借鉴了Kafka实现,支持消息订阅与发布、顺序消息、事务消息、定时消息、消息回溯、死信队列等功能。Ro...

关键字: 源码 存储模块 ck

来源:https://www.cnblogs.com/deng-cc/p/6927447.html最近正好也没什么可忙的,就回过头来鼓捣过去的知识点,到Servlet部分时,以前学习的时候硬是把从上到下的继承关系和接口实...

关键字: IDE 源码 Diagram

一、前言老周这里编译Kafka的版本是2.7,为啥采用这个版本来搭建源码的阅读环境呢?因为该版本相对来说比较新。而我为啥不用2.7后的版本呢?比如2.8,这是因为去掉了ZooKeeper,还不太稳定,生产环境也不太建议使...

关键字: 源码 编译

国庆的时候闲来无事,就随手写了一点之前说的比赛的代码,目标就是保住前100混个大赛的文化衫就行了。现在还混在前50的队伍里面,稳的一比。其实我觉得大家做柔性负载均衡那题的思路其实都不会差太多,就看谁能把关键的信息收集起来...

关键字: 源码

点击上方“小麦大叔”,选择“置顶/星标公众号”福利干货,第一时间送达大家好,我是小麦,以前用单片机做用户交互的菜单的时候,都比较痛苦,如何写一个复用性高,方便维护,可扩展性高的GUI框架呢?当然可以自己动手写一个,这个过...

关键字: 单片机 源码

知道有多少人折腾过液晶显示的菜单,我觉得很多人都应该搞过,我还记得以前大学参加电子设计竞赛获奖的作品,我就用到了一个12864,里面有菜单功能。以前可能觉得菜单高大上,其实并不是想象中的复杂,本文为大家分享一个用单色屏做...

关键字: 源码

知道有多少人折腾过液晶显示的菜单,我觉得很多人都应该搞过,我还记得以前大学参加电子设计竞赛获奖的作品,我就用到了一个12864,里面有菜单功能。以前可能觉得菜单高大上,其实并不是想象中的复杂,本文为大家分享一个用单色屏做...

关键字: 源码

作者:vivo互联网服务器团队-YeWenhao一、RocketMQ架构简介1.1逻辑部署图(图片来自网络)1.2核心组件说明通过上图可以看到,RocketMQ的核心组件主要包括4个,分别是NameServer、Brok...

关键字: 源码 ck

公众号「程序员内点事」 对于Nacos大家应该都不太陌生,出身阿里名声在外,能做动态服务发现、配置管理,非常好用的一个工具。然而这样的技术用的人越多面试被问的概率也就越大,如果只停留在使用层面,那面试可能要吃大亏。比如我...

关键字: 模型 源码 os
关闭