valgrind命令

valgrind 命令 #

valgrind是一个用于内存调试、内存泄漏检测和性能分析的工具套件。它能够检测许多与内存相关的错误,这些错误通常很难被发现,但可能导致程序崩溃或不可预测的行为。

语法 #

valgrind [valgrind选项] [valgrind工具] [工具选项] 程序 [程序选项]

常用选项 #

通用选项 #

选项 描述
-h, --help 显示帮助信息
--version 显示版本信息
-v, --verbose 显示详细信息
-q, --quiet 安静模式,只显示错误信息
`–trace-children=yes no` 是否跟踪子进程
`–track-fds=yes no` 跟踪打开的文件描述符
`–time-stamp=yes no` 为消息添加时间戳
--log-file=文件名 将输出写入指定文件
--log-socket=ip:端口 将输出发送到网络套接字
`–xml=yes no` 以 XML 格式输出
--xml-file=文件名 将 XML 输出写入指定文件
--num-callers=数字 显示的调用栈深度(默认 12)
--tool=工具名 指定要使用的 Valgrind 工具

Memcheck 选项(默认工具) #

选项 描述
`–leak-check=no summary full` 内存泄漏检测级别
--show-leak-kinds=kind1,kind2,... 显示哪些类型的泄漏(definite,indirect,possible,reachable,all)
`–track-origins=yes no` 跟踪未初始化值的来源
--errors-for-leak-kinds=kind1,kind2,... 将哪些类型的泄漏视为错误
`–show-reachable=yes no` 显示仍可访问但可能泄漏的内存块
`–undef-value-errors=yes no` 检测未初始化内存的使用
--ignore-ranges=0xstart1-0xend1,0xstart2-0xend2,... 忽略指定地址范围的访问
--malloc-fill=值 用指定字节填充新分配的内存
--free-fill=值 用指定字节填充已释放的内存
`–partial-loads-ok=yes no` 允许部分有效地址的加载

Cachegrind 选项 #

选项 描述
`–cache-sim=yes no` 启用或禁用缓存模拟
`–branch-sim=yes no` 启用或禁用分支预测模拟
--cachegrind-out-file=文件名 指定输出文件名
--I1=大小,关联性,行大小 设置 L1 指令缓存参数
--D1=大小,关联性,行大小 设置 L1 数据缓存参数
--LL=大小,关联性,行大小 设置最后一级缓存参数

Callgrind 选项 #

选项 描述
--callgrind-out-file=文件名 指定输出文件名
`–dump-instr=yes no` 收集指令级别的信息
`–collect-jumps=yes no` 收集跳转信息
`–collect-systime=yes no` 收集系统调用时间
`–separate-threads=yes no` 分别收集每个线程的信息
--toggle-collect=函数名 在指定函数处切换数据收集

Helgrind 选项 #

选项 描述
`–check-stack-refs=yes no` 检查栈上的引用
`–history-level=none approx full` 历史记录级别,用于数据竞争检测
--conflict-cache-size=数字 冲突缓存大小,单位为百万字节
`–ignore-thread-creation=yes no` 忽略线程创建时的竞争

DRD 选项 #

选项 描述
`–check-stack-var=yes no` 检查栈变量的数据竞争
`–segment-merging=yes no` 启用或禁用段合并
`–ignore-thread-creation=yes no` 忽略线程创建时的竞争
--trace-addr=地址 跟踪对指定地址的访问
`–trace-alloc=yes no` 跟踪内存分配和释放
`–free-is-write=yes no` 将内存释放视为写操作

Massif 选项 #

选项 描述
--massif-out-file=文件名 指定输出文件名
`–stacks=yes no` 是否收集栈上的内存使用情况
--depth=数字 调用树的最大深度
--alloc-fn=函数名 将指定函数视为分配器
--threshold=百分比 阈值,低于此值的分配将被忽略
--peak-inaccuracy=百分比 峰值测量的不准确度
`–time-unit=i ms B` 时间单位(指令、毫秒或字节)
--detailed-freq=数字 详细快照的频率

Valgrind 工具 #

Valgrind 包含多个工具,每个工具用于不同类型的分析:

1. Memcheck(默认工具) #

检测内存错误,如:

  • 使用未初始化的内存
  • 读/写已释放的内存
  • 读/写越界内存
  • 内存泄漏
  • 不匹配的内存分配/释放函数(如 malloc/free 与 new/delete)
  • 重复释放内存

2. Cachegrind #

缓存和分支预测分析器,可以帮助识别:

  • 缓存未命中
  • 分支预测失败
  • 代码中的性能瓶颈

3. Callgrind #

