Memory Detection

内存检测

内存问题是 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工具包

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, 时间戳

bpftrace

基于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); }'

libbpf

 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)

参考阅读

其他工具

工具用途
time -v系统资源监控
coverity静态分析
gpertools性能分析 + heap 检查
heaptrackKDE 内存分析器

手动内存检测

方法一:宏定义截获 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;
}

参考阅读

0%