“踩内存”引发的,内存问题分析总结

“踩内存”引发的,内存问题分析总结

背景

日常开发中,后端服务为了追求性能,常常会采用C/C++进行开发,享受C/C++带来性能提升的同时,开发者需要自己实现内存管理,只要程序实现上有缺陷,就会导致服务不可用,最近就遇到一次“踩内存”导致的服务不可用。

问题现象

同事A的新版本发布上线后,就收到告警消息,模块产生了大量的coredump文件,导致请求出现失败,运维同学经过简单排查,明确不是机器异常、网络故障引起的coredump。

同事A初步分析发现,每次程序出现coredump的位置不太一样,产生coredump的位置都不是新开发的代码,基本都在使用malloc进行内存分配时出现了异常,问题很像是内存出现异常。

分析过程

从linux的系统日志中,看到程序的coredump时,产生了以下报错。

free(): invalid pointer: 0x000000000900ba22 ***
general protection ip:7fdae4c988a0 sp:7ffc673242a0 error:0 in 
malloc(): smallbin double linked lilst corrupted: 0x0000000009ce16c0 ***

上述日志,可以直接理解为程序在释放内存时指针异常,程序访问了内存中保护区域,malloc 内部维护的内存管理链表出现异常,基本可以明确程序出现了“踩内存”导致内存管理出现异常。回退新版本后,不再产生coredump,进一步确定是新版本引入的问题。

分析方法

常见分析思路

根据过往经验,”踩内存”类问题,因缺乏明确的异常位置信息,一般都需要花费很长时间进行定位,主要的定位方法如下。

尝试复现,增加辅助日志,这时候就需要对产生coredump时的流量重放,这里强烈推荐使用k6作为流量重放工具。如果能够复现,这时候就可以使用日志分析法、排除法进行分析。

阅读代码,分析异常,有的case测试环境无法复现,这个时候就需要阅读新增代码分析潜在问题,需要结合过往经验,最好是能够多个人一起看。一般来说,将要上线的代码常见场景基本都测试通过了,要重点关注一些异常场景,分析新增代码可能带来的潜在风险

借助工具,分析风险,现在有很多用于分析内存问题的辅助工具,例如,Valgrind 或 AddressSanitizer,推荐使用AddressSanitizer,这个工具已经集成在GCC编译器,不过要使用4.8.5以上版本的GCC,可以直接在日志中看到潜在风险的代码行数。

ASAN使用方法

同事A的这个版本,修改点并不是特别多,都是一些常规的代码修改。几个同事一起看,也没有发现有啥明显的问题。只能借助工具进行分析,考虑到使用成本,我们在代码中集成了AddressSanitizer ,只需要在编辑脚本中增加参数即可,使用方法如下。

gcc -fsanitize=address -g your_program.c -o your_program 

export $ASAN_OPTIONS= log_path=./asan_log_%p:halt_on_error=0:detect_leaks=1:verbosity=1

使用上述参数会把检测到异常的堆栈写入到asan_log中,当检测到非致命错误时会继续执行。在使用中还需要安装AddressSanitizer库, 具体安装方法可以参考google 相关教程。

原因分析

同事A的这个模块,历史比较悠久,代码中很多坑。使用AddressSanitizer 工具以后,检测出以下几类问题。

  • new 和delete 使用不匹配,使用new 分配数组,却使用delete 释放对象,没有使用delete [],直接会报下列错误。
  • 数组越界,数组下标超过限制,向数组中写入数据,刚写入程序不会出现问题,可导致程序内存出现异常,例如malloc、free异常,具有一定的随机性。
  • 访问野指针,对象已经被释放了,还是会继续读取对象中的数据,根据经验在单线程一般不会有问题,也依赖编译器的实现。
new和delete 使用不匹配

根据扫描出来的问题,结合同学A的代码修改,基本明确就是因为数组越界引起的踩内存问题,数组大小为27,传入的index 却是-124,导致数组越界写入。引起内存异常,根据问题构造场景,100%复现问题。

总结

针对这个内存问题,我一直都在思考如何减少此类问题产生,我想到了以下方法。

  • 对于历史模块的修改,要评估修改的每一行代码会产生什么影响,例如参考之前的修改,调用了一个相同的函数,可要关注函数传入的参数,是否会不符合预期。
  • 尽力不要复制代码,很多开发喜欢复制一段旧代码,再针对性修改,缺乏思考的过程,很容易产生BUG。
  • 关注编译告警,参数非法、类型不匹配,隐式转换,这些问题在编译过程中,都会产生告警,及时处理,规避异常BUG。
  • 定期review,代码review不依赖其他同学,在每个功能开发完毕后,要review本地修改会带来哪些影响,会不会产生问题,不要过度自信。

上述此次内存异常问题的分析过程,以及AddressSanitizer 的使用方法,希望能够对各位同仁分析内存异常类问题有帮助,欢迎大家积极讨论,减少程序BUG 产生。

4 thoughts on ““踩内存”引发的,内存问题分析总结

发表评论

邮箱地址不会被公开。 必填项已用*标注