扩展自 Cachegrind,提供更详细的调用图信息:

  • 函数调用关系
  • 每个函数的执行成本
  • 调用次数和指令计数

4. Helgrind #

线程错误检测器,用于发现:

  • 数据竞争
  • 锁定顺序问题
  • 死锁条件
  • POSIX pthread API 错误

5. DRD #

另一个线程错误检测器,与 Helgrind 类似但使用不同的分析技术:

  • 检测数据竞争
  • 检测锁争用
  • 检测死锁
  • 检测不正确的线程 API 使用

6. Massif #

堆分析器,用于:

  • 测量程序使用的堆内存
  • 识别大量内存分配的位置
  • 减少程序的内存使用

7. DHAT (Dynamic Heap Analysis Tool) #

动态堆分析工具,用于:

  • 分析堆块的生命周期
  • 识别热点分配
  • 分析内存访问模式

8. BBV (Basic Block Vector) #

基本块向量生成器,用于:

  • 生成程序执行的基本块向量
  • 用于 SimPoint 等工具的输入

基本用法 #

1. 使用 Memcheck 检测内存错误 #

valgrind --leak-check=full ./myprogram arg1 arg2

2. 使用 Cachegrind 分析缓存性能 #

valgrind --tool=cachegrind ./myprogram arg1 arg2

3. 使用 Callgrind 分析函数调用 #

valgrind --tool=callgrind ./myprogram arg1 arg2

4. 使用 Helgrind 检测线程错误 #

valgrind --tool=helgrind ./myprogram arg1 arg2

5. 使用 Massif 分析堆内存使用 #

valgrind --tool=massif ./myprogram arg1 arg2

6. 将输出保存到文件 #

valgrind --log-file=valgrind.log ./myprogram arg1 arg2

7. 跟踪子进程 #

valgrind --trace-children=yes ./myprogram arg1 arg2

8. 显示未初始化值的来源 #

valgrind --track-origins=yes ./myprogram arg1 arg2

高级用法 #

1. 自定义内存泄漏检测 #

valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./myprogram

2. 忽略特定库中的错误 #

valgrind --suppressions=suppress.txt ./myprogram

suppress.txt文件示例:

{
   Ignore_libX11_errors
   Memcheck:Leak
   ...
   obj:*/libX11.so*
}

3. 动态控制 Valgrind #

在程序中包含valgrind/valgrind.h头文件,使用特殊宏:

#include <valgrind/valgrind.h>

// 告诉Valgrind忽略接下来的内存泄漏
VALGRIND_DISABLE_ERROR_REPORTING;
// 执行一些可能会产生误报的代码
VALGRIND_ENABLE_ERROR_REPORTING;

// 手动检查内存泄漏
VALGRIND_DO_LEAK_CHECK;

// 添加自定义错误
VALGRIND_ERROR_PRINTF("Custom error message");

4. 使用 GDB 调试 Valgrind 发现的问题 #

valgrind --vgdb=yes --vgdb-error=0 ./myprogram

然后在另一个终端中:

gdb ./myprogram
(gdb) target remote | /usr/lib/valgrind/../../bin/vgdb

5. 分析特定时间段的性能 #

valgrind --tool=callgrind --instr-atstart=no ./myprogram

在程序运行时,使用callgrind_control工具:

callgrind_control -i on   # 开始收集数据
# 执行感兴趣的操作
callgrind_control -i off  # 停止收集数据

6. 自定义缓存参数 #

valgrind --tool=cachegrind --I1=32768,8,64 --D1=32768,8,64 --LL=8388608,16,64 ./myprogram

7. 分析多线程程序的锁争用 #

valgrind --tool=drd --check-stack-var=yes --exclusive-threshold=100 ./myprogram

8. 生成调用图 #

valgrind --tool=callgrind --separate-threads=yes --dump-instr=yes --collect-jumps=yes ./myprogram

然后使用callgrind_annotatekcachegrind可视化结果:

callgrind_annotate callgrind.out.12345

实用示例 #

1. 检测基本内存错误 #

// memcheck_example.c
#include <stdlib.h>

int main() {
    int *x = malloc(10 * sizeof(int));
    x[10] = 0;  // 越界写入

    int y;
    if(y == 0)  // 使用未初始化的值
        return 1;

    return 0;
    // 忘记释放x,导致内存泄漏
}
gcc -g -O0 memcheck_example.c -o memcheck_example
valgrind --leak-check=full ./memcheck_example

输出将显示:

  • 越界写入错误
  • 使用未初始化值的错误
  • 内存泄漏警告

2. 检测多线程程序中的数据竞争 #

// helgrind_example.c
#include <pthread.h>
#include <stdio.h>

