内存检查之Valgrind工具

内存检查之Valgrind工具

Author: Once Day Date: 2025年3月26日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦...

漫漫长路,有人对你微笑过嘛...

全系列文章请查看专栏: Linux实践记录_Once-Day的博客-CSDN博客

参考文章:


文章目录

  • 内存检查之Valgrind工具
        • [1. Valgrind介绍](#1. Valgrind介绍)
          • [1.1 概述](#1.1 概述)
          • [1.2 原理概述](#1.2 原理概述)
          • [1.3 Memcheck工具](#1.3 Memcheck工具)
          • [1.4 Callgrind工具](#1.4 Callgrind工具)
          • [1.5 Cachegrind工具](#1.5 Cachegrind工具)
          • [1.6 Helgrind工具](#1.6 Helgrind工具)
          • [1.7 Massif工具](#1.7 Massif工具)
        • [2. 内存检测实践](#2. 内存检测实践)
        • [3. Valgrind与Gitlab流水线集成](#3. Valgrind与Gitlab流水线集成)
1. Valgrind介绍
1.1 概述

Valgrind是一个功能强大的开源动态分析工具,主要用于检测和调试C/C++程序中的内存管理和线程同步问题。它最初由Julian Seward在2000年开发,旨在帮助程序员发现难以捕捉的bug,特别是那些与内存相关的错误,如内存泄漏、越界访问、使用未初始化的内存等。

经过多年的发展和完善,Valgrind已经成为了业界广泛使用的调试工具之一。它不仅支持Linux,还可以在macOS和Android等平台上运行。除了最初的内存检测功能,Valgrind还不断集成了其他功能模块,如性能分析、线程错误检测、缓存和分支预测分析等,使其成为一个多功能的分析工具套件。

Valgrind的工作原理是在程序运行时对其进行插桩和模拟,记录和分析程序的每一次内存操作和线程同步操作,从而发现潜在的错误。虽然这种方式会使程序的运行速度降低数十倍,但对于追踪难以复现的bug和提高程序运行时的鲁棒性来说,这种性能损失是值得的。

Valgrind的价值在于,它能帮助程序员及早发现和修复潜伏的内存和线程问题,减少因这些问题导致的程序崩溃、数据损坏、安全漏洞等风险。同时,通过Valgrind的性能分析功能,开发者还可以找出程序的性能瓶颈,进行针对性的优化。

Valgrind包含多个工具,如Memcheck,Cachegrind,Helgrind, Callgrind,Massif。

1.2 原理概述

Valgrind的核心原理是通过模拟一个虚拟的CPU环境,对程序的执行进行动态分析和检测。它利用两个关键的数据结构:Valid-Value表和Valid-Address表,来记录和跟踪内存的状态。

具体来说,对于进程地址空间中的每一个字节,Valgrind都为其分配8个bits在Valid-Value表中,用于记录该字节是否已经被初始化并具有有效的值。同时,对于CPU的每个寄存器,也有对应的bit向量来记录其值的有效性。

另一方面,Valid-Address表则为每个字节分配1个bit,用于标识该内存地址是否可以被合法地读写。

当程序执行时,Valgrind的虚拟CPU环境会拦截每一次内存访问和寄存器操作。在读写内存时,它首先检查Valid-Address表中对应的bit,如果该bit表明此地址是无效的,则报告读写错误。

而当内存中的数据被加载到真实CPU的寄存器中时,Valgrind会将对应的Valid-Value bits也加载到虚拟CPU环境中。一旦寄存器中的值被用于计算内存地址或影响程序输出,Valgrind就会检查相应的Valid-Value bits,如果发现使用了未初始化的值,就会报告错误。

由于Valgrind需要对每一个字节和寄存器都进行状态跟踪,并在虚拟CPU环境中模拟执行,因此它会显著增加内存消耗,并使得程序的运行速度比在实际CPU上慢20到30倍。

1.3 Memcheck工具

Memcheck是Valgrind工具套件中最常用和最强大的工具之一,主要用于检测C/C++程序中与内存相关的错误。它通过拦截程序中的内存管理函数调用,并对内存的分配、使用和释放进行跟踪和检查,从而发现各种内存问题。

Memcheck的主要功能包括:

  • 检测未释放的内存(Memory Leak):识别程序中那些已分配但没有正确释放的内存块,帮助避免内存泄漏导致的资源浪费和程序长时间运行后的崩溃。
  • 检测越界访问(Out-of-bounds Access):发现程序读写内存时越过了已分配的内存块边界,避免缓冲区溢出等问题。
  • 检测使用未初始化的内存(Use of Uninitialized Memory):找出程序中使用了未经初始化的变量或内存块的情况,这可能导致程序行为不确定或产生意外结果。
  • 检测非法释放(Invalid Free):识别那些对已经释放的内存块或不是由malloc等分配函数返回的内存指针进行释放的操作,避免双重释放或释放无效指针等错误。
  • 检测重叠内存操作(Overlapping Memory Operation):发现类似strcpy、memcpy等函数在执行时源内存块和目标内存块存在重叠的情况,这可能导致数据损坏。

Memcheck通过在程序运行时动态插入检测代码,对内存访问进行拦截和检查,一旦发现上述问题,就会生成详细的错误报告,包括错误类型、发生位置、涉及的内存地址和调用栈等信息,帮助程序员快速定位和修复内存相关的bug。

1.4 Callgrind工具

Callgrind是Valgrind工具套件中的一个性能分析工具,主要用于帮助开发者深入理解程序的运行行为和优化程序性能。它通过记录程序执行过程中的函数调用关系和指令执行情况,生成详细的性能分析报告,揭示程序的性能瓶颈和优化机会。

Callgrind的主要功能包括:

  • 函数调用关系分析:Callgrind会跟踪记录程序运行过程中的函数调用关系,生成函数调用图(Call Graph)。通过调用图,开发者可以清晰地了解程序的函数调用层次和调用频率,识别出调用关系复杂或者调用频繁的函数,从而进行针对性的优化。
  • 指令级别的性能分析:Callgrind不仅记录函数调用关系,还会统计每个函数中各个代码行或指令的执行次数和占用的时间。这种细粒度的性能数据可以帮助开发者发现函数内部的性能热点,如频繁执行的循环或者耗时的语句,从而进行代码级别的优化。
  • 支持可视化分析:Callgrind生成的性能分析数据可以通过专门的可视化工具(如KCachegrind)进行查看和分析。这些工具提供了直观的图形界面,允许开发者交互式地探索函数调用关系、查看代码的执行频率和时间占用等,使性能问题更容易被发现和理解。
  • 支持抽样分析:对于长时间运行的程序,Callgrind还提供了抽样分析的功能。即以一定的时间间隔对程序进行采样,记录采样点的函数调用和指令执行情况。这种方式可以在较低的性能开销下,获得程序整体执行情况的统计信息,帮助开发者识别性能瓶颈。

Callgrind通过详细记录程序的函数调用和指令执行情况,为性能调优提供了丰富的数据支持。它可以帮助开发者深入理解程序的运行行为,发现性能瓶颈,并指导代码优化的方向。

1.5 Cachegrind工具

Cachegrind是Valgrind工具套件中的另一个性能分析工具,主要用于帮助开发者优化程序的缓存性能。现代计算机系统中,CPU的运算速度远快于内存的访问速度,为了弥补这种差距,通常会在CPU和主存之间引入多级缓存(Cache)。程序的缓存利用率对整体性能有着关键的影响。Cachegrind通过模拟CPU的缓存层次结构,收集和分析程序的缓存使用情况,帮助开发者发现和优化程序中的缓存问题。

Cachegrind的主要功能包括:

  • 缓存命中和丢失统计:Cachegrind会精确地记录程序执行过程中的缓存命中(Hit)和缓存丢失(Miss)次数。它提供了对各级缓存(如L1、L2、L3等)的访问统计,以及命中率和丢失率等关键指标。通过分析这些数据,开发者可以发现程序中缓存利用率低下的代码区域,进而进行针对性的优化。
  • 代码行级别的缓存分析:除了整体的缓存统计数据,Cachegrind还能提供详细到代码行级别的缓存使用情况。它记录了每行代码的缓存命中和丢失次数,帮助开发者精确定位导致缓存问题的具体代码位置。这对于优化关键循环和热点函数的缓存表现尤为重要。
  • 支持多级缓存分析:Cachegrind可以模拟不同层次和大小的缓存结构,如L1数据缓存、L1指令缓存、L2缓存等。它能够分别统计各级缓存的访问情况,帮助开发者全面了解程序在不同缓存层次上的表现,进行有针对性的优化。
  • 与性能分析工具集成:Cachegrind生成的缓存分析数据可以与其他性能分析工具(如Callgrind)结合使用。这样可以将缓存性能数据与函数调用关系和指令执行情况关联起来,更全面地分析程序的性能特征,找出可优化的瓶颈。

对于需要高性能的程序,如科学计算、数据库等,Cachegrind的缓存分析能力可以帮助开发者显著提升程序的运行效率。

1.6 Helgrind工具

Helgrind是Valgrind工具套件中专门用于检测多线程程序中同步问题的工具。在现代并发编程中,线程同步和互斥操作是保证程序正确性和避免竞态条件的关键。然而,开发者在编写多线程程序时,经常会引入一些微妙而难以发现的同步错误,如死锁、数据竞争等。Helgrind通过动态分析程序的执行,检测和报告多线程程序中的同步问题,帮助开发者编写正确且高效的并发程序。

Helgrind的主要功能包括:

  • 检测数据竞争(Data Race):当多个线程并发访问共享数据,且至少有一个线程进行写操作时,如果没有适当的同步机制,就会导致数据竞争。Helgrind会跟踪线程对共享数据的访问,并检测潜在的数据竞争条件。它会报告发生竞争的代码位置和相关的线程信息,帮助开发者定位和修复问题。
  • 检测锁的使用错误:Helgrind会检查程序中锁的使用是否正确,如锁的初始化、销毁、加锁和解锁操作是否配对。它还会检测潜在的死锁情况,如多个线程以不同的顺序获取锁导致的循环等待。Helgrind的锁使用分析可以帮助开发者识别锁的使用错误,避免死锁和其他同步问题。
  • 检测条件变量的使用错误:条件变量是另一种常用的线程同步机制。Helgrind会检查程序中条件变量的使用是否正确,如等待和通知操作是否与适当的互斥锁配合使用。它还会检测潜在的信号丢失和虚假唤醒等问题,帮助开发者正确使用条件变量进行线程间通信。
  • 支持标注和抑制:Helgrind提供了一些特殊的函数标注,允许开发者在代码中显式标记某些同步操作或忽略某些警告。这对于处理一些特殊的同步模式或抑制误报非常有用。通过合理使用标注,开发者可以优化Helgrind的分析结果,减少误报并关注真正的问题。

对于开发大型并发程序、并行算法库或多线程服务器程序的开发者来说,Helgrind是一个非常有价值的工具。它可以在开发和测试阶段及早发现并发问题,避免这些问题在生产环境中导致难以诊断和修复的故障。

1.7 Massif工具

Massif是Valgrind工具套件中专门用于检测和分析程序堆内存使用情况的工具。在长时间运行或处理大量数据的程序中,内存的使用情况对性能和资源消耗有着重要影响。Massif通过跟踪和记录程序在堆上的内存分配和释放操作,生成详细的内存使用报告,帮助开发者分析内存使用模式,发现内存泄漏和优化内存管理。

Massif的主要功能包括:

  • 堆内存分配和使用跟踪:Massif会拦截程序中的内存分配和释放函数(如malloc、free等),记录每次操作的时间、大小和调用栈信息。通过对这些信息的分析,Massif可以生成程序在不同时间点的内存使用快照,展示内存使用量的变化趋势和分配模式。
  • 内存使用量的可视化分析:Massif生成的内存分析数据可以通过专门的可视化工具(如ms_print)进行查看和分析。这些工具提供了直观的图表和报告,展示了程序在不同时间点的内存使用量、内存分配和释放的时间线、以及各个函数或代码块的内存占用情况。开发者可以通过可视化分析,快速定位内存使用异常的位置和模式。
  • 检测内存泄漏:Massif可以帮助开发者发现程序中的内存泄漏问题。通过比较不同时间点的内存快照,Massif可以识别出那些被分配但从未释放的内存块。这些内存块可能是由于忘记释放或程序逻辑错误导致的内存泄漏。Massif的内存泄漏检测能力可以帮助开发者及时修复这些问题,避免长时间运行的程序出现内存溢出或性能下降。
  • 支持多种内存分配器:Massif支持分析使用不同内存分配器的程序,如系统默认的malloc/free、自定义的内存分配器等。这使得Massif可以适用于各种内存管理模型的程序,提供全面的内存使用分析。

对于需要长时间运行或处理大量数据的程序,如服务器程序、数据处理应用等,Massif可以帮助开发者优化内存使用,提高程序的性能和稳定性。

2. 内存检测实践

测试源码如下:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

void leak_memory()
{
    int *ptr = (int *)malloc(sizeof(int));
    *ptr     = 42;
    // 忘记释放ptr指向的内存
}


void invalid_access() {
    int* ptr = (int*)malloc(sizeof(int) * 5);
    ptr[5] = 42;  // 访问数组边界之外的元素
    free(ptr);
}

void invalid_free() {
    int* ptr = (int*)malloc(sizeof(int));
    free(ptr);
    free(ptr);  // 重复释放同一块内存
}

void use_uninitialized() {
    int* ptr = (int*)malloc(sizeof(int));
    printf("Uninitialized value: %d\n", *ptr);  // 读取未初始化的内存
    free(ptr);
}

int main()
{
    leak_memory();
    invalid_access();
    invalid_free();
    use_uninitialized();
    return 0;
}

编译之后使用Valgrind进行检测:

无效内存访问:

无效free操作:

无效初始值:

内存泄漏情况:

下面是对Valgrind Memcheck工具输出的关键字段的解释:

  • ==2475658== Memcheck, a memory error detector,这行表示使用的是Valgrind的Memcheck工具,它是一个内存错误检测器。
  • ==2475658== HEAP SUMMARY,这行标志着堆内存使用情况摘要部分的开始。
  • in use at exit: 4 bytes in 1 blocks,这行表示在程序退出时,仍有4字节内存在1个块中处于使用状态,没有被释放。
  • total heap usage: 1 allocs, 0 frees, 4 bytes allocated,这行给出了整个程序运行过程中堆内存的使用情况。总共分配了1次内存,共4字节,没有任何内存释放操作。
  • 4 bytes in 1 blocks are definitely lost in loss record 1 of 1,这行指出了确定的内存泄漏情况。有4字节内存在1个块中被确定为泄漏,这是本次检测发现的唯一一处内存泄漏。
  • at 0x484880F: malloc (vg_replace_malloc.c:446)...,这几行给出了内存泄漏发生的代码位置信息,包括分配内存的malloc调用、泄漏发生的函数leak_memory以及主函数main。行号信息精确指出了泄漏发生的代码行。
  • LEAK SUMMARY:,这行标志着内存泄漏情况摘要部分的开始。
  • definitely lost: 4 bytes in 1 blocks,指确定泄露的内存,强调了确定泄漏的内存大小和块数。
  • indirectly lost: 0 bytes in 0 blocks,指间接泄露的内存,其总是与 definitely lost 一起出现,只要修复 definitely lost 即可恢复。当使用了含有指针成员的类或结构时可能会报这个错误。
  • possibly lost: 0 bytes in 0 blocks,指可能泄露的内存,大多数情况下应视为与 definitely lost 一样需要尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存的起始地址,但可以访问其中的某一部分数据,则会报这个错误。
  • still reachable: 0 bytes in 0 blocks,如果程序是正常结束的,那么它可能不会造成程序崩溃,但长时间运行有可能耗尽系统资源,因此笔者建议修复它。如果程序是崩溃(如访问非法的地址而崩溃)而非正常结束的,则应当暂时忽略它,先修复导致程序崩溃的错误,然后重新检测。
  • suppressed: 0 bytes in 0 blocks,已被解决。出现了内存泄露但系统自动处理了,可以无视这类错误。
  • ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0),这行给出了错误摘要,表明本次运行总共发现了1个错误,来自1个上下文,没有任何错误被抑制。

通过仔细阅读Valgrind Memcheck的输出信息,可以确定内存泄漏发生的位置、泄漏的内存大小,以及整个程序的堆内存使用情况。

3. Valgrind与Gitlab流水线集成

在C/C++项目开发过程中,可以将单元测试放到Valgrind里面执行,从而检查程序是否存在内存问题,主要包括以下五类问题:

  • 内存泄漏(Memory Leaks),发现未释放的内存,防止程序长期运行时占用过多内存。
  • 无效的内存访问(Invalid Memory Access),访问未分配的内存(如越界访问数组),访问已释放的内存(使用了 free 之后的指针)。
  • 未初始化内存使用(Use of Uninitialized Memory),变量未初始化就被使用,可能导致程序行为不确定。
  • 重叠的 memcpy 或 memmove 操作,可能导致数据损坏。
  • 非法的 free 操作(Invalid Free),释放未分配的指针或重复释放同一块内存。

在ubuntu上安装valgrind非常方便:

bash 复制代码
sudo apt-get install valgrind

我们使用如下的脚本来辅助执行应用程序,里面指定需要抑制的内存错误(有一些测试套件或者用例自身的问题无需关注):

bash 复制代码
#!/bin/bash

# 开启调试
# set -x

# 当前目录路径
export SOURCE_DIR=${SOURCE_DIR:-$(pwd)}

# 导入基本shell工具函数
source $SOURCE_DIR/scripts/utils.sh

# 如果 GEN_SUPPRESSIONS=ON, 则生成suppressions文件
export GEN_SUPPRESSIONS=${GEN_SUPPRESSIONS:-OFF}

# 运行所有单元测试
./scripts/run_valgrind.sh test_program

run_valgrind.sh脚本的内容如下:

bash 复制代码
#!/bin/bash

# 开启shell调试
# set -x

# 当前目录路径, 默认是当前目录的父目录
export SOURCE_DIR=${SOURCE_DIR:-$(dirname $(pwd))}

# 导入基本ANMK shell工具函数
source $SOURCE_DIR/scripts/utils.sh
source $SOURCE_DIR/scripts/run_environment.sh

# 内存检测工具 Valgrind:
# --tool=memcheck: 使用valgrind的memcheck内存检测工具
# --leak-check=full: 进行完整的内存泄漏检测
# --show-leak-kinds=all: 显示所有类型的内存泄漏
# --track-origins=yes: 跟踪内存泄漏的来源,即泄漏发生的具体位置
# 所以总结起来,这个valgrind命令的作用是:
#   使用memcheck工具,进行完整和详细的内存泄漏检测,显示所有的内存泄漏信息,并跟踪泄漏发生的准确位置。
#   通过使用这些参数,可以非常方便和详细地调试程序中的内存泄漏问题。
#   memcheck工具会打印每个泄漏发生的堆栈信息,以及泄漏的大小等信息。
MEMCHECK="valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes -s"

# 添加额外规则, 追踪子进程, 必要时候生成抑制规则 GEN_SUPPRESSIONS定义为1
MEMCHECK="${MEMCHECK} --trace-children=yes --suppressions=$SOURCE_DIR/valgrind-ignore.txt"
if [ "$GEN_SUPPRESSIONS" = "ON" ]; then
    MEMCHECK="${MEMCHECK} --gen-suppressions=yes"
fi

# 发现错误时, exit code不为0
MEMCHECK="${MEMCHECK} --error-exitcode=1"

INFO "MEMCHECK: [$MEMCHECK]"

# 排除在外的单元测试用例, 主要是CheckDeathTest
EXECUTE_ARGS="--gtest_filter=-CheckDeathTest*"

# 运行所有内存测试
run_programs "$MEMCHECK" "$EXECUTE_ARGS" $@
return_code=$?

# 重置环境
environment_reset

# 返回值
if [ $return_code -ne 0 ]; then
    ERROR "Run memory test failed"
    exit 1
fi

INFO "Run memory test success"
exit 0

在这个脚本里,使用MEMCHECK指定Valgrind的运行参数,通过--error-exitcode=1让Valgrind检测到内存错误后返回1,从而传递错误情况到外部执行端。

Gitlab-ci.yaml流水线配置如下:

yaml 复制代码
# 本地开发版本 - 内存测试, 用于通用X86-64环境下测试
develop-memory:
  stage: local-tests
  tags:
    - anmk-build
  needs:
    - job: develop-build
      artifacts: true
    - job: develop-debug-unittest
      artifacts: false
  script:
    - echo "Test Project - Develop Version - Memory Test"
    - ./run_mem_test.sh
    - echo "Test finished"

Once Day

也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~

相关推荐
双叶8361 小时前
(C语言)学生信息表(学生管理系统)(基于通讯录改版)(正式版)(C语言项目)
c语言·开发语言·c++·算法·microsoft
小麦嵌入式2 小时前
Linux驱动开发实战(九):Linux内核pinctrl_map详解与优势分析
linux·c语言·汇编·驱动开发·stm32·嵌入式硬件·硬件工程
阿巴~阿巴~3 小时前
2023年3月全国计算机等级考试真题(二级C语言)
c语言
i love you china5 小时前
深入理解指针5
c语言
卷卷的小趴菜学编程6 小时前
算法篇-------------双指针法
c语言·开发语言·c++·vscode·算法·leetcode·双指针法
XWXnb66 小时前
C语言:多线程
c语言·开发语言
似水এ᭄往昔6 小时前
【C语言】文件操作(2)
c语言·开发语言
GalaxyPokemon7 小时前
C/C++ 基础 - 回调函数
c语言·开发语言·c++
annekqiu8 小时前
MPLAB X IDE 环境中配置字的注意点
c语言·单片机
·醉挽清风·9 小时前
学习笔记—数据结构—二叉树(链式)
c语言·数据结构·c++·笔记·学习·算法