Valgrind的介绍

介绍

Valgrind是一套Linux下,开放源代码(GPL V2)的仿真调试工具的集合。Valgrind由内核(core)以及基于内核的其他调试工具组成。内核类似于一个框架(framework),它模拟了一个CPU环境,并提供服务给其他工具;而其他工具则类似于插件 (plug-in),利用内核提供的服务完成各种特定的内存调试任务。Valgrind的体系结构如下图所示:

image-20240514111938295

通俗的说:

Valgrind是一个由多个组件构成的内存调试工具套件。这些组件可以分为两类:内核和基于内核的其他工具。

  1. 内核:内核在这里可以被理解为Valgrind的核心部分,它提供了一个虚拟的CPU环境,并且提供一些基本的服务,比如内存管理、错误检测等。你可以把它想象成一个工厂的骨架或者框架,提供了基础设施和一些常用的功能。
  2. 其他工具:这些工具则类似于插件,它们利用内核提供的服务,完成各种特定的内存调试任务。比如Memcheck就是其中一个工具,它可以检测内存泄漏、数组越界等问题。你可以把这些工具想象成工厂的流水线,每个流水线都负责完成特定的任务。

Valgrind检测一个可执行程序是否存在内存泄漏的工作原理

它的工作原理主要是通过插桩技术,也就是在程序运行时动态地插入一些检查代码来监视内存的使用情况。

当我们使用Valgrind运行一个程序时,Valgrind会首先加载自己的内核,然后把我们的程序加载到自己的虚拟内存空间中。在这个虚拟内存空间中,Valgrind可以完全控制程序的执行流程,并且可以检测到所有的内存访问。

Valgrind使用一种称为影子内存的技术来跟踪每个字节的内存状态,包括该字节是否已被分配、是否已被释放、是否已被初始化等。每当程序进行一次内存操作(如malloc、free、read、write等)时,Valgrind都会更新影子内存的状态,并检查这次操作是否合法。

如果Valgrind检测到一次非法的内存操作,比如访问了未被初始化的内存,或者访问了已经被释放的内存,它就会立即报告一个错误。当程序退出时,Valgrind会检查是否所有的内存都已经被正确释放,如果有任何内存泄漏,它也会报告一个错误。

valgrind工具

(1)Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。

(2)Callgrind。它主要用来检查程序中函数调用过程中出现的问题。

(3)Cachegrind。它主要用来检查程序中缓存使用出现的问题。

(4)Helgrind。它主要用来检查多线程程序中出现的竞争问题。

(5)Massif。它主要用来检查程序中堆栈使用中出现的问题。

(6)Extension。可以利用core提供的功能,自己编写特定的内存调试工具。

Memcheck检查的原理

Memcheck检测内存问题的原理如下图所示:

image-20240514112408569

Memcheck是Valgrind的一个组件,它用于检测C和C++程序中的内存管理问题。它通过在程序运行期间拦截所有内存访问和管理请求,来检测内存泄漏、使用未初始化的内存、访问已经释放的内存等问题。

Memcheck的工作原理主要依赖于两个全局表:

  1. A-bit(Addressability bit)表:A-bit表用于标记哪些内存是可访问的。对于每个字节的内存,A-bit表都有一个对应的位。如果该位被设置,那么对应的内存字节就是可访问的。否则,该字节是不可访问的。当程序试图读写一个不可访问的内存字节时,Memcheck会报告一个错误。
  2. V-bit(Validity bit)表:V-bit表用于标记哪些内存是已初始化的。对于每个字节的内存,V-bit表都有一个对应的位。如果该位被设置,那么对应的内存字节就是已经被初始化的。否则,该字节是未初始化的。当程序试图读取一个未初始化的内存字节时,Memcheck会报告一个错误。

内存泄漏示例

Valgrind 可以用来检测程序是否有非法使用内存的问题,例如访问未初始化的内存、访问数组时越界、忘记释放动态内存等问题。在 Linux 可以使用下面的命令安装 Valgrind:

1
(base) sv@sv-NF5280M5:/home/sv/桌面$ sudo apt-get install valgrind
1
2
3
4
5
6
$ wget ftp://sourceware.org/pub/valgrind/valgrind-3.13.0.tar.bz2
$ bzip2 -d valgrind-3.13.0.tar.bz2
$ tar -xf valgrind-3.13.0.tar
$ cd valgrind-3.13.0
$ ./configure && make
$ sudo make install

