Linux下内存检测工具:asan
1.GCC 版本 6.3
2.安装asan
yum install devtoolset-6-libasan-devel
3.注意
asan只是开发中使用工具,因此只能在debug模式下有效 不能用于release版本
介绍
首先,先介绍一下 Sanitizer 项目,该项目是谷歌出品的一个开源项目,该项目包含了 ASAN
、LSAN
、MSAN
、TSAN
等内存、线程错误的检测工具,这里简单介绍一下这几个工具的作用:
-
ASAN: 内存错误检测工具,在编译命令中添加
-fsanitize=address
启用 -
LSAN: 内存泄漏检测工具,已经集成到 ASAN 中,可以通过设置环境变量
ASAN_OPTIONS=detect_leaks=0
来关闭ASAN
上的LSAN
,也可以使用-fsanitize=leak
编译选项代替-fsanitize=address
来关闭ASAN的内存错误检测,只开启内存泄漏检查。 -
MSAN: 对程序中未初始化内存读取的检测工具,可以在编译命令中添加
-fsanitize=memory -fPIE -pie
启用,还可以添加-fsanitize-memory-track-origins
选项来追溯到创建内存的位置 -
TSAN: 对线程间数据竞争的检测工具,在编译命令中添加
-fsanitize=thread
启用 其中ASAN
就是我们今天要介绍的重头戏。
ASAN
,全称 AddressSanitizer,可以用来检测内存问题,例如缓冲区溢出或对悬空指针的非法访问等。
根据谷歌的工程师介绍 ASAN
已经在 chromium 项目上检测出了300多个潜在的未知bug,而且在使用 ASAN
作为内存错误检测工具对程序性能损耗也是及其可观的。
根据检测结果显示可能导致性能降低2
倍左右,比Valgrind
(官方给的数据大概是降低10-50
倍)快了一个数量级。
而且相比于Valgrind
只能检查到堆内存的越界访问和悬空指针的访问,ASAN
不仅可以检测到堆内存的越界和悬空指针的访问,还能检测到栈和全局对象的越界访问。
这也是 ASAN
在众多内存检测工具的比较上出类拔萃的重要原因,基本上现在 C/C++ 项目都会使用ASAN
来保证产品质量,尤其是大项目中更为需要。
1、编译选项
1.1 Gcc编译选项
# -fsanitize=address:开启内存越界检测
# -fsanitize-recover=address:一般后台程序为保证稳定性,不能遇到错误就简单退出,而是继续运行,采用该选项支持内存出错之后程序继续运行,需要叠加设置ASAN_OPTIONS=halt_on_error=0才会生效;若未设置此选项,则内存出错即报错退出
ASAN_CFLAGS += -fsanitize=address -fsanitize-recover=address
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS } -fsanitize=address -fsanitize-recover=address")
1.2 Ld链接选项
ASAN_LDFLAGS += -fsanitize=address -g1
如果使用gcc链接,此处可忽略。
2、ASAN运行选项
2.1 ASAN_OPTIONS设置
ASAN_OPTIONS是Address-Sanitizier的运行选项环境变量。
# halt_on_error=0:检测内存错误后继续运行
# detect_leaks=1:使能内存泄露检测
# malloc_context_size=15:内存错误发生时,显示的调用栈层数为15
# log_path=/home/xos/asan.log:内存检查问题日志存放文件路径
# suppressions=$SUPP_FILE:屏蔽打印某些内存错误
export ASAN_OPTIONS=halt_on_error=0:use_sigaltstack=0:detect_leaks=1:malloc_context_size=15:log_path=/home/xos/asan.log:suppressions=$SUPP_FILE
除了上述常用选项,以下还有一些选项可根据实际需要添加:
# detect_stack_use_after_return=1:检查访问指向已被释放的栈空间
# handle_segv=1:处理段错误;也可以添加handle_sigill=1处理SIGILL信号
# quarantine_size=4194304:内存cache可缓存free内存大小4M
ASAN_OPTIONS=${ASAN_OPTIONS}:verbosity=0:handle_segv=1:allow_user_segv_handler=1:detect_stack_use_after_return=1:fast_unwind_on_fatal=1:fast_unwind_on_check=1:fast_unwind_on_malloc=1:quarantine_size=4194304
2.2 LSAN_OPTIONS设置
LSAN_OPTIONS是LeakSanitizier运行选项的环境变量,而LeakSanitizier是ASAN的内存泄漏检测模块,常用运行选项有:
# exitcode=0:设置内存泄露退出码为0,默认情况内存泄露退出码0x16
# use_unaligned=4:4字节对齐
export LSAN_OPTIONS=exitcode=0:use_unaligned=4
ASAN 的基本原理
ASAN
的内存检测方法与Valgrind
的AddrCheck
工具很像,都是使用shadow内存
来记录应用程序的每个字节是否可以被安全的访问,在访问内存时都对其映射的shadow内存
进行检查。但是,
ASAN
使用一个更具效率的shadow内存
映射机制和更加紧凑的内存编码来实现,并且除了堆内存外还能检测栈和全局对象中的错误访问,且比AddrCheck
快一个数量级。
ASAN
由两部分组成:代码插桩模块和运行时库。
代码插桩模块会修改代码使其在访问内存时检查每块内存访问状态,称为
shadow 状态
,以及在内存两侧创建redzone
的内存区域。运行时库则提供一组接口用来替代
malloc
和free
以及相关的函数,使得在分配堆空间时在其周围创建redzone
,并在内存出错时报告错误。首先,我们先介绍一下什么是
shadow 内存
和redzone
。
shadow 内存
在
ASAN
中malloc
函数返回的内存地址通常至少是8
个字节对齐,比如malloc(15)
将分配得到2
块大小为8
字节的内存,在这个场景中,第二块8
字节内存的前5
个字节是可以访问,但剩下的3
个字节是不能访问的。所谓的
shadow 内存
就是在应用程序的虚拟地址空间中预留一段地址空间,用来存储映射应用程序访问的内存块中哪些字节可以被使用的信息,这些信息就是shadow 状态
。其中每1
个字节的shadow 内存
,映射到8
个字节的应用程序内存,因此,shadow状态
可能有3种:
ASAN
使用带有比例和偏移量的直接映射将应用程序地址转换为其对应的shadow内存
地址:
shadow_address = (addr >> 3) + offset
假设
max - 1
是虚拟地址空间中的最大有效地址,则offset
的值应选择为在启动时不被占用的从offset
到offset+Max/8
的区域。以下是 32 位 linux 系统中的地址空间分布
0x1 0000 0000 --------------- | HIGH | | MEMORY | 0x4000 0000 --------------- | HIGH SHADOW | 0x2800 0000 --------------- | BAD REGION | 0x2400 0000 --------------- | LOW SHADOW | 0x2000 0000 --------------- | LOW MEMORY | 0x0000 0000 ---------------虚拟地址空间被划分为高低两部分,每个部分的内存地址映射到相应的
shadow 内存
。注意:将shadow 内存
中的地址进行映射会得到Bad 区域
中的地址,Bad 区域
是被页面保护标记为不可访问的地址空间。
shadow
映射方式可以推导为(addr >> scale) + offset
的形式,其中scale
是的取值范围是1~7
,当scale=N
时,shadow 内存
占用虚拟地址空间的1/2^N
,red-zone
的最小大小为2^N
字节(保证malloc()
的对齐要求)。shadow 内存
中的每个字节描述了2^N
个内存字节的状态并有2^N + 1
个不同的值。
在 32 位 linux 系统中,虚拟地址空间为:
0x00000000-0xffffffff
,offset = 0x20000000(2^29)
。在 64 位系统中,
ofsset = 0x0000100000000000(2^44)
。在某些情况下(例如,在 Linux 上使用
-fPIE/-pie
编译器标志)可以使用零偏移来进一步简化检测。
0: 表示映射的
8
个字节均可以使用k(1<=k<=7): 表示表示映射的8个字节中只有前
k
个字节可以使用负值: 表示映射的
8
个字节均不可使用,且不同的值表示所映射不同的内存类型(堆、栈、全局对象或已释放内存)redzone
ASAN
会在应用程序使用的堆、栈、全局对象的内存周围分配额外内存,这个额外的内存叫做redzone
,redzone
会被shadow 内存
标记为不可使用状态,当应用程序访问redzone
内存时说明已经溢出访问了,此时,ASAN
检测redzone
的shadow 状态
后就会报告相应错误。readzone
越大,检测内存下溢和上溢的范围越大。具体的分配策略将在下面涉及。代码插桩
ASAN
会在应用程序访问内存的位置进行插桩,对于访问完整8字节内存的位置,插入以下代码检查内存对应的 shadow 内存,以此判断是否访问异常:
ShadowAddr = (Addr >> 3) + Offset; if (*ShadowAddr != 0) ReportAndCrash(Addr);由于应用程序访问8字节的内存,因此,其映射的
shadow 内存
的存储值必须是0
,表示该8字节内存完全可用,否则,报错。应用程序对 1、2、或者 4 字节内存的访问要复杂一些,如果访问的内存块对应的
shadow 内存
的存储值如果不是负数,且不为0
,或者将要访问内存块超过了shadow 内存
表示的可用范围,意味着本次将访问到不可使用的内存:
ShadowAddr = (Addr >> 3) + Offset; k = *ShadowAddr; if (k != 0 && ((Addr & 7) + AccessSize > k)) ReportAndCrash(Addr);需要注意的是,
ASAN
对源代码的插桩时机是在LLVM
对代码编译优化之后,也就意味着ASAN
只能检测LLVM
优化后幸存下来的内存访问,例如:被LLVM
优化掉的对栈对象进行访问的代码将不会被ASAN
所识别。同时,
ASAN
也不会对LLVM
生成的内存访问代码进行插桩,例如:寄存器溢出检查等等。另外,即使错误报告代码
ReportAndCrash(Addr)
只会被调用一次,但由于会在代码中的许多位置进行插入,因此,错误报告代码也必须相当紧凑。目前
ASAN
使用了一个简单的函数调用来处理错误报告,当然还有另一个选择是插入一个硬件异常。运行时库
在应用程序启动时,将映射整个
shadow 内存
,因此程序的其他部分不能使用它。BAD 区域
也是受保护的,应用程序也不能访问。在 linux 操作系统中,
shadow 内存
区域不会被占用,因此,映射总是成功的。但在 MacOS 中可能需要禁用地址空间布局(ASLR)。另外,根据 GOOGLE 工程师介绍,
shadow 内存
区域的布局也适用于 windows 操作系统。启用
ASAN
时,源代码中的malloc
和free
函数将会被替换为运行时库中的malloc
和free
函数。
malloc
分配的内存区域被组织为为一个与对象大小相对应的空闲列表数组。当对应于所请求内存大小的空闲列表为空时,从操作系统(例如,使用mmap
)分配带有redzone
的内存区域。n
个内存块,将分配n+1
个redzone
:
| redzone-1 | memory-1 | redzone-2 | memory-2 | redzone-3 |
free
函数会将整个内存区域置成不可使用并将其放入隔离区,这样该区域就不会马上被malloc
分配给应用程序。目前,隔离区是使用一个 FIFO 队列实现的,它在任何时候都拥有一定数量的内存。
默认情况下,
malloc
和free
记录当前调用堆栈,以便提供更多信息的错误报告。malloc
调用堆栈存储在左侧redzone
中(redzone
越大,可以存储的帧数越多),而free
调用堆栈存储在内存区域本身的开头。到这里你应该已经明白了对于动态分配的内存,
ASAN
是怎么实现检测的,但你可能会产生疑惑:动态分配是通过malloc
函数分配redzone
来支持错误检测,那栈对象和全局对象这类没有malloc
分类内存的对象是怎么实现的呢?其实原理也很简单:
对于全局变量,
redzone
在编译时创建,redzone
的地址在应用程序启动时传递给运行时库。运行时库函数会将redzone
设置为不可使用并记录地址以供进一步错误报告。
对于栈对象,
redzone
是在运行时创建和置为不可使用。目前,使用32
字节的redzone
。例如以下代码片段:
void foo() { char a[10]; <function body> } 经ASAN
处理后的代码大致如下:
void foo() { char rz1[32] char arr[10]; char rz2[32-10+32]; unsigned * shadow = (unsigned*)(((long)rz1>>8)+Offset); // 将 redzone 设置为不可使用 shadow[0] = 0xffffffff; // rz1 shadow[1] = 0xffff0200; // arr and rz2 shadow[2] = 0xffffffff; // rz2 <function body> // 将所有内存设置成可以使用 shadow[0] = shadow[1] = shadow[2] = 0; }总结
ASAN 使用
shadow 内存
和redzone
来提供准确和即时的错误检测。传统观点认为,
shadow 内存
和redzone
要么通过多级映射方案产生高开销,要么占用大量的程序内存。但,ASAN
的使用的shadow映射
机制和shadow 状态
编码减少了对内存空间占用。最后,如果你觉得
ASAN
插桩代码和检测的对你某些的代码来说太慢了,那么可以使用编译器标志来禁用特定函数的,使ASAN
跳过对代码中某个函数的插桩和检测, 跳过分析函数的编译器指令是:__attribute__((no_sanitize_address))
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构