2021SC@SDUSC 本篇博客讨论一下测试部分的内存泄露的处理 参考博客senlin‘s blog
目录
内存泄漏的理论知识以及valgrind介绍
定义: 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。 检测方法: 1、静态分析技术 静态分析技术就是直接分析程序的源代码或机器代码,获得一些有用的信息,而并不运行程序本身。目前有许多静态分析的工具,编译器就属于这一类,它读入源程序代码,对源程序进行词法和语法分析,进行数据类型的检查以及一些优化的分析等,以此来提高程序的质量与运行效率。这类静态的分析工具仅仅是读入程序代码进行相关的分析,而并不进行其它额外的操作,如修改源程序代码等。
2、源代码插装技术 为了获得被测程序的动态执行信息,需要对其进行跟踪,一般使用插装方法。所谓插装就是在保持被测程序的逻辑完整性的基础上,在被测程序的特定部位插入一段检测程序又称探针函数,通过探针的执行抛出程序的运行特征数据。基于这些特征数据分析,可以获得程序的控制流及数据流信息,进而获得逻辑覆盖等动态信息,这样就可以在被测程序执行的过程中动态地同步执行程序的检测工作。 基于源代码插装的动态测试框架分为个主要的阶段:
插装交互与动态测试信息分析;
插装阶段;
插装库制作阶段;
测试实施阶段。
3、目标代码插装技术 目标代码插装实现主要分为预处理、测试执行和结果汇总个阶段,系统主要工作是围绕断点而进行的。在预处理阶段,首先静态分析被测程序的目标代码,查找待测程序中源代码各语句、函数入口点在目标代码中的对应位置,然后在相应位置插入断点在测试执行阶段,启动调试进程,当被测程序执行到断点处时,响应断点信息,在相应的断点处完成相应的统计操作在结果汇总阶段,根据各断点处的统计结果,按不同的统计角度进行归并、综合得到最终的统计数据。
第一阶段在测试预处理阶段对被测程序的目标代码进行分析,可以获得目标代码与源代码中语句、函数的对应关系。在目标代码中为相对应的源代码的每条语句及每个函数的入口点插入断点。对于第三方代码,只要其目标代码格式与下生成的目标代码格式一致,我们就可以用与分析用户代码同样的方法获取信息。获取断点的信息后,为所有的断点建立断点链表,同时建立语句及函数的信息链表,供随后的测试执行阶段存储信息。
第二阶段利用OCI技术,我们把测试执行看作是一个在被测进程和检测进程间不断切换的过程。每当被测进程遇到断点,就会将自身挂起,同时发送消息唤醒检测进程,检测进程根据当前断点的地址在断点链表中查找相应节点,并查找对应的语句或函数信息,记录该语句或函数的执行次数、到达或离开的时刻,供以后统计之用。然后,将插入的断点信息去除,恢复原来的指令,转入被测进程继续执行。在转入被测进程之前,必须将上一个断点处的断点恢复上一个断点处的断点在指令运行时被去除了。
第三阶段根据各断点处的统计结果,按不同的统计角度进行归并、综合,进行覆盖率及各种时间的计算,得到最终的统计数据。是公司出品的一种软件测试和质量保证工具,它能检测程序内存泄漏和内存访问冲突等错误。使用目标码插装技术,在编译器生成的目标码中直接插入特殊的检查指令实现对内存错误的检测。在程序的所有代码中插入这些检查逻辑,包括第三方目标码库,并且验证系统调用的接口。目标码插装技术分为链接前插装和链接后插装两种插装方法。检查插装后程序的每个内存读写动作,跟踪内存使用情况,使用类似垃圾收集器的技术来检查内存泄漏。垃圾收集机制分为两阶段垃圾检测和垃圾回收。为了不影响程序的执行速度,提供了一个可调用的垃圾检测器,使用类似于保守式垃圾收集算法,即标记一清除算法。在标记阶段,递归地从数据段、堆栈段到数据堆跟踪分析指针,并使用标准保守式方法为所有被引用的内存块做标记。在清除阶段,逐步访问数据堆,并报告已分配但程序不再引用的内存块,即程序的内存泄漏。
什么是valgrind? Valgrind是一套Linux下,开放源代码的仿真调试工具的集合。Valgrind由内核以及基于内核的其他调试工具组成。内核类似于一个框架,它模拟了一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件 ,利用内核提供的服务完成各种特定的内存调试任务。valgrind主要包括以下工具: 1.Memcheck 最常用的工具,用来检测程序中出现的内存问题,所有对内存的读写都会被检测到,一切对malloc()/free()/new/delete的调用都会被捕获。所以,它能检测以下问题: 对未初始化内存的使用; 读/写释放后的内存块; 读/写超出malloc分配的内存块; 读/写不适当的栈中内存块; 内存泄漏,指向一块内存的指针永远丢失; 不正确的malloc/free或new/delete匹配; memcpy()相关函数中的dst和src指针重叠。 2.Callgrind 和gprof类似的分析工具,但它对程序的运行观察更是入微,能给我们提供更多的信息。和gprof不同,它不需要在编译源代码时附加特殊选项,但加上调试选项是推荐的。Callgrind收集程序运行时的一些数据,建立函数调用关系图,还可以有选择地进行cache模拟。在运行结束时,它会把分析数据写入一个文件。callgrind_annotate可以把这个文件的内容转化成可读的形式。 3.Cachegrind Cache分析器,它模拟CPU中的一级缓存I1,Dl和二级缓存,能够精确地指出程序中cache的丢失和命中。如果需要,它还能够为我们提供cache丢失次数,内存引用次数,以及每行代码,每个函数,每个模块,整个程序产生的指令数。这对优化程序有很大的帮助。 4.Helgrind 它主要用来检查多线程程序中出现的竞争问题。Helgrind寻找内存中被多个线程访问,而又没有一贯加锁的区域,这些区域往往是线程之间失去同步的地方,而且会导致难以发掘的错误。Helgrind实现了名为“Eraser”的竞争检测算法,并做了进一步改进,减少了报告错误的次数。不过,Helgrind仍然处于实验阶段。 5.Massif 堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。 此外,lackey和nulgrind也会提供。Lackey是小型工具,很少用到;Nulgrind只是为开发者展示如何创建一个工具。
valgrind的使用 使用 Valgrind 分析 C++ 程序时,有一些问题需要留意。
问题:一个程序并没有发生内存泄漏,但是从valgrind分析代码中的HEAP SUMMARY可以看到,程序分配了 2 次内存,但却只释放了 1 次内存,为什么会这样呢? 答:实际上这是由于 C++ 在分配内存时,为了提高效率,使用了它自己的内存池。当程序终止时,内存池的内存才会被操作系统回收,所以 Valgrind 会将这部分内存报告为reachable 的,需要注意,reachable 的内存不代表内存泄漏。
检测越界访问 C++ 程序经常出现的 Bug 就是数组越界访问,例如下面的程序出现了越界访问:
#include <vector>
#include <iostream>
int main()
{
std::vector<int> v(10, 0);
std::cout << v[10] << std::endl;
return 0;
}
使用 Valgrind 分析这段程序,Valgrind 会提示越界访问:
$ g++ -std=c++11 -g -o main_cpp main.cpp
$ valgrind --tool=memcheck --leak-check=full ./main_cpp
==31523== Memcheck, a memory error detector
==31523== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31523== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31523== Command: ./main_cpp
==31523==
==31523== Invalid read of size 4
==31523== at 0x400AD7: main (main.cpp:7)
==31523== Address 0x5ab5ca8 is 0 bytes after a block of size 40 alloc'd
==31523== at 0x4C2E216: operator new(unsigned long) (vg_replace_malloc.c:334)
==31523== by 0x4010D3: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (new_allocator.h:104)
==31523== by 0x401040: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (alloc_traits.h:491)
==31523== by 0x400F91: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (stl_vector.h:170)
==31523== by 0x400E7E: std::_Vector_base<int, std::allocator<int> >::_M_create_storage(unsigned long) (stl_vector.h:185)
==31523== by 0x400D1E: std::_Vector_base<int, std::allocator<int> >::_Vector_base(unsigned long, std::allocator<int> const&) (stl_vector.h:136)
==31523== by 0x400C11: std::vector<int, std::allocator<int> >::vector(unsigned long, int const&, std::allocator<int> const&) (stl_vector.h:291)
==31523== by 0x400AB9: main (main.cpp:6)
Invalid read of size 4表示越界读取 4 个字节,这个操作出现在main.cpp文件的第 7 行。另外可以看到,vector分配了一块 40 字节的内存,程序越界访问紧急着这块内存之后的 4 个字节。
另一种经常出现的 Bug,就是程序访问了未初始化的内存。
#include <iostream>
int main()
{
int x;
if (x == 0)
{
std::cout << "X is zero" << std::endl;
}
return 0;
}
使用 Valgrind 检测这个程序:
$ g++ -std=c++11 -g -o main_cpp main.cpp
$ valgrind --tool=memcheck --leak-check=full ./main_cpp
==31554== Memcheck, a memory error detector
==31554== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31554== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31554== Command: ./main_cpp
==31554==
==31554== Conditional jump or move depends on uninitialised value(s)
==31554== at 0x400852: main (main.cpp:6)
输出中提示了main.cpp文件的第 6 行访问了未初始化的内存。
内存泄漏检测代码分析
首先,写出主函数的部分,调用test函数。理论上,使用单个秘密输入进行测试就足够了:如果控制流依赖于秘密,那么工具将生成错误。
测试上下文随机化。最后执行此操作,因为它会使上下文受到污染。 测试公钥、序列化
测试签名。 测试ECDH 对可恢复签名进行测试签名 测试密钥对创建和密钥对调整添加。 在keypair_tweak_add 中,该调整不会被视为秘密
|