这里主要演示memcheck的示例。其他更多工具的示例请看:valgrind基本功能介绍、基础使用方法说明

未即时释放

Valgrind 可以用来检测程序在哪个位置发生内存泄漏,例如下面的程序:

1
2
3
4
5
6
#include <stdlib.h>
int main()
{
int *array = malloc(sizeof(int));
return 0;
}

编译程序时,需要加上-g选项:

1
$ g++ -g -o main_c main.cpp

使用 Valgrind 检测内存使用情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ valgrind --tool=memcheck --leak-check=full  ./main_c
==31416== Memcheck, a memory error detector
==31416== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31416== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31416== Command: ./main_c
==31416==
==31416==
==31416== HEAP SUMMARY:
==31416== in use at exit: 4 bytes in 1 blocks
==31416== total heap usage: 1 allocs, 0 frees, 4 bytes allocated
==31416==
==31416== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==31416== at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31416== by 0x400537: main (main.c:5)
==31416==
==31416== LEAK SUMMARY:
==31416== definitely lost: 4 bytes in 1 blocks
==31416== indirectly lost: 0 bytes in 0 blocks
==31416== possibly lost: 0 bytes in 0 blocks
==31416== still reachable: 0 bytes in 0 blocks
==31416== suppressed: 0 bytes in 0 blocks
==31416==
==31416== For counts of detected and suppressed errors, rerun with: -v
==31416== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

看看输出信息中的HEAP SUMMARY,它表示程序在堆上分配内存的情况,其中的1 allocs表示程序分配了 1 次内存,0 frees表示程序释放了 0 次内存,4 bytes allocated表示分配了 4 个字节的内存。
 另外,Valgrind 也会报告程序是在哪个位置发生内存泄漏。例如,从下面的信息可以看到,程序发生了一次内存泄漏,位置是main.c文件的第 5 行。

Valgrind 也可以用来检测 C++ 程序的内存泄漏,下面是一个正常的 C++ 程序,没有发生内存泄漏:

1
2
3
4
5
6
7
#include <string>
int main()
{
auto ptr = new std::string("Hello, World!");
delete ptr;
return 0;
}

使用 Valgrind 分析这段程序:

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
$ valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./main_cpp
==31438== Memcheck, a memory error detector
==31438== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31438== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31438== Command: ./main_cpp
==31438==
==31438==
==31438== HEAP SUMMARY:
==31438== in use at exit: 72,704 bytes in 1 blocks
==31438== total heap usage: 2 allocs, 1 frees, 72,736 bytes allocated
==31438==
==31438== 72,704 bytes in 1 blocks are still reachable in loss record 1 of 1
==31438== at 0x4C2DBF6: malloc (vg_replace_malloc.c:299)
==31438== by 0x4EC3EFF: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)
==31438== by 0x40104E9: call_init.part.0 (dl-init.c:72)
==31438== by 0x40105FA: call_init (dl-init.c:30)
==31438== by 0x40105FA: _dl_init (dl-init.c:120)
==31438== by 0x4000CF9: ??? (in /lib/x86_64-linux-gnu/ld-2.23.so)
==31438==
==31438== LEAK SUMMARY:
==31438== definitely lost: 0 bytes in 0 blocks
==31438== indirectly lost: 0 bytes in 0 blocks
==31438== possibly lost: 0 bytes in 0 blocks
==31438== still reachable: 72,704 bytes in 1 blocks
==31438== suppressed: 0 bytes in 0 blocks
==31438==
==31438== For counts of detected and suppressed errors, rerun with: -v
==31438== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

使用 Valgrind 分析 C++ 程序时,有一些问题需要留意。例如,这个程序并没有发生内存泄漏,但是从HEAP SUMMARY可以看到,程序分配了 2 次内存,但却只释放了 1 次内存,为什么会这样呢?

解释:

你的这段代码中,new std::string("Hello, World!") 这行代码实际上进行了两次内存分配:

  1. 第一次分配是创建 std::string 对象本身,这会在堆上分配一块内存用来存储 std::string 对象的元数据,如长度、容量等。
  2. 第二次分配是在 std::string 对象初始化时,对字符串 “Hello, World!” 进行分配,存储实际的字符串内容。

