内存检测
内存问题是 C/C++ 程序中最常见也最隐蔽的 bug 类型,包括:
内存泄漏(Memory Leak) 堆栈溢出(Buffer Overflow/Underflow) 释放后使用(Use-After-Free) 重复释放(Double-Free) 未初始化内存使用 线程竞争(Thread Sanitizer) 工具对比
工具 检测类型 性能开销 适用场景 ASAN 越界、UAF、泄漏 ~2x 开发阶段快速定位 TSAN 线程竞争、数据竞争 ~2-5x 多线程程序 MSAN 未初始化内存 ~2x 隐蔽 Bug 检测 LSAN 泄漏检测 低 轻量级泄漏检测 Valgrind 泄漏、错误 ~20-50x 全面检测、无源码
通常开发阶段使用 ASAN,CI 中使用 Valgrind,多线程程序配合 TSAN。
ASAN (AddressSanitizer)
AddressSanitizer 是 Google 开发的快速内存错误检测器。
检测范围
堆溢出和下溢 栈溢出 全局变量溢出 释放后使用 (Use-after-free) 重复释放 (Double-free) 内存泄漏 编译选项
1
2
3
4
5
# GCC/Clang 编译
gcc -fsanitize= address -fno-omit-frame-pointer -g -O1 demo.c -o demo
# CMake
cmake -DADDRESS_SANITIZER= TRUE ..
常用选项
1
2
3
4
5
6
7
8
# 通过环境变量配置
ASAN_OPTIONS = detect_leaks = 1:halt_on_error= 0:log_path= ./asan.log ./demo
# 常用选项
ASAN_OPTIONS = detect_leaks = 1 # 启用泄漏检测
ASAN_OPTIONS = halt_on_error = 0 # 错误后继续运行
ASAN_OPTIONS = symbolize = 1 # 启用符号解析
ASAN_OPTIONS = detect_stack_use_after_return = 1 # 检测栈 UAF
输出示例
1
2
3
4
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
WRITE of size 4 at 0x602000000000
#0 0x7ffff7a5c123 in main demo.c:10
#1 0x7ffff7a2d0b3 in __libc_start_main
Valgrind
Valgrind 是一套 Linux 程序调试和性能分析工具,Memcheck 工具可检测内存泄漏和内存错误。
基本使用
1
2
3
4
5
# 完整检测
valgrind --leak-check= full --show-leak-kinds= all --track-origins= yes --verbose ./demo
# 快速检测
valgrind --tool= memcheck ./demo
常用参数
参数 说明 --leak-check=full详细检查内存泄漏 --show-leak-kinds=all显示所有类型的泄漏 --track-origins=yes追踪未初始化值来源 --verbose显示详细信息
输出解读
1
2
3
4
5
6
7
8
==12345== HEAP SUMMARY:
==12345== in use at exit: 0 bytes in 0 blocks
==12345== total heap usage: 1,000 allocs, 1,000 frees, 50,000 bytes allocated
==12345== LEAK SUMMARY:
==12345== definitely lost: 0 bytes in 0 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 256 bytes in 1 blocks
BPF (eBPF)
eBPF(extended Berkeley Packet Filter)是一种内核追踪技术,可在不修改内核代码的情况下动态追踪和分析系统行为。
bpf support
events介绍: tracepoint :内核静态追踪, kprobes : 内核动态追踪, uprobes :用户级动态追踪, perf_events :定时采样和 PMC。
ebpf-tracing
BPF程序生成BPF字节码,把字节码注册进BPF内核虚拟机里,BPF程序进行事件配置,通过Perf Buffer从内核里把输出拿到用户空间进行显示出来。
BPF 程序有两种方式将测量数据反馈到用户空间:一种是按事件详细数据传递,另一种是通过 BPF 映射。BPF 映射可以实现数组、关联数组和直方图,并适合传递摘要统计数据。
BCC(BPF Compiler Collection)提供丰富的内存分析工具。
安装
1
2
3
4
5
6
7
# Ubuntu/Debian
sudo apt-get install bpfcc-tools
# 验证安装
dpkg -L bpfcc-tools | head -20
ls /usr/sbin/*-bpfcc
python3 -c "from bcc import BPF; print('BCC OK')" # 检查 BCC Python 模块
1
2
3
4
5
#!/usr/bin/python
from bcc import BPF
# This may not work for 4.17 on x64, you need replace kprobe__sys_clone with kprobe____x64_sys_clone
BPF ( text = 'int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World! \\ n"); return 0; }' ) . trace_print ()
1
2
3
sudo python3 hello_world.py
zsh-443524 [010] ...21 171874.434960: bpf_trace_printk: Hello, World!'
进程名字-id, cpu, 时间戳
基于bcc实现,安装 sudo apt-get install -y bpftrace,教程:One-Liner Tutorial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 追踪所有 malloc 调用,按进程统计
sudo bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc { @[comm] = count(); }'
# 列出所有探针
sudo bpftrace -l 'tracepoint:syscalls:sys_enter_*'
# 追踪 openat 系统调用并打印进程名和文件路径
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
# 按进程名统计所有系统调用次数
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# 为指定 PID 的 read 系统调用返回值生成延迟直方图,/.../:这是一个滤波器,该动作只有在过滤表达式为真时才执行
sudo bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 18644/ { @bytes = hist(args->ret); }'
# 用线性直方图统计 vfs_read 返回值(读取字节数)分布
sudo bpftrace -e 'kretprobe:vfs_read { @bytes = lhist(retval, 0, 2000, 200); }'
# 测量 vfs_read 函数执行延迟并按进程生成直方图
sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns[comm] = hist(nsecs - @start[tid]); delete(@start[tid]); }'
# 统计 5 秒内所有调度器相关 tracepoint 触发次数
sudo bpftrace -e 'tracepoint:sched:sched* { @[probe] = count(); } interval:s:5 { exit(); }'
# 99Hz 采样生成内核调用栈火焰图数据
sudo bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
# 捕获进程切换时的内核调用栈分布
sudo bpftrace -e 'tracepoint:sched:sched_switch { @[kstack] = count(); }'
# 统计块设备 I/O 请求大小分布直方图
sudo bpftrace -e 'tracepoint:block:block_rq_issue { @ = hist(args->bytes); }'
1
2
3
4
5
6
7
8
9
10
sudo apt install -y libelf-dev pkg-config
sudo apt install clang
git clone https://github.com/libbpf/libbpf.git
cd libbpf/src
NO_PKG_CONFIG = 1 make
mkdir build root
sudo BUILD_STATIC_ONLY = y PREFIX = /usr/local/bpf make install
ls /usr/lib/linux-tools/*/bpftool 2>/dev/null | head -1
sudo ln -sf $( ls /usr/lib/linux-tools/*/bpftool | head -1) /usr/local/bin/bpftool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
cmake_minimum_required ( VERSION 3.16 )
project ( ebpf_test C )
set ( CMAKE_C_STANDARD 11 )
set ( CMAKE_C_STANDARD_REQUIRED ON )
set ( CMAKE_EXPORT_COMPILE_COMMANDS ON )
set ( BPFTOOL_EXECUTABLE "/usr/local/bin/bpftool" )
find_path ( LIBBPF_INCLUDE_DIR
NAMES bpf/libbpf.h
PATHS /usr/local/bpf/include /usr/include
DOC "libbpf include directory"
)
find_library ( LIBBPF_STATIC_LIB
NAMES libbpf.a
PATHS /usr/local/bpf/lib64 /usr/lib /usr/lib64
DOC "libbpf static library"
)
include ( FindPackageHandleStandardArgs )
find_package_handle_standard_args ( Libbpf
REQUIRED_VARS LIBBPF_INCLUDE_DIR LIBBPF_STATIC_LIB BPFTOOL_EXECUTABLE
)
if ( Libbpf_FOUND )
message ( STATUS "Found libbpf: ${LIBBPF_STATIC_LIB}" )
message ( STATUS "Found bpftool: ${BPFTOOL_EXECUTABLE}" )
endif ()
find_library ( LIBELF elf REQUIRED )
find_library ( LIBZ z REQUIRED )
find_library ( LIBPTHREAD pthread REQUIRED )
find_library ( LIBM m REQUIRED )
# ========== 生成 vmlinux.h ==========
set ( VMLINUX_H ${ CMAKE_BINARY_DIR } /include/vmlinux.h )
add_custom_command (
OUTPUT ${ VMLINUX_H }
COMMAND /bin/bash -c "mkdir -p ${CMAKE_BINARY_DIR}/include && ${BPFTOOL_EXECUTABLE} btf dump file /sys/kernel/btf/vmlinux format c > ${VMLINUX_H}"
COMMENT "Generating vmlinux.h from BTF"
VERBATIM
)
# ========== 1. 编译 BPF 程序 ==========
set ( BPF_BYTECODE ${ CMAKE_BINARY_DIR } /hello.bpf.o )
set ( SKEL_HEADER ${ CMAKE_BINARY_DIR } /hello.skel.h )
add_custom_command (
OUTPUT ${ BPF_BYTECODE }
DEPENDS ${ CMAKE_SOURCE_DIR } /src/hello.bpf.c ${ VMLINUX_H }
COMMAND ${ CLANG_EXECUTABLE } -g -O2 -target bpf -D__TARGET_ARCH_x86_64
-I ${ CMAKE_BINARY_DIR } /include
-I ${ CMAKE_SOURCE_DIR } /inlcude
-I ${ LIBBPF_INCLUDE_DIR }
-c ${ CMAKE_SOURCE_DIR } /src/hello.bpf.c
-o ${ BPF_BYTECODE }
COMMENT "Compiling BPF bytecode: hello.bpf.c → hello.bpf.o"
VERBATIM
)
# ========== 2. 生成 skeleton 头文件 ==========
add_custom_command (
OUTPUT ${ SKEL_HEADER }
DEPENDS ${ BPF_BYTECODE }
COMMAND ${ BPFTOOL_EXECUTABLE } gen skeleton ${ BPF_BYTECODE } > ${ SKEL_HEADER }
COMMENT "Generating skeleton: hello.skel.h"
VERBATIM
)
add_custom_target ( generate_bpf ALL DEPENDS ${ SKEL_HEADER } )
# ========== 3. 编译用户态程序 ==========
add_executable ( hello ${ CMAKE_SOURCE_DIR } /src/hello.c )
target_include_directories ( hello PRIVATE
${ CMAKE_BINARY_DIR } /include
${ CMAKE_BINARY_DIR }
${ CMAKE_SOURCE_DIR } /inlcude
${ LIBBPF_INCLUDE_DIR }
)
target_link_libraries ( hello PRIVATE
${ LIBBPF_STATIC_LIB }
${ LIBELF }
${ LIBZ }
${ LIBPTHREAD }
${ LIBM }
)
target_compile_options ( hello PRIVATE -Wall -Wextra )
add_dependencies ( hello generate_bpf )
install ( TARGETS hello RUNTIME DESTINATION bin )
install ( FILES ${ BPF_BYTECODE } DESTINATION share/ebpf )
参考阅读
其他工具
手动内存检测
方法一:宏定义截获 malloc/free
适用于单文件简单场景:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
void * _malloc ( size_t size , const char * filename , int line ) {
void * p = malloc ( size );
char buff [ 128 ] = { 0 };
sprintf ( buff , "./%p.mem" , p );
FILE * fp = fopen ( buff , "w" );
fprintf ( fp , "[+] %s:%d addr:%p, size: %ld \n " , filename , line , p , size );
fclose ( fp );
return p ;
}
void _free ( void * ptr , const char * filename , int line ) {
if ( ! ptr ) return ;
char buff [ 128 ] = { 0 };
sprintf ( buff , "./%p.mem" , ptr );
if ( unlink ( buff ) < 0 ) {
printf ( "double free: %p at %s:%d \n " , ptr , filename , line );
return ;
}
free ( ptr );
printf ( "[-] free: %p at %s:%d \n " , ptr , filename , line );
}
#define malloc(size) _malloc(size, __FILE__, __LINE__)
#define free(ptr) _free(ptr, __FILE__, __LINE__)
方法二:使用 __libc_malloc 重载
直接重载 libc 库中的 malloc/free 实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// gcc -shared -fPIC -o methods_1.so methods_1.c
// LD_PRELOAD=./methods_1.so ./a.out
// func --> malloc() { __builtin_return_address(0)}
// callback --> func --> malloc() { __builtin_return_address(1)}
// main --> callback --> func --> malloc() { __builtin_return_address(2)}
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>
#include <string.h>
#include <sys/stat.h>
extern void * __libc_malloc ( size_t size );
extern void __libc_free ( void * p );
static int enable_malloc_hook = 1 ;
static int enable_free_hook = 1 ;
static void clean_mem_dir ( void ) {
DIR * dir = opendir ( "./mem" );
if ( ! dir ) return ;
struct dirent * entry ;
char path [ 256 ];
while (( entry = readdir ( dir )) != NULL ) {
if ( strcmp ( entry -> d_name , "." ) == 0 || strcmp ( entry -> d_name , ".." ) == 0 )
continue ;
if ( strstr ( entry -> d_name , ".mem" )) {
snprintf ( path , sizeof ( path ), "./mem/%s" , entry -> d_name );
unlink ( path );
}
}
closedir ( dir );
}
__attribute__ (( constructor ))
static void init_leak_detector ( void ) {
mkdir ( "./mem" , 0755 );
clean_mem_dir ();
}
void * malloc ( size_t size ) {
if ( enable_malloc_hook ) {
enable_malloc_hook = 0 ;
void * p = __libc_malloc ( size );
if ( ! p ) {
enable_malloc_hook = 1 ;
return p ;
}
void * caller = __builtin_return_address ( 0 );
char buff [ 128 ];
snprintf ( buff , sizeof ( buff ), "./mem/%p.mem" , p );
FILE * fp = fopen ( buff , "w" );
if ( fp ) {
fprintf ( fp , "[+%p] --> addr:%p, size:%zu \n " , caller , p , size );
fclose ( fp );
}
enable_malloc_hook = 1 ;
return p ;
}
return __libc_malloc ( size );
}
void free ( void * p ) {
if ( ! p ) return ;
if ( enable_free_hook ) {
enable_free_hook = 0 ;
char buff [ 128 ];
snprintf ( buff , sizeof ( buff ), "./mem/%p.mem" , p );
if ( unlink ( buff ) < 0 ) {
printf ( "double free: %p \n " , p );
}
__libc_free ( p );
enable_free_hook = 1 ;
} else {
__libc_free ( p );
}
}
方法三:使用 dlsym hook
使用 RTLD_NEXT 获取真实函数地址(需链接 -ldl):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// g++ -shared -fPIC -o mem_hook.so mem_hook.cpp -ldl
// LD_PRELOAD=./mem_hook.so ./demo
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
typedef void * ( * malloc_t )( size_t size );
typedef void ( * free_t )( void * ptr );
static malloc_t malloc_f = NULL ;
static free_t free_f = NULL ;
static int enable_malloc_hook = 1 ;
static int enable_free_hook = 1 ;
void * malloc ( size_t size ) {
void * p = NULL ;
if ( enable_malloc_hook ) {
enable_malloc_hook = 0 ;
p = malloc_f ( size );
void * caller = __builtin_return_address ( 0 );
char buff [ 128 ] = { 0 };
sprintf ( buff , "./%p.mem" , p );
FILE * fp = fopen ( buff , "w" );
fprintf ( fp , "[+]%p --> addr:%p, size:%zu \n " , caller , p , size );
fclose ( fp );
enable_malloc_hook = 1 ;
} else {
p = malloc_f ( size );
}
return p ;
}
void free ( void * ptr ) {
if ( ! ptr ) return ;
if ( enable_free_hook ) {
enable_free_hook = 0 ;
char buff [ 128 ] = { 0 };
sprintf ( buff , "./%p.mem" , ptr );
if ( unlink ( buff ) < 0 ) {
printf ( "double free: %p \n " , ptr );
}
free_f ( ptr );
enable_free_hook = 1 ;
} else {
free_f ( ptr );
}
}
__attribute__ (( constructor ))
static void init_hook ( void ) {
if ( malloc_f == NULL ) {
malloc_f = ( malloc_t ) dlsym ( RTLD_NEXT , "malloc" );
}
if ( free_f == NULL ) {
free_f = ( free_t ) dlsym ( RTLD_NEXT , "free" );
}
}
调试技巧:addr2line -f -e demo -a <指针地址>
方法四:使用 malloc hook(已弃用)
glibc 2.24+ 已弃用 __malloc_hook,仅作学习参考:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// g++ ./methods_3.cpp -o methods_3
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
/*save old hook variable*/
static void * ( * old_malloc_hook )( size_t , const void * );
static void ( * old_free_hook )( void * , const void * );
/*prototype define for ous*/
static void * my_malloc_hook ( size_t size , const void * caller );
static void my_free_hook ( void * ptr , const void * caller );
static void save_orighook_to_old ( void ) {
old_malloc_hook = __malloc_hook ;
old_free_hook = __free_hook ;
}
static void save_myaddr_to_hook ( void ) {
__malloc_hook = my_malloc_hook ;
__free_hook = my_free_hook ;
}
static void restore_oldhook_to_hook ( void ) {
__malloc_hook = old_malloc_hook ;
__free_hook = old_free_hook ;
}
/* my malloc hook */
static void * my_malloc_hook ( size_t size , const void * caller ) {
void * result ;
restore_oldhook_to_hook ();
result = malloc ( size );
printf ( "malloc(%u) | call from %p, return %p \n " ,
( unsigned int ) size , caller , result );
save_orighook_to_old ();
save_myaddr_to_hook ();
return result ;
}
/* my free hook */
static void my_free_hook ( void * ptr , const void * caller ) {
restore_oldhook_to_hook ();
free ( ptr );
printf ( "free(%p) | called from %p \n " , ptr , caller );
}
__attribute__ (( constructor ))
static void my_init_hook ( void ) {
save_orighook_to_old ();
save_myaddr_to_hook ();
}
int main ( void ) {
char * p = ( char * ) malloc ( 10 );
free ( p );
return 0 ;
}
参考阅读
栈回溯 (Backtrace)
获取栈回溯的方法
方法 说明 限制 backtrace()libc 函数 通用 __builtin_return_addressGCC 内置 需 -O0 unw_backtrace()libunwind.so 接口通用 std::stacktrace_entryC++23 标准库 C++23+
测试
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
static void print_backtrace ( const char * context ) {
const int max_frames = 64 ;
void * buffer [ max_frames ];
int nptrs = backtrace ( buffer , max_frames );
char ** strings = backtrace_symbols ( buffer , nptrs );
if ( strings == NULL ) {
fprintf ( stderr , "[BT FAILED] %s \n " , context );
return ;
}
fprintf ( stderr , "--- Backtrace for '%s' --- \n " , context );
for ( int i = 0 ; i < nptrs ; i ++ ) {
fprintf ( stderr , "%s \n " , strings [ i ]);
}
fprintf ( stderr , "--- End --- \n " );
free ( strings );
}
void foo ( void ) {
print_backtrace ( "foo" );
}
void bar ( void ) {
foo ();
}
int main ( void ) {
bar ();
return 0 ;
}
参考阅读