5. 深入了解

Valgrind 模拟一个 Intel x86 处理器,并在该合成处理器中运行我们的测试程序。这两个处理器并不完全相同。Valgrind 被编译成一个共享对象,valgrind.so。一个 shell 脚本valgrind设置LD_PRELOAD环境变量,使其指向 valgrind.so。这使得 .so 文件作为额外的库加载到任何后续执行的动态链接 ELF 二进制文件中,从而允许程序被调试。

动态链接器调用 Valgrind 的初始化函数。然后合成 CPU 从真实 CPU 接管控制权。在内存中可能还有其他 .so 文件。动态链接器调用所有这些 .so 文件的初始化函数。现在动态链接器调用main已加载程序的 main 函数。当 main 函数返回时,合成 CPU 调用 valgrind.so 的终结函数。在终结函数执行期间,会打印所有检测到的错误的摘要,并检查内存泄漏。终结函数退出,将控制权从合成 CPU 交还给真实的 CPU。

5.1. Valgrind 如何跟踪每个字节的有效性

对于处理的每个字节,合成处理器维护 9 个位,8 个 'V' 位和 1 个 'A' 位。“V”位指示字节中 8 位的有效性,“A”位指示字节地址的有效性。这些有效值 (V) 位仅在两种情况下被检查

  1. 当数据用于地址生成时,

  2. 当需要做出控制流决策时。

在上述两种情况中的任何一种情况下,如果数据被发现是未定义的,将生成错误报告。但是,在复制或添加未定义数据时,不会生成错误报告。

然而,浮点数据的情况有所不同。在浮点读取指令期间,会检查与数据对应的“V”位。因此,在浮点数的情况下,复制未初始化的值会产生错误。

#include <stdlib.h>
int main()
{
        int *p, *a;
        p = malloc(10*sizeof(int));
        a = malloc(10*sizeof(int));
        a[3] = p[3];
        free(a);
        free(p);
        return 0;
}

/*  produce no errors */

#include <stdlib.h>
int main()
{
        float *p, *a;
        p = malloc(10*sizeof(float));
        a = malloc(10*sizeof(float));
        a[3] = p[3];
        free(a);
        free(p);
        return 0;
}

/* produces error */

内存中但不在 CPU 中的所有字节都有一个关联的有效地址 (A) 位,该位指示程序是否可以访问相应的内存位置。当程序启动时,会设置与每个全局变量对应的“A”位。当调用malloc, new或任何其他内存分配函数时,会设置与已分配字节对应的“A”位。使用free/new/new‘’释放已分配的块时,相应的“A”位被清除。在执行系统调用时,“A”位会进行相应的更改。

当从内存加载值时,Valgrind 会检查与每个字节对应的“A”位,如果与字节对应的“A”位被设置,则会检查其“V”位。如果“V”位未被设置,则会生成错误,并且“V”位被设置为指示有效性。这避免了长链错误。如果与加载的字节对应的“A”位为 0,则其“V”位将被强制设置为已设置,即使该值无效。

看看下面的程序。运行它。

#include <stdlib.h>
int main()
{
        int *p, j;
        p = malloc(5*sizeof(int));
        j = p[5];
        if (p[5] == 1)
                i = p[5]+1;
        free(p);
        return 0;
}

这里发生两个错误。它们都是由于访问地址位置p + sizeof(int)*5程序未分配该地址。在执行j = p[5]时,由于地址p + sizeof(int)*5无效,因此从位置开始的 4 个字节的“V”位p+sizeof(int)*5被强制设置为已设置。因此,未初始化的值既不会在执行期间出现j = p[5]也不会在执行期间出现if(p[5]==1).

5.2. 缓存分析

现代 x86 机器使用两级缓存。这两级缓存是 L1 和 L2,其中 L1 是一个分离缓存,由指令缓存 (I1) 和数据缓存 (D1) 组成。L2 是一个统一缓存。

缓存的配置意味着它的大小、关联性和行数。如果处理器请求的数据出现在较高层级中,则称为命中。如果在较高层级中未找到数据,则该请求称为未命中。然后访问层级结构中的较低层级以检索包含请求数据的块。在现代机器中,首先在 L1 中搜索处理器请求的数据/指令。如果命中,则将该数据/指令复制到处理器中的某个寄存器。否则,搜索 L2。如果命中,则将数据/指令复制到 L1,然后从 L1 复制到寄存器。如果对 L2 的请求也是未命中,则必须访问主内存。

Valgrind 可以模拟缓存,这意味着它可以显示程序运行时缓​​存中发生的事情。为此,首先使用以下选项编译您的程序-g选项,像往常一样。然后使用 shell 脚本cachegrind而不是valgrind.

示例输出

==7436== I1  refs:      12,841
==7436== I1  misses:       238
==7436== L2i misses:       237
==7436== I1  miss rate:   1.85%
==7436== L2i miss rate:   1.84%
==7436==
==7436== D   refs:       5,914  (4,626 rd + 1,288 wr)
==7436== D1  misses:       357  (  324 rd +    33 wr)
==7436== L2d misses:       352  (  319 rd +    33 wr)
==7436== D1  miss rate:    6.0% (  7.0%   +   2.5%  )
==7436== L2d miss rate:    5.9% (  6.8%   +   2.5%  )
==7436==
==7436== L2 refs:          595  (  562 rd +    33 wr)
==7436== L2 misses:        589  (  556 rd +    33 wr)
==7436== L2 miss rate:     3.1% (  3.1%   +   2.5%  )

   L2i misses means the number of instruction misses that occur in L2
cache.
   L2d misses means the number of data misses that occur in L2 cache.
   Total number of data references = Number of reads + Number of writes.
   Miss rate means fraction of misses that are not found in the upper
level.

shell 脚本cachegrind还会生成一个文件,cachegrind.out,其中包含逐行缓存分析信息,这些信息对于人类来说是难以理解的。一个程序vg_annotate可以轻松地解释这些信息。如果 shell 脚本vg_annotate在不带任何参数的情况下使用,它将读取文件cachegrind.out并生成人类可理解的输出。

当 C、C++ 或汇编源代码程序作为输入传递给vg_annotate时,它会显示缓存读取、写入、未命中等的次数。

I1 cache:         16384 B, 32 B, 4-way associative
D1 cache:         16384 B, 32 B, 4-way associative
L2 cache:         262144 B, 32 B, 8-way associative
Command:          ./a.out
Events recorded:  Ir I1mr I2mr Dr D1mr D2mr Dw D1mw D2mw
Events shown:     Ir I1mr I2mr Dr D1mr D2mr Dw D1mw D2mw
Event sort order: Ir I1mr I2mr Dr D1mr D2mr Dw D1mw D2mw
Thresholds:       99 0 0 0 0 0 0 0 0
Include dirs:
User annotated:   valg_flo.c
Auto-annotation:  off

用户注释的源代码valg_flo.c:

Ir I1mr I2mr Dr D1mr D2mr Dw D1mw D2mw

 .   .   .   .   .    .   .   .    .   #include<stdlib.h>
 .   .   .   .   .    .   .   .    .   int main()
 3   1   1   .   .    .   1   0    0   {
 .   .   .   .   .    .   .   .    .           float *p, *a;
 6   1   1   .   .    .   3   0    0           p = malloc(10*sizeof(float));
 6   0   0   .   .    .   3   0    0           a = malloc(10*sizeof(float));
 6   1   1   3   1    1   1   1    1           a[3] = p[3];
 4   0   0   1   0    0   1   0    0           free(a);
 4   0   0   1   0    0   1   0    0           free(p);
 2   0   0   2   0    0   .   .    .   }