valgrind使用方法

十二月 08, 2024 [Linux 与系统] #Linux #Shell 与命令行 #obsidian #操作系统原理

valgrind使用方法

1. Preface

 valgrind是一套Linux下开源的程序仿真调试和分析工具的集合;集合中的每个工具负责执行某种类型的仿真,调试,或者分析任务;它的主要结构包括一个内核(软件模拟CPU环境)以及一系列的小工具。

valgrind包含的工具主要如下:

2. valgrind安装

源码下载地址:Valgrind官方网站

wget http://www.valgrind.org/downloads/valgrind-3.14.0.tar.bz2

2.1 解压安装包

tar -xvf valgrind-3.20.0.tar.bz2

2.2 运行autogen.sh
cd valgrind-3.20.0./autogen.sh

如果提示缺少相应的autotools系列工具,则执行:

sudo apt get install autoconf

autotools系列工具包括如下子工具:

2.3 运行configure脚本

./configure

2.4 编译安装
make sudo make install
3. valgrind使用

使用格式:

valgrind [options] prog-and-args

options选项:

关于输出的选线:

4. Memcheck的使用

其他的参数可查阅说明书:https://valgrind.org/docs/manual/mc-manual.html#mc-manual.overview

Memcheck可检测的错误包括:

1.非法的读写错误

在程序读写Memcheck认为不合法的地址时,会输出类似的错误,例如访问已经释放的内存,以及非法的栈地址;

测试代码:

#include <stdlib.h>int main(int argc, char* argv[]){    int* p = (int*)malloc(sizeof(int));    *p = 1;    *(p+1) = 2;      // 非法地址,未经过分配    free(p);    return 0;}

Memcheck Log输出信息:

img

输出信息中需要注意如果ERROE SUMMARY: 后的错误数量不为0,则仔细查看输出的日志信息;

2.使用未初始化的值

测试代码:

#include <stdio.h>int main(int argc, char* argv[]){    int x;    printf("The value is %d\n", x);    return 0;}

Memcheck Log输出信息:

如果不想Memcheck检测此类错误,可以添加参数设置:

--undef-value-errors=no

3.未初始化或者在系统调用中无法寻址的变量

Memcheck会检测所有系统调用涉及到的参数,包括如下:

测试代码:

#include <stdlib.h>#include <unistd.h>void test(){   char* arr  = (char*)malloc(10);   int*  arr2 = (int*)malloc(sizeof(int));   write( 1 /* stdout */, arr, 10 );   exit(arr2[0]);   // arr2[0]无法进行寻址}int main(void){   test();}

Memcheck输出信息:

img

4. 非法的内存释放

Memcheck会对程序中使用new/malloc分配的内存进行持续追踪,因此它能够检测出传递给free/delete的内存地址是否是合法的;例如对同一块动态分配的内存执行两次释放;则第二次释放,free/delete的参数(地址)就是不合法的

测试程序:

#include <stdlib.h>#include <unistd.h>int main(void){   char* arr  = (char*)malloc(10);   free(arr);   free(arr);      // 重复释放内存   return 0;}

img

5. 堆内存释放和分配函数不匹配

在c++中,内存的分配与释放函数必须匹配:

#include <stdlib.h>#include <unistd.h>#include <malloc.h>int main(void){    char* arr  = (char*)malloc(10);    delete arr;    return 0;}

img

备注:在Linux平台上混用new和free并不会导致出错,但是在其他的平台上却不一定,可能会导致程序crash;

The reason behind the requirement is as follows. In some C++ implementations, delete[] must be used for objects allocated by new[] because the compiler stores the size of the array and the pointer-to-member to the destructor of the array's content just before the pointer actually returned. delete doesn't account for this and will get confused, possibly corrupting the heap. <from: https://valgrind.org/docs/manual/mc-manual.html#mc-manual.bad-syscall-args>

6. 源地址和目标地址的出现内存覆盖(overlapping)

c/c++语言可直接对内存进行操作,提供了许多的内存拷贝函数,memcpy, strcpy, strncpy, strcat, strncat,POSIX标准规定,如果对象之间的内存拷贝出现相互覆盖,则将导致最后的结果是不确定的;

7. 分配内存的参数不合法(fishy argument)

  在分配内存时需要指定内存的大小,如果指定内存大小的参数不合法(负值或者过大的值2^64),则memcheck会指出不合法的内存值;memcheck对下列函数的参数都会进行校验:

