内存越界类问题都不太好搞,但也充满乐趣,有如侦探抽丝剥茧般。因为处理过好些这类问题,也就有想法将这些手段总汇起来,建立成一个辑录供参考。实际第一篇早已做出,即GCC SSP Canary功能简介一篇,这也就是第二篇了。从electric-fence开始主要因其代码简单,一来方便阅读,二来方便自己修改优化。小工具用好即为神器,会用然后再做修改,便能顺心随意。本文最后的总结也提及一个修改tcmalloc实现自己需求的团队例子,器终归是器,用的人才是决定其价值的关键。
electric-fence 是一款malloc类函数内存调试工具,主要通过mprotect对memalign、malloc、free、valloc、calloc、strndup、strdup、realloc等函数进行重写,实现越界防护检测机制。这个实现也就决定其只能用于内存越界访问行为debug,没法检测内存泄漏,也无法准确定位C++中new, new[], delete, delete[]的问题(但可以大概定位)。想起曾有大佬让我用efence查内存泄漏,而且还是对象泄漏问题…
efence的核心实现由initialize、memalign、free组成,捋顺这三个,整个工作机制也就打通了。
通过LD_PRELOAD和GDB使用eFence
如果你只想了解最核心的原理,可以直接通过本文memalign实现 – allocator核心一节了解,然后直接参考使用efence检测内存泄漏问题这篇文章就可顺溜地使用efence了。这里也补充介绍两种debug方式:LD_PRELOAD和GDB。
如果是已经编译好但未链接libefence的bin档,我们可以通过”LD_PRELOAD=./libefence.so bin”来预加载libefence.so再执行bin,实现efence符号对libc中malloc符号的覆盖。
gdb需在.gdbinit增加如下代码,也可以在此基础上也可增加你需要的配置(具体参考efence - Linux man page介绍)。然后在debug对应程序时,执行efence on即可开启对应功能。
1 | define efence |
initialize函数 – 配置库运行功能和创建内存索引
顾名思义,initialize函数完成整个库的初始化和环境配置工作。其实现主要分为两部分,第一部分是通过环境变量获取各项功能开关设置,第二部分是申请一块不小于1M的内存并配置相关数据结构。
功能开关配置
使能对应功能,首先需要code中配置对应变量为-1,然后通过同名环境变量来实现对对应功能的开关,相关变量大概说明如下。
- EF_DISABLE_BANNER:是否打印库版本信息
- EF_ALIGNMENT:Efence malloc分配空间的内存对齐字节数,默认值为sizeof(int),这个值也是Efence能够检测的内存越界的最小值。
- EF_PROTECT_BELOW:默认情况下,efence检测是高地址越界问题,若将此值设置为1,则表示检测内存低地址越界问题。
- EF_PROTECT_FREE:使能use after free检测。
- EF_ALLOW_MALLOC_0:是否检查malloc(0)行为
- EF_FREE_WIPES:free内存后,是否对该区域填充0xbd
1 | /* |
Initialize的内存操作部分
Initialize的内存操作主要实现流程如下:
- 第一次申请内存时,通过mmap向操作系统申请不少于MEMORY_CREATION_SIZE字节内存,code中设定为1MB,该值实际会自动向上取最小满足页对齐的大小。
- 申请到的内存第一页用于存放slot结构体数组。slot用于管理被分配的各个内存单元信息,如返回给用户的实际地址、内部实际地址、大小等,具体如下。
- slot[0]用于存放slot结构体数组起始地址和当前数组大小的信息。
1 | struct _Slot { |
代码解析如下
1 | size_t size = MEMORY_CREATION_SIZE; // MEMORY_CREATION_SIZE = 1024*1024; |
memalign函数 – allocator核心
memalign是整个efence的核心部分,所申请的内存块都是以页为单位(受限于mprotect),其取值为最小满足用户申请的内存大小的页数再加一,多加的这一页就是eFence工作的根本。如下例子中,用户申请的内存小于一页,而memalign实际申请了两页内存。
- 当EF_PROTECT_BELOW为0、查高地址越界访问时,allocator会设置Page 1为PROT_NONE,Page 0为RW,然后Page 1的起始地址addr - sizeof(Variables)即为返回的地址(落在Page0中)。
- 当EF_PROTECT_BELOW非0、查低地址越界访问时,Page 0会被设置为PROT_NONE,Page 1起始地址为返回地址。
- 完成如上设置,当出现内存越界访问时,就会对PROTECTED区域进行读写,导致页错误而coredump,这也就是efence实现的原理了。

realization of efence
memalign函数的具体实现分为下面几步,显然,在大量申请小内存的场景中,efence如果没有及时释放申请的内存,内存将会严重碎片化。
- 对用户申请内存的大小做预处理。检查是否malloc(0)->按函参alignment大小对齐->增加1个页的大小,然后取最小满足该内存大小的最小页数(即internalSize)
- 查询空闲的slot用于记录本次申请。如果当前未使用的slot数小于7,申请一块大slot数组一个页的内存,扩展slot数组。
- 内存分配,从slot数组FREE的记录中查找满足internalSize的内存,最终可分为如下三种case:
1)刚好有一块大小满足internalSize的内存,直接使用;
2)有不少于一块大于申请大小的内存。选取最小的一块,将这块内存分割为两部分,一部分作为结果返回给用户,剩余部分用一个NOT_IN_USE的slot记录供下次申请;
3)所有记录都小于申请的大小。重新申请一块不小于1M且页对齐的内存,并用一个空闲的slot(NOT_IN_USE)记录下来,然后执行第2个case的操作。
1 | /* |
free函数 – 碎片化问题处理
基本上,通过对memalign的解析,我们大概可以预想free的功能要怎么实现了。
- 如果开启EF_PROTECT_FREE,也就是UAF检测,则将slot->mode = PROTECTED,不再被使用,否则设置为FREE供后续使用
- 如果开启EF_FREE_WIPES,也就是poison memory的话,则将对应内存memset为0xbd
- 将free的内存区域mprotect为PROT_NONE
然后我没想到的是上面提及的内存碎片化,作者在这里做了处理,即在free时候尝试合并前后同类块以降低碎片化,不过看起来有bug。
1 | extern C_LINKAGE void free(void * address) |
总结
经过如上分析,我们可以发现efence并不是很完善,还有一些优化的空间。另外他也有如下局限性,
1、malloc和free在slot都是线性查找,复杂度为O(n),性能较低。
2、内存消耗大,特别对于频繁的小内存分配,每次至少申请两个页,内存利用率低。
3、无法对同一块内存同时做上下越界检查。
按照我对eFence这版code的理解和阅读过程中产生的疑问,我做了部分修改,具体参考commit,还有待验证。而我也有想法尝试优化这个实现,一方面是锻炼自己,一方面也是做一个新的挑战,虽然efence跟ASAN确实还是天壤之别的。这次也是因为看到一个团队对tcmalloc进行客制化,实现很好的内存管控,也就有想法多了解些开源方案,当后续遇到类似问题可进行定制,进而提高工作效率。具体就是知乎上这个例子了。
1 | 我们团队的同事搞出了一套终极解决方案用于解决各种内存相关问题(例如内存泄漏,内存被踩坏等),很好用。简单来说就一句话修改tcmalloc,加入audit信息。具体修改包括如下2个方面: |
v1.5.2