Address Sanitizer(Asan)原理及实战定位

  • Asan

    ASAN(AddressSanitizer的缩写)是一款面向C/C++语言的内存错误问题检查工具,可以检测如下内存问题:

    • 使用已释放内存(野指针)
    • 堆内存越界(读写)
    • 栈内存越界(读写)
    • 全局变量越界(读写)
    • 函数返回局部变量
    • 内存泄漏

    ASAN工具主要由两部分组成:

    • 编译器插桩模块
    • 运行时库

    运行时库:(libasan.so.x)会接管malloc和``free函数。malloc执行完后,已分配内存的前后(称为“红区”)会被标记为“中毒”状态,而释放的内存则会被隔离起来(暂时不会分配出去)且也会被标记为“中毒”状态。

    编译器插桩模块:加了ASAN相关的编译选项后,代码中的每一次内存访问操作都会被编译器修改为如下方式:

    编译前:

    *address = ...;  // or: ... = *address;
    

    编译后:

    if (IsPoisoned(address)) {
      ReportError(address, kAccessSize, kIsWrite);
    }
    *address = ...;  // or: ... = *address;
    

    该方式的关键点就在于读写内存前会判断地址是否处于“中毒”状态,还有如何把IsPoisoned实现的非常快,把``ReportError实现的非常紧凑,从而避免插入的代码过多。

    ASan对缓冲区溢出防护的的基本步骤如下:

    \1. 通过在被保护的栈、全局变量、堆周围建立标记为中毒状态(Poisnoned)的red-zones;

    以栈缓冲区溢出检测为例,如下代码展示了red-zone的建立。

    将缓冲区和red-zone通过每8字节对应1字节的映射的方式建立影子内存区,影子内存区的获取函数为MemToShadow。

    \3. 如果出现对red-zone的读、写或执行的访问,则ASan可以ShadowIsPoisoned检测出来并报错。

    报错信息给出出错的源文件名、行号、函数调用关系、影子内存状态。其中影子内存状态信息中出错的部分用中括号标识出来。如下所示:

    二. 内存映射与插桩

    进程的虚拟内存空间被ASAN划分为2个独立的部分:

    • 主应用内存区 (Mem): 普通APP代码内存使用区。
    • 影子内存区 (Shadow): 该内存区仅ASAN感知,影子顾名思义是指该内存区与主应用内存区存在一种类似“影子”的对应关系。ASAN在将主内存区的一个字节标记为“中毒”状态时,也会在对应的影子内存区写一个特殊值,该值称为“影子值”。

    这两个内存区需要精心划分,确保可以快速从主应用内存区映射到影子内存区(MemToShadow)。

    主应用内存与影子内存Shadow bytes映射

    ASAN将8字节的主应用区内存映射为1字节的影子区内存。

    针对任何8字节对齐的主应用区内存,总共有9种不同的影子内存值:

    • 4字中的全部8字节都未“中毒”(可访问的),影子值是0。

    • 4字中的全部8字节都“中毒”(不可访问的),影子值是负数。

    • k个字节未“中毒”,后8-k字节“中毒”,影子值是``k。这一功能的达成是由malloc函数总是返回8字节对齐的内存块来保证的,唯一能出现该情况的场景就在申请内存区域的尾部。例如,我们申请13个字节,即malloc(13),这样我们会得到一个完整的未“中毒”的4字和前5个字节未“中毒”、后3个字节“中毒”的4字。

    ASAN日志格式

    每条错误记录都是以一串"="号开始的,具体内容包含(重点关注标红的字段):

    错误进程号:内存问题发生的Linux进程号

    错误类型:global-buffer-overflow/(全局变量越界), heap-buffer-overflow 堆内存越界, stack-buffer-overflow栈内存越界, double-free(重复释放内存) 内存越界调用栈:重复内存错误的调用栈信息,如果要显示文件名行号,需要加-g选项 上次内存释放调用栈:上一次释放内存的调用栈信息 内存申请调用栈:内存申请的调用栈信息

    越界地址说明:对出错时访问的内存地址做进一步说明,以便更直观的判断问题所在影子内存状态、影子内存魔术字、Asan内部信息,分析问题是可不关注

    分析Asan报错日志

    ==7829==ERROR: LeakSanitizer: detected memory leaks
    
    Direct leak of 7 byte(s) in 1 object(s) allocated from:
        #0 0x42c0c5 in __interceptor_malloc /usr/home/lib/asan/asan_malloc_linux.cc:74  -- 泄漏内存的申请调用栈
        #1 0x43ef81 in main /usr/home/memory-leak.c:5
        #2 0x7fef044b876c in __libc_start_main /build//glibc-2.15/csu/libc-start.c:226
    
    SUMMARY: AddressSanitizer: 7 byte(s) leaked in 1 allocation(s).
    

    上面的代码内存泄漏很明显,第6行代码将保存申请内存的P指针置空,导致内存泄漏了;而生成的日志内容也挺简单:

    "7829"表示程序的进程号,后面的"ERROR:"表明ASAN发现了内存泄漏,如果没有发现内存泄漏,这些信息都不会打,也不会生成相关日志;

    换一行的"leak of 7 byte(s)"表明内存泄漏了7个字节,"1 object(s)“表明泄漏了1次,而泄漏内存的申请调用栈就在后面打印了,其中”/usr/home/memory-leak.c:5"表明内存申请的文件名、行号。

    函数插桩

    可以自定制新开发功能,基于ASAN(AddressSanitizer的缩写)的文件句柄问题检查功能, 采用LD_PRELOAD机制和dlsym接口,接管libc库的open/close等接口,并进行判断与调用栈记录。就像Asan使用自定义的运行时库函数接管malloc/free,new/delete,mem*等函数。

    ASAN内存泄漏检测原理如下:

    1、ASAN会接管内存申请接口,即用户的内存全都由ASAN来管理;

    2、当进程退出时触发ASAN内存泄漏检测,开发可以使用复位单板、重启设备等一切可以让进程正常退出的方法来触发ASAN进行内存泄漏检测;

    3、开始内存泄漏检查后,ASAN会遍历当前所有已经分配给用户但没有释放的堆内存,扫描这些内存是否被某个指针引用着,这些指针可能是全局变量、局部变量或者是堆内存里面的指针,如果没有则认为是泄漏了;如果内存被引用着,比如内存申请完后放在V8的Local数据区、插树、插链表等,是不会报泄漏的;

    4、将所有泄漏的内存信息打出来,包含内存大小和内存申请的调用栈信息;

    因此,如果日志中报内存泄漏了,开发同学需要分析该内存申请的指针是存放到哪儿的,在流程中这个指针是在哪儿被改了,但是没有释放内存

    讲一个会导致内存泄漏的场景,某基础模块发现4个内存泄漏问题,都是内存申请后存放到全局变量中,开发说该内存会一直使用不释放,不算泄漏,但实际上该全局变量会被初始化两次,第二次没有判断全局变量是否已经初始化,而直接赋成新的内存指针值,导致泄漏。

    已知ASAN内存泄漏误报场景:

    1、结构体非4字节对齐:例如报结构体A内存泄漏,且A内存的指针存放在结构体B中,由于ASAN扫描内存时是按4字节偏移进行的,所以如果A指针在结构体B中的偏移非4的整数倍,ASAN就扫不到,出现误报(主干要求:非4字节对齐的结构体需要整改);

    2、信号栈内存:该内存是在信号处理函数执行时做栈内存用的,其指针会保存到内核中,所以ASAN扫描不到,产生误报;

    3、内存指针偏移后保存:有子系统代码实现比较特殊,将申请的内存指针做一个很大的偏移(超过该内存的合法范围)操作后保存起来,通过偏移后的值ASAN根本找不到对应的内存,所以误报泄漏;

    4、存在ASAN未监控的内存接口

ASAN使用要添加的编译选项

-fsanitize=address -fsanitize=undefined -fsanitize=leak -fsanitize-recover=all -fno-omit-frame-pointer -fno-stack-protector -fsanitize=leak

set(CMAKE_C_FLAGS	" -m64 -Wno-write-strings -fgnu89-inline -fexceptions  -fno-inline -rdynamic -fsanitize=address -fsanitize-recover=all  -fsanitize=leak")
set(CMAKE_CXX_FLAGS	" -std=c++11 -m64 -Wno-write-strings -fexceptions  -fno-inline -rdynamic -fsanitize=address -fsanitize-recover=all  -fsanitize=leak")

-fsanitize=address -fsanitize=undefined -fsanitize=leak -fsanitize-recover=all -fno-omit-frame-pointer -fno-stack-protector -fsanitize=leak

更多入门资料可参考
https://bbs.huaweicloud.com/blogs/100056
https://www.jianshu.com/p/3a2df9b7c353

你可能感兴趣的:(C++/C内存问题检测工具)