因此,你的程序实际上进行了两次内存分配。

然后在 delete ptr; 这行代码执行时,会调用 std::string 的析构函数,自动释放用于存储字符串内容的内存,然后再释放 std::string 对象本身的内存。所以虽然你只写了一次 delete,但实际上进行了两次内存释放。

然而,Valgrind 报告的 “frees” 数量可能只计算了你直接调用的 delete 操作,而没有计算 std::string 析构函数中自动进行的内存释放,因此它显示 “1 frees”。这也是为什么你的代码显示 “2 allocs, 1 frees”。

检测越界访问

C++ 程序经常出现的 Bug 就是数组越界访问,例如下面的程序出现了越界访问:

1
2
3
4
5
6
7
8
#include <vector>
#include <iostream>
int main()
{
std::vector<int> v(10, 0);
std::cout << v[10] << std::endl;
return 0;
}

使用 Valgrind 分析这段程序,Valgrind 会提示越界访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ g++ -std=c++11 -g -o main_cpp main.cpp
$ valgrind --tool=memcheck --leak-check=full ./main_cpp
==31523== Memcheck, a memory error detector
==31523== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31523== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31523== Command: ./main_cpp
==31523==
==31523== Invalid read of size 4
==31523== at 0x400AD7: main (main.cpp:7)
==31523== Address 0x5ab5ca8 is 0 bytes after a block of size 40 alloc'd
==31523== at 0x4C2E216: operator new(unsigned long) (vg_replace_malloc.c:334)
==31523== by 0x4010D3: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (new_allocator.h:104)
==31523== by 0x401040: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (alloc_traits.h:491)
==31523== by 0x400F91: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (stl_vector.h:170)
==31523== by 0x400E7E: std::_Vector_base<int, std::allocator<int> >::_M_create_storage(unsigned long) (stl_vector.h:185)
==31523== by 0x400D1E: std::_Vector_base<int, std::allocator<int> >::_Vector_base(unsigned long, std::allocator<int> const&) (stl_vector.h:136)
==31523== by 0x400C11: std::vector<int, std::allocator<int> >::vector(unsigned long, int const&, std::allocator<int> const&) (stl_vector.h:291)
==31523== by 0x400AB9: main (main.cpp:6)

Invalid read of size 4表示越界读取 4 个字节,这个操作出现在main.cpp文件的第 7 行。另外可以看到,vector分配了一块 40 字节的内存,程序越界访问紧急着这块内存之后的 4 个字节。

检测未初始化的内存

另一种经常出现的 Bug,就是程序访问了未初始化的内存。例如:

1
2
3
4
5
6
7
8
9
10
#include <iostream>
int main()
{
int x;
if (x == 0)
{
std::cout << "X is zero" << std::endl;
}
return 0;
}

使用 Valgrind 检测这个程序:

1
2
3
4
5
6
7
8
9
$ g++ -std=c++11 -g -o main_cpp main.cpp
$ valgrind --tool=memcheck --leak-check=full ./main_cpp
==31554== Memcheck, a memory error detector
==31554== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==31554== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==31554== Command: ./main_cpp
==31554==
==31554== Conditional jump or move depends on uninitialised value(s)
==31554== at 0x400852: main (main.cpp:6)

输出中提示了main.cpp文件的第 6 行访问了未初始化的内存。

即学即用一波【394. 字符串解码

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
#include<iostream>
#include <queue>
using namespace std;

signed main(){
string s;
cin>>s;
// 3[a2[c]]
int len=s.size();
deque<string> str;
deque<int> q;
string ans="",cur="";
int num=0;
for(int i=0;i<len;i++){
if(s[i]>='0'&&s[i]<='9'){
num=num*10+(int)(s[i]-'0');
}else if(s[i]=='['){
q.push_back(num);
num=0;
if(cur.size()>0) str.push_back(cur);
cur="";
}else if(s[i]==']'){
if(cur.size())
str.push_back(cur);
string str_tmp=str.back();str.pop_back();
int num_tmp=q.back();q.pop_back();
string res="";
for(int j=0;j<num_tmp;j++){
res=res+str_tmp;
}
if(q.size()){
string tmp="";
tmp=str.back();
str.pop_back();
str.push_back(tmp+res);
}else{
ans=ans+res;
}
cur="";
}else{
if(q.size()){
cur=cur+s[i];
}else{
ans=ans+s[i];
}
}
}

cout<<ans<<endl;
return 0;
}
/*
"3[z]2[2[y]pq4[2[jk]e1[f]]]ef"
*/

