内存访问错误检查器

作者:Cesare Pizzi

所有 C 程序员至少都见过一次可怕的词语“Segmentation fault—Core dumped”,这通常发生在他们最新的创作运行之后。通常,此消息是由于内存管理中的错误引起的。(正如所有 C 程序员都知道的那样,这种语言在访问内存时并不关心边界或限制。)在本文中,我计划比较三种用于追踪此类错误的产品

  • Checker 0.9.9.1

  • Electric Fence 2.0.5

  • Mem-Test 0.10

我将解释如何使用这三种不同的产品,使用包含非常常见错误的小型 C 代码示例。我将展示每种产品如何(以及是否)检测到错误。所有这三个软件包都用它们自己的代码替换了常用的内存访问函数,并且可以在内存问题出现时检测到它们。我在我的奔腾 133 Linux 机器上进行了测试,该机器配备 32MB 内存和 2.0.34 内核。
关于安装的简要说明

Checker 以常见的 tgz 格式(gzipped tar 文件)提供,安装过程很简单。运行 “configure” 脚本,然后 make 所有文件。我的安装过程很顺利;我没有看到任何问题。注意:您需要 gcc 2.8.1 才能使用最新版本的 Checker。

Electric Fence 提供二进制和源代码格式,需要内核 1.1.83 或更高版本。

Mem-Test 以 tgz 格式提供,并且使用提供的 Makefile 构建非常简单。

它们的工作原理

Electric Fence (EF) 是一个库——将其链接到您的程序,然后运行它。EF 将在错误指令的确切行(而不是 100 行之后)引起段错误,因此通过使用调试器跟踪程序,您可以找到问题的根源。在您的程序分配的每个区域之后(或之前,通过使用正确的选项)放置一个无法访问的内存页,当程序超出边界时,EF 将立即引发错误。

Mem-Test 是另一个您可以简单地链接到您的对象的库——只需记住包含头文件 mem_test_user.h 即可。正如我们将看到的,这个程序与其他两个程序有点不同,它检测特定的错误。当程序运行时,它会创建一个日志,在其中存储所有内存分配/释放。通过使用软件包中提供的 Perl 脚本,它将向您显示代码中存在的内存泄漏。由于 Electric Fence 不检测这种特定错误,因此它可以与 Mem-Test 结合使用。

Checker 也是一个库,它利用了 gcc-fcheck-memory-usage 选项。实际上使用了一个不同的编译器来构建您的程序:checkergcc。它是一个桩,它调用 gcc 并使用其自己的内存访问库编译程序。程序编译完成后,您可以运行它,checker 将向您显示一份完整的报告,其中包含它在您的源代码中发现的错误。Checker 使用位图来存储程序正在使用的任何内存区域。此位图将包含每个内存区域的访问权限。例如,一个区域可以是只写的(当变量尚未初始化时)、可读写的、不可访问等等。通过这种方式,它将能够检测到内存访问错误。

示例代码

我们将要查看的六个 C 代码片段是

  • postr.c:此代码 (列表 1) 执行对未初始化内存区域的读取(使用 printf)。缺少字符串终止符 (\0) 将迫使 printf 在 malloc 区域之后读取。

  • prer.c:此代码片段 (列表 2) 包含两个错误。printf 正在访问分配区域之前的一个字节(指针已递减),然后 free 是使用 malloc 未返回的地址完成的。

  • postw.c:在此代码 (列表 3) 中,strcpy 正在 10 字节区域中写入 12 个字节(包括 \0)。此外,printf 正在读取未初始化的最后两个字节。

  • prew.c:此代码 (列表 4) 正在写入已分配内存之前的位置。free 和 printf 将像之前的示例一样导致错误。

  • uninit.c:此代码 (列表 5) 对 NULL 指针进行赋值。对于 C 语言新手程序员来说,这是一个常见的错误。

  • unfree.c:在此示例 (列表 6) 中,我遗漏了释放一些已分配的内存。

后读

为了测试 Checker,我使用以下命令行编译了 列表 1

checkergcc -o postr postr.c

所有 gcc 命令行选项都可以与 Checker 一起使用。编译过程顺利完成,当我运行 postr 时,我得到了以下输出