int shared_var = 0;

void* thread_func(void* arg) {
    shared_var++;  // 没有锁保护的共享变量访问
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_func, NULL);
    pthread_create(&t2, NULL, thread_func, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    printf("shared_var = %d\n", shared_var);
    return 0;
}
gcc -g -O0 -pthread helgrind_example.c -o helgrind_example
valgrind --tool=helgrind ./helgrind_example

输出将显示线程之间的数据竞争。

3. 分析程序的缓存性能 #

// cachegrind_example.c
#include <stdlib.h>

#define SIZE 10000

void matrix_multiply(int **a, int **b, int **c, int size) {
    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {
            c[i][j] = 0;
            for (int k = 0; k < size; k++) {
                c[i][j] += a[i][k] * b[k][j];
            }
        }
    }
}

int main() {
    int size = 1000;
    int **a = malloc(size * sizeof(int*));
    int **b = malloc(size * sizeof(int*));
    int **c = malloc(size * sizeof(int*));

    for (int i = 0; i < size; i++) {
        a[i] = malloc(size * sizeof(int));
        b[i] = malloc(size * sizeof(int));
        c[i] = malloc(size * sizeof(int));
        for (int j = 0; j < size; j++) {
            a[i][j] = i + j;
            b[i][j] = i - j;
        }
    }

    matrix_multiply(a, b, c, size);

    // 释放内存
    for (int i = 0; i < size; i++) {
        free(a[i]);
        free(b[i]);
        free(c[i]);
    }
    free(a);
    free(b);
    free(c);

    return 0;
}
gcc -g -O0 cachegrind_example.c -o cachegrind_example
valgrind --tool=cachegrind ./cachegrind_example

输出将显示缓存未命中的统计信息。

4. 分析堆内存使用 #

// massif_example.c
#include <stdlib.h>
#include <string.h>

void allocate_memory() {
    void *p = malloc(1000000);  // 分配1MB
    memset(p, 0, 1000000);
    // 不释放p,导致内存泄漏
}

int main() {
    for (int i = 0; i < 10; i++) {
        allocate_memory();
    }
    return 0;
}
gcc -g -O0 massif_example.c -o massif_example
valgrind --tool=massif ./massif_example
ms_print massif.out.12345

输出将显示堆内存使用随时间的变化。

5. 检测死锁 #

// drd_example.c
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void* thread1_func(void* arg) {
    pthread_mutex_lock(&mutex1);
    sleep(1);  // 增加死锁的可能性
    pthread_mutex_lock(&mutex2);

    // 临界区

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);
    return NULL;
}

void* thread2_func(void* arg) {
    pthread_mutex_lock(&mutex2);
    sleep(1);  // 增加死锁的可能性
    pthread_mutex_lock(&mutex1);

    // 临界区

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread1_func, NULL);
    pthread_create(&t2, NULL, thread2_func, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}
gcc -g -O0 -pthread drd_example.c -o drd_example
valgrind --tool=drd ./drd_example

输出将显示潜在的死锁条件。

常见错误类型及解释 #

Memcheck 错误 #

1. 非法读/写 #

Invalid read of size 4
Invalid write of size 8

这表示程序正在访问无效的内存地址,可能是:

  • 访问已释放的内存
  • 访问数组越界
  • 使用未初始化的指针

2. 使用未初始化的值 #

Conditional jump or move depends on uninitialised value(s)

这表示程序在条件语句中使用了未初始化的变量。

3. 非法释放 #

Invalid free()

这表示程序尝试:

  • 释放已经释放的内存
  • 释放未通过 malloc/new 分配的内存
  • 释放堆块的中间位置

4. 内存泄漏 #

LEAK SUMMARY:
   definitely lost: 1,024 bytes in 1 blocks
   indirectly lost: 0 bytes in 0 blocks
   possibly lost: 2,048 bytes in 2 blocks
   still reachable: 4,096 bytes in 4 blocks
  • definitely lost:确定泄漏,没有指针指向这块内存
  • indirectly lost:间接泄漏,通过泄漏的块可以访问到
  • possibly lost:可能泄漏,有指针指向块的内部而不是开始处
  • still reachable:仍可访问,程序结束时未释放但仍有指针指向

5. 重叠源和目标 #

Source and destination overlap in memcpy(...)

这表示在内存复制函数中,源和目标区域重叠。

Helgrind/DRD 错误 #

1. 数据竞争 #

Possible data race during read of size 4 at 0x....

这表示多个线程同时访问同一内存位置,且至少有一个是写操作,没有适当的同步。

2. 锁顺序违规 #

Lock order violated

这表示程序中存在潜在的死锁条件,因为在不同的线程中以不同的顺序获取锁。

