你处理过哪些低级BUG?见笑,我耗时8小时排查低级BUG|popen内存泄漏
扫描二维码
随时随地手机看文章
最不值得一提的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时有个知识点:为了防止某用户打开过多的文件,系统对进程件访问数目有限制,默认是1024。2016年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” 可获取。