Skip to content
Dreamszhu edited this page Nov 11, 2020 · 6 revisions

Valgrind 是一个动态分析工具构建框架,可以用来分析程序的内存、线程等问题探测, 程序性能分析等。具体的功能见官网,这是非常值得尝试的工具。这里要使用的就是valgrind 的内存错误分析工具。

支持很多工具:memcheck,addrcheck,cachegrind,Massif,helgrind和Callgrind等。在运行Valgrind时,你必须指明想用的工具,如果省略工具名,默认运行memcheck。

我们大部分的时候使用的是Web模式,这时的调试相对麻烦一些。

由于我们需要发现PHP的内存泄漏,根据前面的章节我们知道 PHP 的内存分配是有一个内存池的, 也就是说,并不是每次 emalloc 都会向操作系统申请内存,如果池有足够的内存的话是会从池里进行分配的, 而 valgrind 分析内存泄漏依赖的是内存的实际分配和实际释放之间的关系,它会记下所有的 malloc 调用和 free 调用,如果出现不匹配的情况,那么就是发生了内存泄漏,所以在这种情况 下我们需要将PHP的内存管理功能关闭才能不影响到我们的分析。

PHP 提供了一个 hook,我们可以在启动PHP前指定 USE_ZEND_ALLOC 环境变量为 0,即关闭内存管理功能。 这样所有的内存分配都会直接向操作系统申请,这样 valgrind 就可以帮助我们定位问题。

例子:

// 关闭这个,valgrind 才能正确跟踪
export USE_ZEND_ALLOC=0
// 开启这个,valgrind 才能正确显示所在代码
export ZEND_DONT_UNLOAD_MODULES=1
valgrind --leak-check=full php ../unit-tests/test.php
valgrind --tool=memcheck --leak-check=full --show-reachable=yes php ../unit-tests/test.php

可以加上选项 --track-origins=yes 来查找,未初始化的来源

  • php-fpm

想使用 valgrind 分析 php-fpm 可以通过修改 php-fpm 启动脚本,在启动脚本中增加环境变量以及修改原来的启动命令,修改如下:

export USE_ZEND_ALLOC=0

php_fpm_BIN="/usr/local/php/bin/php-fpm"
php_fpm_BIN="valgrind --log-file=/home/reeze/valgrind-log/%p.log /usr/local/php/bin/php-fpm"

参数

--log-file 报告文件名。如果没有指定,输出到stderr。
--tool=memcheck 指定Valgrind使用的工具。Valgrind是一个工具集,包括Memcheck、Cachegrind、Callgrind等多个工具。memcheck是缺省项。
--leak-check 指定如何报告内存泄漏(memcheck能检查多种内存使用错误,内存泄漏是其中常见的一种),可选值有:

    no 不报告
    summary 显示简要信息,有多少个内存泄漏。summary是缺省值。
    yes 和 full 显示每个泄漏的内存在哪里分配。

show-leak-kinds 指定显示内存泄漏的类型的组合。类型包括definite, indirect, possible,reachable。也可以指定all或none。缺省值是definite,possible。

错误类型

  • illegal read 非法读 / illegal write 非法写

如下代码分配了长度为1024的缓存buf,然后在buf+1020的位置写入一个8字节的uint64_t,并打印它。这个uint64_t已经超出buf的界限。

char* buf = (char*) malloc (1024);
uint64_t* bigNum = (uint64_t*)(buf + 1020);
*bigNum = 0x12345678AABBCCDD;
printf ("bigNum is %llu\n", *bigNum);
free(buf);
  • Use of uninitialised values 使用未初始化的值

如下代码打印了一个未初始化的值unused。

int unused; 
printf ("unused=%d", unused);
  • Use of uninitialised or unaddressable values in system calls 系统调用时使用未初始化的值或不能访问的地址

如下代码中arr没有初始化,就调用write()输出了。

char* arr = (char*)malloc(10);
write (1, arr, 10);
  • illegal fress 非法释放

如下代码中,释放的地址arr+2不是分配时得到的地址arr。

char* arr = (char*)malloc(10);
free (arr+2);
  • When a heap block is freed with an inappropriate deallocation function 不匹配的释放

如下代码中分配得到一个int数组d[],释放的时候却使用delete,而不是delete[]。

int* d = new int[5];
delete d;
  • Overlapping source and destination blocks 移动/复制缓存数据时,源与目标重叠

如下代码中memcpy复制的源和目标重叠。

char buf[256] = "";
memcpy (buf+10, buf, 100);
  • Fishy argument values 函数调用的参数可能不合法

下面的代码中,使用了负值-10调用malloc。

int sz = -10;
int* arr2 = (int*)malloc (sz);
free(arr2);
  • Memory leak detection 内存泄露

以下代码分配了100字节没有释放。

int* arr = (int*)malloc (100);
  • 关于内存泄露

memcheck跟踪所有malloc()/new()分配的堆块,所以它能在程序退出时知道哪些块还没有释放。 这里把程序能访问到的指针集合叫做root-set。memcheck根据从root-set能不能到达某个块,判断这个块是不是泄露了(reachable还是lost)。 到达块有两种方式:一是start-pointer,它指向块的开始地址;二是interior-pointer,它指向块的中间某个位置。 有start-pointer引用的缓存被认为是reachable。 有interior-pointer引用的缓存则不一定了。 它可能是分配之后的指针故意向后移动的结果。这时程序使用缓存开始的几个字节保存特别信息,而真正的数据在后面某个位置。比如指向C++的std::string内部数组的指针、指向new[]分配的C++对象数组的指针、多重继承的情况等。这时缓存是reachable的。 但它也可能只是偶然指向缓存。这时缓存就是lost的。 所以interior-pointer引用的缓存是possible lost。

根据从root-set到缓存是否需要跳转,memcheck 将分配的缓存的状态分为几种:

  • directly reachable: root-set中的指针指向缓存,不需跳转
  • indirectly reachable: root-set中的指针不直接指向缓存,需要跳转
  • directly lost:没有任何指针指向缓存
  • indirectly lost:有指针指向缓存,但从root-set中也无法到达这个指针

下面是memcheck中LEAK SUMMARY部分中的项目:

  • Still reachable: 从root-set中通过start-pointer直接到达或跳转到达缓存。这一项是确定不是lost的,用户无需关系。
  • Definitely lost:(3)的情况。没有任何指针指向缓存。这一项确定是lost的,用户需解决。
  • Indirectly lost:(4)和(9)中BBB的情况。有start-pointer或interior-pointer指向缓存,但从root-set不能到达这些指针。这一项可以推迟解决,因为Indirectly lost一定与definitely lost对应,definitely lost解决了,indirectly lost也就变成reachable或者possible lost了。
  • Possibly lost: 有interior-pointer指向缓存,从root-set能到达这些指针。因为memcheck不能判断interior-pointer是否lost,所以需要用户排除。

所以实际上只要关心definitely lost和possible lost就可以了。 缺省值 -show-leak-kinds=definite,possible

Clone this wiki locally