使用内存检测工具检测

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
(base) sv@sv-NF5280M5:/home/sv/桌面$ vim main.cpp
(base) sv@sv-NF5280M5:/home/sv/桌面$ g++ -g -o main_c main.cpp
(base) sv@sv-NF5280M5:/home/sv/桌面$ valgrind --tool=memcheck --leak-check=full ./main_c
==1335995== Memcheck, a memory error detector
==1335995== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1335995== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==1335995== Command: ./main_c
==1335995==
3[z]2[2[y]pq4[2[jk]e1[f]]]ef
==1335995== Use of uninitialised value of size 8
==1335995== at 0x49ACC38: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28)
==1335995== by 0x10A87C: main (main.cpp:32)
==1335995==
==1335995== Invalid read of size 8
==1335995== at 0x49ACC38: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28)
==1335995== by 0x10A87C: main (main.cpp:32)
==1335995== Address 0x1e8 is not stack'd, malloc'd or (recently) free'd
==1335995==
==1335995==
==1335995== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==1335995== Access not within mapped region at address 0x1E8
==1335995== at 0x49ACC38: std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28)
==1335995== by 0x10A87C: main (main.cpp:32)
==1335995== If you believe this happened as a result of a stack
==1335995== overflow in your program's main thread (unlikely but
==1335995== possible), you can try to increase the size of the
==1335995== main thread stack using the --main-stacksize= flag.
==1335995== The main thread stack size used in this run was 8388608.
==1335995==
==1335995== HEAP SUMMARY:
==1335995== in use at exit: 1,183 bytes in 5 blocks
==1335995== total heap usage: 7 allocs, 2 frees, 74,911 bytes allocated
==1335995==
==1335995== LEAK SUMMARY:
==1335995== definitely lost: 0 bytes in 0 blocks
==1335995== indirectly lost: 0 bytes in 0 blocks
==1335995== possibly lost: 0 bytes in 0 blocks
==1335995== still reachable: 1,183 bytes in 5 blocks
==1335995== suppressed: 0 bytes in 0 blocks
==1335995== Reachable blocks (those to which a pointer was found) are not shown.
==1335995== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==1335995==
==1335995== Use --track-origins=yes to see where uninitialised values come from
==1335995== For lists of detected and suppressed errors, rerun with: -s
==1335995== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
段错误

一下就知道,问题在这

1
2
tmp=str.back();
str.pop_back();

asan和valgrind的区别

  1. 实现方式:
    1. ASan:ASan是一种静态和动态混合的工具,它通过在编译时插入额外的代码来检测内存错误。ASan会在程序运行时检测内存访问错误,如缓冲区溢出、使用已释放的内存等。
    2. Valgrind:Valgrind是一个动态二进制工具,它通过在程序运行时对二进制代码进行重写和模拟来检测内存错误。Valgrind提供了一种更广泛的工具集,包括Memcheck(用于内存错误检测)、Cachegrind(用于缓存分析)、Callgrind(用于函数调用跟踪)等。
  2. 性能开销:
    1. ASan:ASan通常比Valgrind更快,因为它在编译时引入了一些运行时检查,但这些检查开销相对较小。它对程序性能的影响通常较小。
    2. Valgrind:Valgrind在运行时对程序进行重写和模拟,因此通常比ASan更慢。在某些情况下,Valgrind的性能开销可能会很高,特别是对于大型应用程序。
  3. 检测能力:
    1. ASan:ASan主要用于检测内存访问错误,如缓冲区溢出、内存泄漏等。它在这些方面非常强大。
    2. Valgrind:Valgrind的Memcheck工具不仅可以检测内存访问错误,还可以检测未初始化的内存访问、内存泄漏、不匹配的内存分配/释放等多种问题。因此,Valgrind在某些情况下可能提供更全面的错误检测。

参考资料