3. 未初始化的锁 #

pthread_mutex_lock: mutex is not initialised

这表示程序尝试锁定未初始化的互斥锁。

性能影响和限制 #

使用 Valgrind 会显著减慢程序的执行速度:

  • Memcheck:程序运行速度降低 10-50 倍
  • Cachegrind:降低 20-100 倍
  • Callgrind:降低 20-100 倍
  • Helgrind/DRD:降低 20-100 倍
  • Massif:降低 5-20 倍

限制:

  1. 不能检测所有类型的内存错误
  2. 可能产生误报或漏报
  3. 对于大型程序,可能需要大量内存
  4. 不适合实时系统或性能敏感的应用程序
  5. 某些特定的硬件指令或系统调用可能不被支持

与其他工具的比较 #

工具 优点 缺点 适用场景
Valgrind 全面的内存和线程错误检测,无需重新编译 显著降低执行速度 开发和测试阶段的深入调试
AddressSanitizer 更快的执行速度,良好的内存错误检测 需要重新编译,功能不如 Valgrind 全面 需要较高性能的调试场景
ThreadSanitizer 专注于线程问题,较快的执行速度 需要重新编译,仅检测线程问题 多线程程序的调试
Electric Fence 简单易用,专注于缓冲区溢出 功能有限,性能影响大 简单程序的缓冲区溢出检测
Dr. Memory 支持 Windows 和 Linux,类似 Memcheck 不如 Valgrind 成熟 Windows 平台的内存调试

提示和技巧 #

1. 编译优化 #

为了获得最佳的调试体验,使用以下编译选项:

gcc -g -O0 -Wall program.c -o program

-g添加调试信息,-O0禁用优化。

2. 创建抑制文件 #

如果有已知的但不关心的错误(如第三方库中的问题),可以创建抑制文件:

valgrind --gen-suppressions=all --log-file=suppression.log ./program

然后编辑suppression.log,提取抑制规则,并在后续运行中使用:

valgrind --suppressions=my_suppressions.supp ./program

3. 使用宏进行条件编译 #

#include <stdio.h>

#ifdef VALGRIND_DEBUG
#include <valgrind/memcheck.h>
#define VALGRIND_MALLOC_FREELIKE_BLOCK(addr, size) VALGRIND_MALLOCLIKE_BLOCK(addr, size, 0, 0)
#define VALGRIND_FREE_FREELIKE_BLOCK(addr) VALGRIND_FREELIKE_BLOCK(addr, 0)
#else
#define VALGRIND_MALLOC_FREELIKE_BLOCK(addr, size)
#define VALGRIND_FREE_FREELIKE_BLOCK(addr)
#endif

void* my_custom_allocator(size_t size) {
    void* ptr = /* custom allocation */;
    VALGRIND_MALLOC_FREELIKE_BLOCK(ptr, size);
    return ptr;
}

void my_custom_deallocator(void* ptr) {
    VALGRIND_FREE_FREELIKE_BLOCK(ptr);
    /* custom deallocation */
}

4. 使用环境变量 #

Valgrind 支持多种环境变量来控制其行为:

VALGRIND_OPTS="--leak-check=full --track-origins=yes" ./run_my_program.sh

5. 分析特定函数 #

valgrind --tool=callgrind --toggle-collect=main ./program

这将只在main函数执行期间收集数据。

6. 使用客户端请求 #

在代码中插入特殊的宏来与 Valgrind 通信:

#include <valgrind/valgrind.h>

// 手动检查内存泄漏
VALGRIND_DO_LEAK_CHECK;

// 标记内存为已初始化
VALGRIND_MAKE_MEM_DEFINED(ptr, size);

// 标记内存为未初始化
VALGRIND_MAKE_MEM_UNDEFINED(ptr, size);

// 添加自定义错误消息
VALGRIND_PRINTF("Custom message: %d\n", value);

7. 使用 callgrind_annotate 分析结果 #

valgrind --tool=callgrind ./program
callgrind_annotate callgrind.out.12345 source_file.c

这将显示source_file.c中每行代码的执行成本。

8. 使用 KCachegrind 可视化结果 #

KCachegrind 是一个图形化工具,可以可视化 Callgrind 和 Cachegrind 的输出:

kcachegrind callgrind.out.12345

9. 使用 Massif-visualizer 可视化内存使用 #

massif-visualizer massif.out.12345

10. 结合 GDB 和 Valgrind #

valgrind --vgdb=yes --vgdb-error=0 ./program

在另一个终端:

gdb ./program
(gdb) target remote | /usr/lib/valgrind/../../bin/vgdb

扩展阅读 #