动态内存分配及内存泄漏相关概念的案例教学
2019-01-23彭双和赵佳利
韩 静,彭双和,赵佳利
(北京交通大学 智能交通数据安全与隐私保护技术北京市重点实验室,北京 100044)
0 引 言
20世纪计算机刚刚兴起的时候,计算机内存较小,动态分配[1]的内存若不及时释放,会极大地占用系统资源,从而导致一系列的未知错误。因此,编程人员对于内存动态分配有着较强的内存释放意识。然而,随着计算机的不断更新发展,计算机内存不断扩大,学生对内存泄漏[2]的感知越来越弱,很多时候,我们根本察觉不到内存泄漏的存在,但这并不代表内存泄漏的危害会逐渐消失。内存泄漏的堆积最终会耗尽系统所有的内存,给应用程序带来极大的不稳定性。因此,内存分配与内存释放往往是成对出现,这样才能科学有效地利用内存空间。
同理,对文件进行操作时,打开文件与关闭文件也必须成对出现,这也是学生在使用C语言的文件I/O接口时经常出现的错误。不关闭文件会导致数据丢失,因为在向文件写数据时,首先将数据传输到缓冲区,待缓冲区充满后才正式输出给文件。如果数据未充满缓冲区而程序结束运行,缓冲区中的数据就会丢失。用fclose()函数关闭文件,先将缓冲区中的数据输出到磁盘文件,然后才释放文件指针变量,从而避免了数据丢失。
1 教学改革的必要性
虽然在教学过程中老师一再强调malloc()与free()以及fopen()与fclose()函数的成对出现,但在实际编程过程中,仍有部分学生没有养成良好的编程习惯,从而忽略了释放内存或关闭文件操作,而且对于编译器报错信息不太敏感的学生很难发现自己的错误所在,从而会盲目修改程序,浪费大量时间。这一方面是由于学生对内存分配以及文件操作知识掌握得不充分,不熟练;另一方面是没有意识到内存泄漏以及数据丢失造成的严重后果,没有引起大家的重视。因此,对于学生编程习惯以及程序安全意识的培养也格外重要。
此外,检测函数的参数及其返回值,在对危险函数检测、内存泄漏检测或者污点追踪等方面都有广泛的应用。Intel-Pin[3]作为一个强大的分析工具,在性能评估和漏洞检测等方面有着重大贡献。它提供一些接口(API),可以实现程序运行时各种信息的收集,利用这些 API,用户可以根据需要开发出各种分析工具,从而实现对可执行程序的动态分析[4],处理程序运行过程中生成的代码和属性,从中获取并将函数的调用、内存的访问、指令的执行等信息并将其记录下来,从而实现污点分析、指令路径追踪、协议逆向、漏洞挖掘等目的。这些工作都值得在此基础上继续深入挖掘研究,而且对于学生安全相关知识的培养也有很大帮助。
2 相关原理简介
2.1 Intel-Pin简介
Intel-Pin是Intel公司推出的一款动态二进制分析框架, 支持IA-32和x86-64指令集架构,可用于创建动态程序分析工具,然后可以使用这些工具来监视和记录程序运行时的行为。使用Pin创建的名为Pintool的工具可用于在用户空间应用程序上执行程序分析。
为获取malloc()函数返回值,我们通过Pin对程序进行插桩,在程序运行时获取其相关信息。作为一个动态的二进制检测工具,检测是在编译后的二进制文件的运行时执行的。 因此,它不需要重新编译源代码,并且支持动态生成代码的测试程序。换句话说,Pintool能完全控制程序运行时的执行,我们可以根据需要改变程序执行流程。Pin包含4种不同粒度级[5]的插桩,分别为:①指令级插桩(instruction instrumentatio),通过函数INS_AddInstrumentFunctio实现;②轨迹级插装(trace instrumentation),通过函数TRACE_AddInstrumentFunction实现;③镜像级插装(image instrumentation), 使 用 IMG_AddInstrumentFunction函数,由于其依赖于符号信息去确定函数边界,因此必须在调用PIN_Init之前调用PIN_InitSymbols;④函数级的插装(routine instrumentation),使用RTN_AddInstrumentFunction函数,函数级插装比镜像级插装更有效,因为只有镜像中的一小部分函数被执行。
此外,Pin提供了丰富的API记录、修改或对现有的编译二进制文件进行其他操作,表1列举了一些关键API及其功能描述。在想要分析一个程序但没有该程序的源代码的情况下,Pin尤其有用,这一特征也与要进行的实验十分吻合。
2.2 内存泄漏原理
内存泄漏(memory leak)是指程序中己动态分配的堆内存由于某种原因(程序未释放或无法释放),造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
没有释放动态分配的存储空间而造成内存泄漏,是使用动态存储变量的主要问题,尤其对于初学动态内存分配的学生而言,更容易犯这种错误,而且内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃,这也是需要我们在日常编程中注意及时释放内存的原因。
3 实验设计
实验的设计初衷及要实现的功能是在动态二进制插桩平台Pin上,通过编写插桩工具Pintool,对可执行程序进行动态插桩,从而成功检测到可执行程序中的malloc()和free()函数的参数及malloc()函数的返回值,通过结果分析可以观测到其成对出现,而且malloc()的返回值刚好是free()要释放的地址。此外,fopen()与fclose()原理与之相似,这里一并进行研究探讨,对于其他成对出现的函数,学生也可以此作为参考对其进行测试。
表1 调用函数功能描述
与普通程序一样,PinTool的入口位置依然为main()函数,主函数流程如图1所示。
Pintool插桩过程和分析过程的主要实现过程为:当代码经由Pin时,Pintool能触发Pin提供的回调例程,进而获取被插桩函数的信息。Pintool实现流程如图2所示。
图1 主函数流程图
图2 Pintool实现流程图
3.1 粒度选择
考虑到性能及开销,这里选择镜像级插桩,一旦加载了程序中的镜像,Pintool就可以在镜像中找到需要的函数名并插入分析代码,进而找到其参数。
3.2 分析例程
要使用Pin API,必须在代码中包含pin.h头文件,Pintool会将结果写入到一个输出文件。然后需要定义代码序列中特定点处执行的分析例程,分析例程决定了对测试程序进行的操作。在malloc和fopen执行后记录返回地址,每次调用malloc和fopen时都会调用该函数。
此外,预先定义一个布尔型的全局变量Record,用来控制是否打印记录,然后在分析函数中通过条件语句判断,从而过滤掉一些不必要的输出信息,使得输出结果更加明了,便于观察分析。
3.3 插桩例程
插桩例程的主要功能是告诉Pin何时执行分析例程。每次运行加载函数过程中都会调用该例程,加载程序函数时,Pin就可以在适当的点插入分析例程。
该模块主要是对malloc()和free()函数进行插桩。 打印每次调用malloc()、free()的大小以及malloc()的返回值。通过RTN_InsertCall函数在适当的点插入分析例程,该函数包括3个强制参数:首先是想要插桩的函数;其次是枚举类型IPOINT,用来指定插入分析例程的位置;最后是要插入的分析例程。参数必须由IARG_END终止,为了将fopen及malloc函数的返回值传递给分析例程,需要指定IARG_FUNCRET_EXITPOINT_VALUE。
例如,在对malloc函数的插桩过程中,第二个参数表示在malloc函数执行前执行 Arg1Before函数,并依次为 Arg1Before 函数传递两个参数。IARG_ADDRINT 表示第一个参数是 IARG_ADDRINT 型的常量,其值是宏FOPEN;IARG_FUNCARG_ENTRYPOINT_VALUE 表示第二个参数是仅在函数入口处有效的函数本身参数,其值是0。其核心代码如下:
3.4 结束例程
结束例程在插桩程序终止时调用,它包含了两个参数,一个是保存程序主函数返回值的代码参数,另一个参数用来传递附加信息给检测函数。
4 C语言程序的测试与结果分析
通过一个小程序来测试我们的Pintool,该程序中包含了打开文件与关闭文件的操作,并且通过malloc函数动态分配了一块地址空间,随后将其释放,其核心代码如下:
将该代码编译成可执行程序,即可在命令行中使用我们的Pintool工具对其进行检测。在终端输入如下命令,此时文件夹中不存在hh.txt文件,故打开失败。在控制台输出打开文件失败的信息,同时在Pintool创建的输出文件中可以查看函数信息,如图3所示。
创建hh.txt文件后,文件打开成功,此时控制台信息提示文件打开成功,此时Pintool的函数输出信息如图4所示。
图3 打开失败输出
图4 打开成功输出
实验结果分析:由图3和图4文件输出结果可见,若文件正常打开,可捕获到fclose()函数,而且最终会通过free()函数释放该地址空间,同样,对于malloc()函数,在其返回地址处依然有对应的free()函数;若文件打开失败,则不会调用关闭文件函数,而且函数也没有正常结束,有一块地址空间没有正常释放。通过对输出文件的分析,可以清楚地观测到malloc()函数与free()函数以及fopen()和fclose()函数的对应关系。
5 结 语
该工具通过镜像级插桩实现了对malloc()函数分配内存大小及其返回值的获取,同时监测其返回地址处是否调用free()函数来释放地址空间,从而在没有源文件的情况下得知其源代码是否规范,malloc()函数与free()函数是否对应。此外,我们也对fopen()函数和fclose()函数进行了监测。通过这种实验的方式,学生对这些函数有了更深入的理解,相较于老师在课堂上多次强调效果却微乎其微,学生自己动手操作更加直观、形象、深刻,能达到事半功倍的效果,同时也可以举一反三,通过一些微小的修改检测其他函数或者自定义函数的参数及返回值,在不断尝试中体会学习的乐趣,这对教学改革有着重大意义。该工具也可以应用在许多场景中,为后续工作带来了许多便利,这也是我们要进一步研究探索的方向。