malloc, calloc, realloc, memalign, new, new []. __builtin_new, __builtin_vec_new

#include <stdlib.h>#include <unistd.h>#include <malloc.h>#include <string.h>int main(void){    char* arr  = (char*)malloc(-10);    for (size_t i = 0; i < 10; i++)    {        /* code */        arr[i] = i+1;    }    free(arr);    return 0;}

img

8. 内存泄漏检测

  memcheck会在程序运行过程中追踪所有堆内分配的内存,因此在程序退出的时候,它能够知到哪些动态分配的内存还没有被释放;

可通过以下两种方式访问分配的内存:

  1. start-pointer: 此类指针指向内存的起始位置(连续内存);
  2. interior-pointer:此类指针指向内存中间的某个位置(连续内存);

interior-pointer例子: 指向std::string的指针。在stdString中,前3个word用来存储字符串的长度,容量,以及引用计数,后续才是字符串的实际内容,所以指向stdString的指针,实际的起始位置是存储位置中3个word之后的位置。

通过增加参数--leak-check=full,memcheck可以给出内存泄漏的详细描述信息,可能导致内存泄漏的情况如下所示:

img

 Pointer chain                AAA Leak Case   BBB Leak Case     -------------            -------------   -------------(1)  RRR ------------> BBB                    DR(2)  RRR ---> AAA ---> BBB    DR              IR(3)  RRR               BBB                    DL(4)  RRR      AAA ---> BBB    DL              IL(5)  RRR ------?-----> BBB                    (y)DR, (n)DL(6)  RRR ---> AAA -?-> BBB    DR              (y)IR, (n)DL(7)  RRR -?-> AAA ---> BBB    (y)DR, (n)DL    (y)IR, (n)IL(8)  RRR -?-> AAA -?-> BBB    (y)DR, (n)DL    (y,y)IR, (n,y)IL, (_,n)DL(9)  RRR      AAA -?-> BBB    DL              (y)IL, (n)DLPointer chain legend:- RRR: a root set node or DR block- AAA, BBB: heap blocks- --->: a start-pointer- -?->: an interior-pointerLeak Case legend:- DR: Directly reachable- IR: Indirectly reachable- DL: Directly lost- IL: Indirectly lost- (y)XY: it's XY if the interior-pointer is a real pointer- (n)XY: it's XY if the interior-pointer is not a real pointer- (_)XY: it's XY in either case

memcheck官方手册对其描述为:

参数: --show-leak-kinds=definite,possible,(all).可指定显示特定类型内存泄露错误;

5. DRD的使用

  DRD主要用于检查多线程c和c++程序中的错误;适用于使用POSIX标准线程的程序或者是基于POSIX标准多线程概念设计的程序;

POSIX标准多线程概念具备如下特点:

多线程程序存在的问题包括:

常用参数:

DRD可以查看互斥量的占用时间,示例代码:

#define _GNU_SOURCE 1#include <assert.h>#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <time.h>#include <unistd.h>static void delay_ms(const int ms) {  struct timespec ts;  assert(ms >= 0);  ts.tv_sec = ms / 1000;  ts.tv_nsec = (ms % 1000) * 1000 * 1000;  nanosleep(&ts, 0);}void lock_mutex(const int ms) {  pthread_mutex_t     mutex;  fprintf(stderr, "Locking mutex ...\n");  pthread_mutex_init(&mutex, NULL);  pthread_mutex_lock(&mutex);  delay_ms(ms);  pthread_mutex_unlock(&mutex);  pthread_mutex_destroy(&mutex);}int main(int argc, char** argv) {  int interval = 0;  int optchar;  while ((optchar = getopt(argc, argv, "i:")) != EOF)   // 处理命令行参数,指定延时时间  {    switch (optchar)     {    case 'i':      interval = atoi(optarg);      break;    default:      fprintf(stderr, "Usage: %s [-i <interval time in ms>].\n", argv[0]);      break;    }  }  lock_mutex(interval);  fprintf(stderr, "Done.\n");  return 0;}

编译代码:

gcc -g -lpthread ./main.cpp -o main

通过参数--exclusive-threshold=<n>检查独占锁占用的时间,通过如下命令检测独占锁占用时间是否超过10ms:

valgrind --tool=drd --exclusive-threshold=15 ./main -i 20

输出:可以看到在上图中,(互斥量)独占锁被占用了22ms,大于在DRD中设置的阈值15ms,因此会给出提示;

img

调整参数:

valgrind --tool=drd --exclusive-threshold=15 ./main -i 10

输出:可以看到在上图中,独占锁被占的时间没有超过15ms的,因此不会给出提示:

img

除了检测互斥量的占用时间,还可以检测读写锁的占用时间,示例代码如下:

#define _GNU_SOURCE 1#include <assert.h>#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <time.h>#include <unistd.h>static void delay_ms(const int ms) {  struct timespec ts;  assert(ms >= 0);  ts.tv_sec = ms / 1000;  ts.tv_nsec = (ms % 1000) * 1000 * 1000;  nanosleep(&ts, 0);}void write_lock(const int ms){   pthread_rwlock_t rwlock;      // 读写锁   fprintf(stderr, "Locking rwlock exclusively ...\n");   pthread_rwlock_init(&rwlock, NULL);   pthread_rwlock_wrlock(&rwlock);   delay_ms(ms);   pthread_rwlock_unlock(&rwlock);   pthread_rwlock_destroy(&rwlock);}int main(int argc, char** argv) {  int interval = 0;  int optchar;  while ((optchar = getopt(argc, argv, "i:")) != EOF)   // 处理命令行参数,指定延时时间  {    switch (optchar)     {    case 'i':      interval = atoi(optarg);      break;    default:      fprintf(stderr, "Usage: %s [-i <interval time in ms>].\n", argv[0]);      break;    }  }  write_lock(interval);  fprintf(stderr, "Done.\n");  return 0;}

使用DRD进行检测:valgrind --tool=drd --exclusive-threshold=10 ./main -i 20

输出:

img

检测读锁的占用时间,示例代码如下所示:

#define _GNU_SOURCE 1#include <assert.h>#include <pthread.h>#include <stdio.h>#include <stdlib.h>#include <time.h>#include <unistd.h>static void delay_ms(const int ms) {  struct timespec ts;  assert(ms >= 0);  ts.tv_sec = ms / 1000;  ts.tv_nsec = (ms % 1000) * 1000 * 1000;  nanosleep(&ts, 0);}void read_lock(const int ms){    pthread_rwlock_t readLock;    fprintf(stderr, "Locking rwlock exclusively ...\n");    pthread_rwlock_init(&readLock, NULL);    pthread_rwlock_rdlock(&readLock);    pthread_rwlock_rdlock(&readLock);    delay_ms(20);    pthread_rwlock_unlock(&readLock);    // 读锁不是独占锁    pthread_rwlock_unlock(&readLock);         pthread_rwlock_destroy(&readLock);}int main(int argc, char** argv) {  int interval = 0;  int optchar;  while ((optchar = getopt(argc, argv, "i:")) != EOF)   // 处理命令行参数,指定延时时间  {    switch (optchar)     {    case 'i':      interval = atoi(optarg);      break;    default:      fprintf(stderr, "Usage: %s [-i <interval time in ms>].\n", argv[0]);      break;    }  }  read_lock(interval);  fprintf(stderr, "Done.\n");  return 0;}

读锁不是独占锁,所以需要使用参数--shared-threshold=<n>来进行分析:

增加参数-s,可在结果中展示具体的错误信息:valgrind --tool=drd --shared-threshold=20 -s ./main -i 20

img

对于非独占锁,如果lock的次数与unlock的次数不一致,也会进行提示:

void read_lock(const int ms){    pthread_rwlock_t readLock;    fprintf(stderr, "Locking rwlock exclusively ...\n");    pthread_rwlock_init(&readLock, NULL);    pthread_rwlock_rdlock(&readLock);    pthread_rwlock_rdlock(&readLock);    delay_ms(ms);    pthread_rwlock_unlock(&readLock);      // 仅释放一次    //pthread_rwlock_unlock(&readLock);        pthread_rwlock_destroy(&readLock);}

DRD检测命令:valgrind --tool=drd --shared-threshold=20 -s ./main -i 10

检测输出结果:

img

如上图所示,输出结果提示,第52行的destory出错,是因为destory上面缺少一个unlock操作;