From Checker (pid:00411): (ruh) read uninitialized byte(s) in a block.
When Reading 5 byte(s) at address 0x0805ce1c, inside the heap (sbrk).
0 byte(s) into a block (start: 0x805ce1c, length: 10, mdesc: 0x0).
The block was allocated from:
  pc=0x08054e2b in chkr_malloc at stubs-malloc.c:52
  pc=0x08048812 in main at postr.c:10
  pc=0x08054ee1 in this_main at stubs-main.c:14
  pc=0x0804875a in *unknown* at *unknown*:0
Stack frames are:
  pc=0x08054ebf in chkr_stub_printf at stubs-stdio.c:54
  pc=0x080489f1 in main at postr.c:17
  pc=0x08054ee1 in this_main at stubs-main.c:14
  pc=0x0804875a in *unknown* at *unknown*:0
exa
Checker 执行了程序并发现了问题——第 17 行(printf 行)的未初始化读取。这是由内存区域中缺少字符串终止符引起的。乍一看,此输出看起来相当混乱,但如果您仔细阅读,您会发现很多信息:它发现的错误类型、内存分配的位置(第 10 行)以及问题发生的位置(第 17 行)。

要使用 Mem-Test 编译程序,您必须对 postr 源代码进行轻微修改。添加头文件(#include "mem_test_user.h")以包装各种内存分配函数并使用修改后的版本。使用以下命令编译程序

gcc -o postr postr.c -lmem_test

我在编译命令中添加了另一个库 (mem_test)。当您运行 postr 可执行文件时,新库将创建一个名为 MEM_TEST_FILE 的文件,其中将记录所有内存访问和泄漏。在这种特定情况下,Mem-Test 没有发现问题,因为它旨在仅识别内存泄漏。

对于 Electric Fence,我们需要重新编译程序,包括参考库

gcc -g -o postr postr.c -lefence

我添加了 -g 选项以在可执行文件中包含调试信息。这是必需的,因为 EF 将在错误行上精确地引起段错误,因此您需要单步执行代码以找到导致问题的确切行。这是可执行文件的输出

Electric Fence 2.0.5 Copyright (C) 1987-1995 Bruce Perens. exa
EF 没有在代码中发现任何问题,因此没有生成错误。

EF 有四个不同的开关,可以通过设置以下环境变量之一来启用:EF_ALIGNMENTEF_PROTECT_BELOWEF_PROTECT_FREEEF_ALLOW_MALLOC_0

EF_ALIGNMENT 设置 malloc(或 calloc 和 realloc)完成的每次内存分配的对齐方式。默认情况下,此大小设置为 sizeof(int),因为这通常是 CPU 要求的对齐方式。当您分配的大小不是字大小的倍数时,这可能会成为问题。由于无法访问的页面必须设置为字对齐地址,因此在已分配的内存到无法访问的页面之间存在一个空洞。您可以通过将环境变量设置为 0 来解决此问题;通过这种方式,您将能够找到单字节溢出。这将强制 malloc 返回一个未对齐的地址,但这在大多数情况下不是问题。在某些情况下(当您为必须字对齐的对象进行奇数大小分配时),您将收到总线错误 (SIGBUS)。我从未使用 EF 见过 SIGBUS 错误(我在实际程序中使用过它);我从 EF 文档中获得了此信息。

EF 通常会将无法访问的页面放置在每次内存分配之后。通过将 EF_PROTECT_BELOW 设置为 1,它会将此页面放置在分配之前,因此您可以检查下溢。

EF 允许您分配已释放的内存。如果您认为您的程序正在访问空闲内存,请将 EF_PROTECT_FREE 设置为 1。EF 不会重新分配任何已释放的内存,并且任何访问都将被检测到。

字节数为零的 malloc 调用被认为是错误的。如果您需要使用此类调用,您可以告诉 EF 通过将 EF_ALLOW_MALLOC_0 设置为非零值来忽略此错误。

我将 EF_ALIGNMENT 设置为 0,以查看是否会检测到 postr 错误,但 EF 仍然没有看到它。

预读

Checker 在 列表 2 中的正确行 (printf) 找到了问题,并指出了释放与 malloc 返回地址不同的地址。实际上,我递减了 foo 指针并尝试释放此地址。

Mem-Test 没有发现问题,但这在意料之中。

如果我链接 EF 库而不指定任何开关,Electric Fence 只会返回一个关于释放非 malloc 返回值的错误

Electric Fence 2.0.5 Copyright (C) 1987-1995 Bruce Perens.
ElectricFence Aborting: free(400b3ff3): address not from malloc().
Illegal Instruction (core dumped)

然后,我尝试设置 protect below 开关

export EF_PROTECT_BELOW=1
使用此变量,EF 引起了段错误。使用 gdb,我将程序跟踪到发生段错误的 printf 位置。
后写

对于 列表 3,Checker 在第 14 行 (strcpy) 发现了边界违规。此外,它还在第 16 行 (printf) 发现了未初始化的数据读取。实际上,print 将在分配区域之后读取。

Mem-Test 没有给出预期报告。

同样,Electric Fence 的第一次运行(没有任何开关)没有报告任何错误。然后我将 EF_ALIGNMENT 设置为 0,strcpy 导致核心转储。

预写

Checker 正确检测到 列表 4 中的错误是错误的 free。Mem-Test 没有给出报告。当我没有设置开关时,Electric Fence 仅检测到错误的 free,但设置 EF_PROTECT_BELOW 后,它也发现了预写。

未初始化的指针

Checker 在 列表 5 中找到了进行错误赋值的确切行。Mem-Test 没有预期或创建任何报告。发生了核心转储,但未创建日志。Electric Fence 没有检测到此错误。当您运行程序时,无论您是否使用 EF,都会得到核心转储。

未释放的内存

默认情况下,Checker 不会发现内存泄漏。文档显示了您可以设置的几个开关来修改检查。不同的开关通过定义环境变量 CHECKEROPTS 来设置。更有趣的选项是

  • -o=file:将输出重定向到文件。

  • -d=xx:禁用某种类型的内存访问错误。

  • -D=end:在程序结束时进行泄漏检测。

  • -m=x:定义 malloc(0) 的行为。

我运行了 export CHECKEROPTS="-D=end",然后重新编译。现在它在 列表 6 中发现了 50 字节的内存泄漏。Checker 实现了一个垃圾检测器来找出这种类型的错误。您可以通过设置此选项或通过在程序内部调用特定的 Checker 函数来调用它。

Mem-Test 轻松识别出内存泄漏,并提供清晰的报告

50 bytes of memory left allocated at 134524624
134524624 was last touched by licalloc at line 12 of unfree.c

Electric Fence 没有返回任何消息。

总结

从这些测试来看,Checker 显然是一个完整的产品,它毫无问题地发现了所有错误。它非常易于使用,并且您不必设置很多开关,因为默认情况下,它会检查各种错误。当您使用外部库和函数(例如 GDBM)时,它确实存在一点问题。实际上,为了确保它会检查所有内容,您应该使用它重新编译所有外部程序。如果您调用一个未使用它编译的函数,则用于跟踪内存访问的内存位图将不会更新;这将在您的检查中创建漏洞。您有两种方法可以做到这一点:使用 checkergcc 重新编译库,或创建函数桩。桩是每个函数的特定别名,它们对传递给函数和从函数返回的参数执行一些检查。特别是,您必须检查指针以查看您将通过使用它们访问的内存区域是否具有正确的状态(可读、可写等)。

软件包中提供了许多用于最流行函数(例如 stdio 和字符串函数)的即用型桩。在查看这些桩之后,为您无法使用 checkergcc 重新编译的库编写您自己的桩应该不难。

另一方面,Electric Fence 在错误检测方面表现出一些犹豫,但更容易使用。将其链接到程序并运行它就足够了(外部库没有问题)。如果与 Mem-Test 结合使用,它还将检测内存泄漏。为了获得最佳结果,请注意开关:使用正确的拼写和正确的对齐方式并保护(在内存分配下方或之后)。

资源

Memory Access Error Checkers
Cesare Pizzi 从 VIC-20 开始玩电脑。当不玩电子产品时,他会和女友 Barbara 一起光顾他家乡山区的酒馆。可以通过 cpizzi@bigfoot.com 联系到他。
加载 Disqus 评论