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_annotate
或kcachegrind
可视化结果:
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 倍
限制:
- 不能检测所有类型的内存错误
- 可能产生误报或漏报
- 对于大型程序,可能需要大量内存
- 不适合实时系统或性能敏感的应用程序
- 某些特定的硬件指令或系统调用可能不被支持
与其他工具的比较 #
工具 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
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