Oracle Solaris Studio 12.2 Discover 和 Uncover 用户指南

第 2 章 Sun 内存错误搜索工具 (Discover)

Sun 内存错误搜索工具 (Discover) 软件是用于检测内存访问错误的高级开发工具。

本章包括有关下列内容的信息:

Discover 的使用要求

必须正确准备二进制文件

Discover 使用通过 Sun Studio 12、Sun Studio 12 Update 1、Oracle Solaris Studio 12.2 编译器编译的二进制文件,或通过 GCC for Sun Systems 4.2.0 版或更高版本编译的二进制文件(在基于 SPARC 或 x86 的系统上,且运行的是 Solaris 10 5/08 操作系统或更高的 Solaris 10 Update 版本)。

当使用 Sun Studio 或 Oracle Solaris Studio 编译器时,您必须使用 -O 选项或 -xO[n] 选项以优化值进行编译。使用 GCC 编译器时,不需要特定的优化级别。

如果未满足这些要求,Discover 会出错,并且不校验二进制文件。但是,您可以使用 -l 选项(请参见校验选项)来校验未满足这些要求的二进制文件,并运行该二进制文件来检测有限数量的错误。

按照说明进行编译的二进制文件包括一些称为注释的信息,用于帮助 Discover 正确地校验二进制文件。添加这些少量信息不会影响二进制文件的性能或运行时内存使用情况。

通过在编译二进制文件时使用 -g 选项生成调试信息,Discover 可以在报告错误和警告的同时显示源代码和行号信息,并可以生成更准确的结果。如果在编译二进制文件时未使用 -g 选项,Discover 将仅显示相应计算机级别指令的程序计数器。另外,使用 -g 选项进行编译可帮助 Discover 生成更准确的报告(请参见解释 Discover 错误消息)。

不能使用使用预装或审计的二进制文件

由于 Discover 使用运行时链接程序的某些特殊功能,因此您不能将其用于使用预装或审计的二进制文件。

如果程序需要设置 LD_PRELOAD 环境变量,则可能无法与 Discover 正确配合,因为 Discover 需要插入某些系统函数,如果函数已预先装入,则无法执行此操作。

同理,如果程序使用了运行时审计(二进制文件链接到 -p 选项或 -P 选项,或者需要设置 LD_AUDIT 环境变量),则此审计将与 Discover 使用的审计相冲突。如果二进制文件链接到审计,Discover 将在校验时失败。如果在运行时设置了 LD_AUDIT 环境变量,结果将无法确定。

快速入门

下例说明了如何准备程序、使用 Discover 对其进行校验、运行程序,以及针对检测到的内存访问错误生成一个报告。此示例使用了一个访问未初始化数据的简单程序。


% cat test_UMR.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
  //  UMR:  accessing uninitialized data
  int *p = (int*) malloc(sizeof(int));
  printf("*p = %d\n", *p);
  free(p);
}

% cc -g -02 test_UMR.c
% a.out
*p = 131464
% discover -w - a.out
% a.out
ERROR (UMR): accessing uninitialized data from address 0x50010 (4 bytes) at:
        main() + 0x54  <test_UMR.c:7>
                 4:    {
                 5:      //  UMR:  accessing uninitialized data
                 6:      int *p = (int*) malloc(sizeof(int)); 
                 7:=>    printf(**p = %d\n", *p);
                 8:      free(p);
                 9:    }
        _start() + 0x108
    block at 0x50010 (4 bytes long) was allocated at:
        malloc() + 0x220
        main() + 0x1c  <test_UMR.c:6>
                3:    int main()
                4:    {
                5:      //  UMR:  accessing uninitialized data
                6:=>    int *p = (int*) malloc(sizeof(int)); 
                7:      printf(**p = %d\n", *p);
                8:      free(p);
                9:    }
        _start() + 0x108
*p = 327704
**************** Discover Memory Report *****************
No allocated memory left on program exit.
DISCOVER SUMMARY:
        unique errors   : 1 (1 total, 0 filtered)
        unique warnings : 0 (0 total, 0 filtered)
FILTERS: type=ML:func=_rt_boot:depth=*; type=*:func__f90_esfw:depth=*;
type=ML:func=__f90_ssfw:depth=*; 

Discover 输出显示了在何处使用了未初始化内存、将其分配至了何处,以及结果摘要。

校验准备好的二进制文件

准备好目标二进制文件后,下一步就是对其进行校验。校验功能会在至关重要的位置添加代码,以便在运行二进制文件时 Discover 能够跟踪内存操作。

可使用 discover 命令校验二进制文件。例如,以下命令将校验二进制文件 a.out,并使用校验过的 a.out 来覆写输入 a.out


discover a.out

当您运行校验过的二进制文件时,Discover 将监视该程序对内存的使用情况。在运行期间,Discover 会向 HTML 文件(在本例中,缺省情况下该文件为 a.out.html)中写入一份报告,其中详细列出了内存访问错误,可使用 Web 浏览器来查看该文件。校验二进制文件时,可以使用 - 选项,请求将报告写入 ASCII 文件或 stderr

当 Discover 校验二进制文件时,如果由于该二进制文件未注释而导致 Discover 发现无法校验的代码,Discover 将显示如下警告:


discover: (warning): a.out: 80% of code instrumented (16 out of 20 functions)

无注释代码可能来自链接到二进制文件中的汇编语言代码,或者来自使用早于必须正确准备二进制文件中所列版本的编译器或操作系统编译的模块。

缓存共享库

当 Discover 校验某个库时,将向该库中添加代码。当在运行时装入相关的共享库时,这些代码会配合运行时链接程序对这些共享库进行校验。校验过的库存储在高速缓存中;如果原始库自上次校验以来未发生更改,可以重新使用这些库。缺省情况下,高速缓存目录为 $HOME/SUNW_Bit_Cache。您可以使用 -D 选项来更改该目录。

校验共享库

如果整个程序(包括所有共享库)均已校验,Discover 将生成最准确的结果。缺省情况下,当您校验主可执行文件时,Discover 会插入代码,这样,当您运行该程序时,Discover 会在所有共享库打开时自动校验这些共享库是静态链接在程序中的,还是由 dlopen() 动态打开的。

应根据必须正确准备二进制文件中的说明准备程序使用的所有共享库。缺省情况下,如果运行时链接程序遇到一个未准备好的库,会发生致命错误。但是,您可以让 Discover 忽略一个或多个库。

忽略库

某些库可能无法准备,或者出于其他某种原因无法校验。要解决此问题,您可以使用 -s-T-N 选项(请参见校验选项)或 bit.rc 文件中的规范(请参见bit.rc 初始化文件)让 Discover 忽略这些库,但是,这样准确性会有所降低。

如果某个库无法校验,且无法标识为可忽略,Discover 将在校验时失败,或者程序将在运行时失败并出现错误消息。

缺省情况下,Discover 使用 bit.rc 系统文件中的规范将未准备好的系统库和编译器提供的库设置为可忽略。由于 Discover 了解最常用库的内存特征,因此,对准确性的影响微乎其微。

命令行选项

您可以将以下选项与 discover 命令结合使用来校验二进制文件。

输出选项

-o file

将校验过的二进制文件写入 file。缺省情况下,校验过的二进制文件会覆写输入的二进制文件。

-w text_file

将 Discover 的二进制文件报告写入 text_file。该文件是在您运行校验过的二进制文件时创建的。如果 text_file 是相对路径名,则该文件位于与您运行校验过的二进制文件时所在工作目录相对的位置。要使每次运行二进制文件时该文件名均具有唯一性,请在文件名中添加字符串 %p,要求 Discover 运行时包含进程 ID。例如,选项 -w report.%p.txt 将生成一个文件名为 report.process_id.txt 的报告文件。如果文件名中包含多处 %p,只有第一个实例会替换为进程 ID。

如果不指定此选项或 -H 选项,会以 HTML 格式将报告写入 output_file.html,其中 output_file 是校验过的二进制文件的基本名称。该文件位于您运行校验过的二进制文件时所在的工作目录中。

您可以同时指定此选项和 -H 选项,同时以文本和 HTML 格式写入报告。

-H html_file

以 HTML 格式将 Discover 的二进制文件报告写入 html_file。此文件是在您运行校验过的二进制文件时创建的。如果 html_file 是相对路径名,则该文件位于与您运行校验过的二进制文件时所在工作目录相对的位置。要使每次运行二进制文件时该文件名均具有唯一性,请在文件名中添加字符串 %p,要求 Discover 运行时包含进程 ID。例如,选项 -H report.%p.html 会生成一个文件名为 report.process_id.html 的报告文件。如果文件名中包含多处 %p,只有第一个实例会替换为进程 ID。

如果不指定此选项或 -w 选项,会以 HTML 格式将报告写入 output_file.html,其中 output_file 是校验过的二进制文件的基本名称。该文件位于您运行校验过的二进制文件时所在的工作目录中。

您可以同时指定此选项和 -w 选项,同时以文本和 HTML 格式写入报告。

-e n

仅在报告中显示 n 个内存错误(缺省情况下显示所有错误)。

-E n

仅在报告中显示 n 个内存泄漏(缺省情况下显示 100 个)。

-f

在报告中显示偏移(缺省情况下隐藏偏移)。

-m

在报告中显示重整名称(缺省情况下显示取消重整名称)。

-S n

仅在报告中显示 n 个栈帧(缺省情况下显示 8 个)。

校验选项

-l

在轻量模式下运行 Discover。使用此选项可以更快地执行程序,并且无需特意根据必须正确准备二进制文件中的说明准备程序,但只能检测到有限数量的错误。

-i

使用线程分析器校验数据争用检测。如果使用此选项,仅在运行时执行数据争用检测,而不执行其他任何内存检查。必须使用 collect 命令运行校验过的二进制文件,以生成可以在性能分析器中查看的实验(请参见《Oracle Solaris Studio 12.2:线程分析器用户指南》)。

-s

尝试校验一个无法校验的二进制文件时发出警告,但不标出错误。

-T

仅校验命名的二进制文件。在运行时不校验任何相关的共享库。

-N library

不校验与前缀 library 匹配的任何相关共享库。如果库名称的前几个字符与 library 匹配,则忽略该库。如果 library 以 / 开头,则根据库的绝对完整路径名进行匹配。否则,根据库的基本名称进行匹配。

-K

不读取 bit.rc 初始化文件(请参见bit.rc 初始化文件)。

缓存选项

-D cache_directory

cache_directory 用作存储缓存的校验过的二进制文件的根目录。缺省情况下,高速缓存目录为 $HOME/SUNW_Bit_Cache

-k

强制重新校验高速缓存中找到的所有库。

其他选项

-h-?

帮助。输出简短的用法消息并退出。

-v

详细。输出 Discover 正在执行的操作的日志。重复使用该选项可获得更多信息。

-V

输出 Discover 版本信息并退出。

bit.rc 初始化文件

Discover 在启动时,会通过读取一系列 bit.rc 文件来初始化自身的状态。系统文件 Oracle_Solaris_Studio_installation_directory /prod/lib/postopt/bit.rc 为某些变量提供了缺省值。Discover 先读取此文件,然后依次读取 $HOME/.bit.rc(如果存在)和 current_directory/.bit.rc(如果存在)。

bit.rc 文件包含用于设置某些变量以及在某些变量中进行附加和删除的命令。当 Discover 读取 set 命令时,会放弃变量的前一个值(如果有)。当读取 append 命令时,会将参数附加到变量的现有值(该参数置于冒号分隔符后面)。当读取 remove 命令时,将从变量的现有值中删除参数及其冒号分隔符。

bit.rc 文件中设置的变量包括校验时要忽略的库列表,以及计算库中无注释(未准备)代码百分比时要忽略的函数或函数前缀的列表。

有关更多信息,请参阅 bit.rc 系统文件头中的注释。

SUNW_DISCOVER_OPTIONS 环境变量

可以通过将 SUNW_DISCOVER_OPTIONS 环境变量设置为命令行选项 -b-e-E-f-H-l-L-m-S-w 的列表,以更改校验过的二进制文件的运行时行为。例如,如果要将报告的错误数更改为 50 并将报告中的栈深限制为 3,可以将此环境变量设置为 -e 50 -s 3

运行校验过的二进制文件

使用 Discover 校验二进制文件后,可以如常运行二进制文件。通常,如果特定的输入组合导致程序行为异常,您可以使用 Discover 对其进行校验,然后使用相同的输入运行程序,以调查潜在的内存问题。校验过的程序在运行时,Discover 以选定的格式(文本和/或 HTML)会将发现的与任何内存问题相关的信息写入到指定的输出文件。有关解释报告的信息,请参见分析 Discover 报告

由于校验会产生系统开销,因此,程序经过校验后运行速度会显著减慢。根据内存访问的频率,运行速度最多可能会慢 50 倍。

分析 Discover 报告

Discover 报告提供的信息可帮助您有效地定位并修复源代码中的问题。

缺省情况下,该报告以 HTML 格式写入到 output_file.html,其中 output_file 是校验过的二进制文件的基本名称。该文件位于您运行校验过的二进制文件时所在的工作目录中。

校验二进制文件时,可以使用 -H 选项请求将 HTML 输出写入指定的文件,或使用 -w 选项请求将其写入文本文件(请参见命令行选项)。

校验二进制文件后,可以更改SUNW_DISCOVER_OPTIONS 环境变量中报告的 -H-w 的设置。例如,如果希望在以后运行程序时将报告写入不同的文件,可以执行此操作。

分析 HTML 报告

使用 HTML 报告格式可以对程序进行交互分析。开发人员可以通过使用电子邮件或者通过发布到 Web 页上来轻松共享 HTML 格式的数据。HTML 报告与 JavaScript 交互功能相结合,使得用户可以轻松浏览 Discover 消息。

"Errors"(错误)选项卡(请参见使用 "Errors"(错误)选项卡)、"Warnings"(警告)选项卡(请参见使用 "Warnings"(警告)选项卡)和 "Memory Leaks"(内存泄漏)选项卡(请参见使用 "Memory Leaks"(内存泄漏)选项卡)分别用于浏览错误消息、警告消息和内存泄漏报告。

使用左侧的控制面板(请参见使用控制面板)可以更改当前显示在右侧的选项卡内容。

使用 "Errors"(错误)选项卡

在浏览器中首次打开 HTML 报告时,"Errors"(错误)选项卡处于选中状态,并显示执行校验过的二进制文件期间发生的内存访问错误列表。

Discover HTML 报告的 "Errors"(错误)选项卡

单击某个错误时,将显示发生该错误时的栈跟踪:

Discover HTML 报告的 "Errors"(错误)选项卡(包含栈跟踪)

如果代码是使用 -g 选项编译的,可以通过单击栈跟踪中的每个函数来查看相应函数的源代码:

Discover HTML 报告的 "Errors"(错误)选项卡(包含源代码)

使用 "Warnings"(警告)选项卡

"Warnings"(警告)选项卡显示了有关可能的访问错误的所有警告消息。单击某个警告时,将显示发生该警告时的栈跟踪。如果代码是使用 -g 选项编译的,可以通过单击栈跟踪中的每个函数来查看相应函数的源代码。

Discover HTML 报告的 "Warnings"(警告)选项卡(包括栈跟踪和源代码)

使用 "Memory Leaks"(内存泄漏)选项卡

"Memory Leaks"(内存泄漏)选项卡的顶部显示程序运行结束时仍保持已分配状态的总块数,并在下方列出了相应的块。

Discover HTML 报告的 "Memory Leaks"(内存泄漏)选项卡

单击某个块时,将显示该块的栈跟踪。如果代码是使用 -g 选项编译的,可以通过单击栈跟踪中的每个函数来查看相应函数的源代码。

Discover HTML 报告的 "Memory Leaks"(内存泄漏)选项卡(包括栈跟踪和源代码)

使用控制面板

要查看所有错误、警告和内存泄漏的栈跟踪,请在控制面板的 "Stack Traces"(栈跟踪)部分中单击 "Expand All"(全部展开)。要查看所有函数的源代码,请在控制面板的 "Source Code"(源代码)部分中单击 "Expand All"(全部展开)。

要隐藏所有错误、警告和内存泄漏的栈跟踪或源代码,请单击相应的 "Collapse All"(全部折叠)。

选择 "Errors"(错误)选项卡后,将显示控制面板的 "Show Errors"(显示错误)部分,可以用来控制要显示的错误类型。缺省情况下,所有检测到的错误的复选框均处于选中状态。要隐藏某种错误类型,请单击对应的复选框以移除复选标记。

选择 "Warning"(警告)选项卡后,将显示控制面板的 "Show Warnings"(显示警告)部分,可以用来控制要显示的警告类型。缺省情况下,所有检测到的警告的复选框均处于选中状态。要隐藏某种警告类型,请单击对应的复选框以移除复选标记。

控制面板的底部显示了报告摘要,其中列出了错误和警告总数,以及泄漏的内存量。

分析 ASCII 报告

按脚本进行处理时,或者无权访问 Web 浏览器时,适合使用 ASCII(文本)格式的 Discover 报告。下面是 ASCII 报告的一个示例。


$ a.out

ERROR 1 (UAW): writing to unallocated memory at address 0x50088 (4 bytes) at:
   main() + 0x2a0  <ui.c:20>
       17:     t = malloc(32);
       18:     printf("hello\n");
       19:     for (int i=0; i<100;i++)
       20:=>    t[32] = 234; // UAW
       21:     printf("%d\n", t[2]);   //UMR
       22:     foo();
       23:     bar();
   _start() + 0x108
ERROR 2 (UMR): accessing uninitialized data from address 0x50010 (4 bytes) at:
   main() + 0x16c  <ui.c:21>$
       18:     printf("hello\n");
       19:     for (int i=0; i<100;i++)
       20:      t[32] = 234; // UAW
       21:=>   printf("%d\n", t[2]);   //UMR
       22:     foo();
       23:     bar();
       24:    }
   _start() + 0x108
       was allocated at (32 bytes):
   main() + 0x24  <ui.c:17>
       14:     x = (int*)malloc(size); // AZS warning
       15:    }
       16:    int main() {
       17:=>   t = malloc(32);
       18:     printf("hello\n");
       19:     for (int i=0; i<100;i++)
       20:      t[32] = 234; // UAW
   _start() + 0x108
0
WARNING 1 (AZS): allocating zero size memory block at:
   foo() + 0xf4  <ui.c:14>
       11:    void foo() {
       12:     x = malloc(128);
       13:     free(x);
       14:=>   x = (int*)malloc(size); // AZS warning
       15:    }
       16:    int main() {
       17:     t = malloc(32);
   main() + 0x18c  <ui.c:22>
       19:     for (int i=0; i<100;i++)
       20:      t[32] = 234; // UAW
       21:     printf("%d\n", t[2]);   //UMR
       22:=>   foo();
       23:     bar();
       24:    }
   _start() + 0x108

***************** Discover Memory Report *****************

1 block at 1 location left allocated on heap with a total size of 128 bytes

   1 block with total size of 128 bytes
   bar() + 0x24  <ui.c:9>
       6:           7:    void bar() {
        8:     int *y;
        9:=>   y = malloc(128);  // Memory leak
       10:    }
       11:    void foo() {
       12:     x = malloc(128);
   main() + 0x194  <ui.c:23>
       20:      t[32] = 234; // UAW
       21:     printf("%d\n", t[2]);   //UMR
       22:     foo();
       23:=>   bar();
       24:    }
   _start() + 0x108

ERROR 1: repeats 100 times
DISCOVER SUMMARY:
   unique errors   : 2 (101 total, 0 filtered)
   unique warnings : 1 (1 total, 0 filtered)

该报告包括错误和警告消息,其后是摘要。

错误消息以 ERROR 一词开头,并包含一个三字母代码、一个 ID 编号以及错误描述(本例中为 writing to unallocated memory)。其他详细信息包括访问的内存地址,以及读取或写入的字节数。在描述的后面,是发生错误时的栈跟踪,可以根据栈跟踪在进程生命周期中定位错误的位置。

如果程序是使用 -g 选项编译的,栈跟踪将包括源文件名称和行号。如果源文件可访问,则输出错误位置附近的源代码。每一帧中的目标源代码行均以符号 => 表示。

如果同一内存位置重复出现字节数相同的同一错误类型,完整的消息(包括栈跟踪)仅输出一次。对于多次出现的每个相同错误,会统计后续的错误,并在报告的末尾列出重复计数(如下例中所示)。


ERROR 1: repeats 100 times

如果出错内存访问的地址位于堆上,则在栈跟踪的后面输出相应堆块的信息。这些信息包括块的起始地址和大小,以及分配该块时的栈跟踪。如果该块已释放,将还包括解除分配点的栈跟踪。

警告消息的输出格式与错误消息相同,只不过是以 WARNING 一词开头。一般来说,这些消息只是提醒您注意那些不会影响应用程序正确性的情况,但提供的有用信息可帮助您改进程序。例如,分配大小为零的内存不会造成损害,但如果经常这样做可能会降低性能。

内存泄漏报告包含有关在堆上分配的、但程序退出时未释放的内存块的信息。下面是内存泄漏报告的一个示例。


$ DISCOVER_MEMORY_LEAKS=1 ./a.out
...
***************** Discover Memory Report *****************

2 blocks left allocated on heap with total size of 44 bytes
    block at 0x50008 (40 bytes long) was allocated at:
        malloc() + 0x168 [libdiscover.so:0xea54]
        f() + 0x1c [a.out:0x3001c]
          <discover_example.c:9>:
                 8:    {
                 9:=>    int *a = (int *)malloc( n * sizeof(int) );
                10:      int i, j, k;
        main() + 0x1c [a.out:0x304a8]
          <discover_example.c:33>:
                32:      /* Print first N=10 Fibonacci numbers */
                33:=>    a = f(N);
                34:      printf("First %d Fibonacci numbers:\n", N);
        _start() + 0x5c [a.out:0x105a8]
...

标题下面的第一行汇总了在堆上保持已分配状态的堆块数及其总大小。报告的大小是从开发人员的立场提供的,也就是说,不包括内存分配器的簿记系统开销。

内存泄漏摘要的后面输出了每个未释放堆块及其分配点栈跟踪的详细信息。该栈跟踪报告类似于前面介绍错误和警告消息时提到的栈跟踪报告。

Discover 报告的结尾是总摘要。该摘要报告了唯一警告和错误的数目,并在括号中提供了错误和警告的总数(包括重复项)。例如:


DISCOVER SUMMARY:
        unique errors   : 3 (3 total)
        unique warnings : 1 (5 total)

内存访问错误和警告

Discover 会检测和报告许多内存访问错误,并就可能是错误的访问向您发出警告。

内存访问错误

Discover 可检测到以下内存访问错误:

以下几部分列出了一些简单的示例程序,这些程序会生成上述某些错误。

ABR


#include <stdio.h>
#include <stdlib.h>
int main()
{
  //  ABR: reading memory beyond array bounds at address 0x%1x (%d byte%s)"
  int *a = (int*) malloc(sizeof(int[5]));
  printf("a[5] = %d\n",a[5]);
)

ABW


#include <stdlib.h>
int main()
{
  //  ABW:  writing to memory beyond array bounds
  int *a = (int*) malloc(sizeof(int[5]));
  a[5] = 5;
}

BFM


#include <stdlib.h>
int main()
{

  //  BFM:  freeing wrong memory block
  int *p = (int*) malloc(sizeof(int));
  free(p+1);
}

BRP


#include <stdlib.h>
int main()
{
  //  BRP is "bad address parameter for realloc 0x%1x"
  int *p = (int*) realloc(0,sizeof(int));
  int *q = (int*) realloc(p+20,sizeof(int[2]));
}

DFM


#include <stdlib.h>
int main()
{
  // DFM is "double freeing memory"
  int *p = (int*) malloc(sizeof(int));
  free(p);
  free(p);'
}

FMR


#includ <stdio.h>
#include <stdlib.h>
int main()
{
  //  FMR is "reading from freed memory at address 0x%1x  (%d byte%s)"
  int *p = (int*) malloc(sizeof(int));
  free(p);
  printf("p = 0x%h\n",p);
}

FMW


#include <stdlib.h>
int main()
{
  //  FMW is "writing to freed memory at address 0x%1x (%d byte%s)"
  int *p = (int*) malloc(sizeof(int));
  free(p);
  *p = 1;
}

FRP


#include <stdlib.h>
int main()
{
  //  FRP:  freed pointer passed to realloc
  int *p = (int*) malloc(sizeof(int));
  free(0);
  int *q = (int*) realloc(p,sizeof(int[2]));
}

IMR


#include <stdlib.h>
int main()
{
  //  IMR:  read from invalid memory address
  int *p = 0;
  int i = *p;   // generates Signal 11...
}

IMW


#include <stdlib.h>
int main()
{
  //  IMW:  write to invalide memory address
  int *p = 0;
  *p = 1;       // generates Signal 11...
}

PIR


#include <stdio.h>
#include <stdlib.h>
int main()
{
  //  PIR:  accessing partially initialized data
  int *p = (int*) malloc(sizeof(int));
  *((char*)p) = 'c';
  printf("*(p = %d\n",*(p+1));
}  

UAR


#include <stdio.h>
#include <stdlib>
int main()
{
  //  UAR is "reading from unallocated memory"
  int *p = (int*) malloc(sizeof(int));
  printf("*(p+1) = %d\n",*(p+1));
}

UAW


#include <stdio.h>
#include <stdlib.h>
int main()
{
  // UAW is "writing to unallocated memory"
  int *p = (int*) malloc(sizeof(int));
  *(p+1) = 1;
}

UMR


#include <stdio.h>
#include <stdlib.h>
int main()
{
  // UMR is "accessing uninitialized data from address 0x%1x (A%d byte%s)"
  int *p = (int*) malloc(sizeof(int));
  printf("*p = %d\n",*p);
} 

内存访问警告

Discover 会报告下列内存访问警告:

下面列出了一个会生成 AZS 警告的简单示例程序。

AZS


#include <stdlib.h>
int main()
{
  //  AZS:  allocating zero size memory block
  int *p = malloc();
}

解释 Discover 错误消息

在某些情况下,Discover 可能会报告一个实际上不算错误的错误。这种情况称为“误报”。与同类工具相比,Discover 在校验时会分析代码以减少误报的次数,不过难免也会出现误报。以下几部分提供了一些提示,可能对识别 Discover 报告中的误报并尽可能地避免出现误报有所帮助。

部分初始化内存

以 C/C++ 语言编写的位字段可让您创建紧凑的数据类型。例如:


struct my_struct {
  unsigned int valid : 1;
  char         c;
};

在本例中,结构成员 my_struct.valid 在内存中仅占用一位。但是,在 SPARC 平台上,CPU 只能以字节为单位修改内存,因此,只有装入含有 struct.valid 的整个字节才能访问或修改该结构成员。此外,编译器有时会认为一次装入多个字节(例如,由四个字节构成的计算机字)会更高效。当 Discover 检测到此类装入操作时,如果没有更多信息,会假定使用了所有四个字节。例如,字段 my_struct.valid 已初始化,但字段 my_struct.c 未初始化,如果装入了包含这两个字段的计算机字,则 Discover 会标记部分初始化内存读取 (PIR) 错误。

误报的另一个原因是位字段初始化。要写入某个字节的一部分,编译器必须首先生成用于装入该字节的代码。如果该字节不是在读取之前写入的,将生成未初始化内存读取 (UMR) 错误。

要避免位字段误报,请在编译时使用 -g 选项或 -g0 选项。这些选项为 Discover 提供了额外的调试信息,可帮助 Discover 识别位字段装入和初始化,从而可以排除大多数误报。如果出于某种原因无法使用 -g 选项编译,请使用 memset() 等函数初始化结构。例如:


...
struct my_struct s;
/* Initialize structure prio to use */
memset(&sm 0, sizeof(struct my_struct));
...

可疑装入

有时,当装入的结果对某些程序路径无效时,编译器会从已知的内存地址生成装入。这种情况经常发生在 SPARC 平台上,因为此类装入指令可以放置在分支指令的延迟槽中。下面提供了一个示例代码片段:


int i'
if (foo(&i) != 0) { /* foo returns nonzero if it has initialized i */
  printf("5d\n", i);
}

根据此代码,编译器可能会生成如下代码:


int i;
int t1, t2'
t1 = foo(&i);
t2 = i; /* value in i is loaded */
if (t1 != 0) {
  printf("%d\n", t2);
}

假定本例中的函数 foo() 返回了 0 且未初始化 i。仍会从 i 生成装入,尽管它并未使用。但是,Discover 将会发现该装入,并报告装入了未初始化变量 (UMR)。

Discover 尽量使用数据流分析来识别此类情况,但有时无法检测到此类情况。

使用较低的优化级别进行编译可以减少这些类型的误报的发生。

未校验的代码

有时,Discover 无法完整地校验您的程序。某些代码可能来自无法重新编译的汇编语言源文件或第三方库,因此无法校验。Discover 并不知道未校验代码正在访问和修改的内存块。例如,假定某个第三方共享库中的某个函数初始化了一个内存块,主程序(校验过的程序)稍后读取了该内存块。由于 Discover 并不知道该库已初始化内存,后续读取操作将生成未初始化内存错误 (UMR)。

为解决此类问题,Discover API 中包含了下列函数:


void __ped_memory_write(unsigned long addr, long size, unsigned long pc);
void __ped_memory_read(unsigned long addr, long size, unsigned long pc);
void __ped_memory_copy(unsigned long src, unsigned lond dst, long size, unsigned long pc);

您可以从程序调用这些 API 函数,将具体事件告知 Discover,例如,向内存区写入 (__ped_memory_write()) 或从内存区读取 (__ped_memory read())。对于这两种情况,内存区的起始地址将通过 addr 参数传递,内存区的大小通过 size 参数传递。将 pc 参数设置为 0

使用 __ped_memory_copy 函数告知 Discover 正在将内存从一个位置复制到另一个位置。源内存的起始地址将通过 src 参数传递,目标区的起始地址通过 dst 参数传递,大小通过 size 参数传递。将 pc 参数设置为 0

要使用 API,请在程序中将这些函数声明为弱函数。例如,在源代码中包含以下代码片段。


#ifdef __cplusplus
extern "C" {
#endif

extern void __ped_memory_write(unsigned long addr, long size, unsigned long pc);
extern void __ped_memory_read(unsigned long addr, long size, unsigned long pc);
extern void __ped_memory_copy(unsigned long src, unsigned long dst, long size, unsigned long pc);

#prgama weak __ped_memory_write
#pragma weak __ped_memory_read
#pragma weak __ped_memory_copy

#ifdef __cplusplus
}
#endif

API 函数是在内部 Discover 库中定义的,该库在校验时链接到程序。但是,如果未校验程序,便不会链接该库,这样,对 API 函数的所有调用都会导致应用程序挂起。因此,如果不是在 Discover 下运行程序,则必须禁用这些函数。另外,您也可以使用 API 函数的空定义创建一个动态库,并将其链接到程序。在此情况下,如果您未在 Discover 下运行程序,将使用您的库,但如果在 Discover 下运行程序,将自动调用实际的 API 函数。

使用 Discover 时的限制

仅校验有注释的代码

Discover 只能校验按照必须正确准备二进制文件中的说明准备的代码。无注释代码可能来自链接到二进制文件中的汇编语言代码,或者来自使用早于该部分中所列版本的编译器或操作系统编译的模块。

在准备时,特别要排除包含 asm 语句或 .il 模板的汇编语言模块和函数。

计算机指令可能不同于源代码

Discover 处理计算机代码。该工具可检测到装入和存储等计算机指令中的错误,并将这些错误与源代码相关联。某些源代码语句没有关联的计算机指令,因此,看上去 Discover 没有检测到明显的用户错误。例如,请看以下 C 语言代码片段:

  int *p = (int *)malloc(sizeof(int));
  int i;

  i = *p;  /* compiler may not generate code for this statement */
  printf("Hello World!\n");

  return;

由于内存未初始化,读取 p 所指向的地址处存储的值就是一个潜在的用户错误。但是,优化编译器可以检测到未使用变量 i,因此,不会生成让语句读取内存并将其分配到 i 的代码。在此情况下,Discover 不会报告使用了未初始化内存 (UMR)。

编译器选项影响生成的代码

编译器生成的代码不一定始终如您所愿。由于编译器生成的代码因所使用的编译器选项(包括 -On 优化选项)的不同而异,因此,Discover 报告的错误也可能有所不同。例如,在 -O1 优化级别上生成的代码中报告的错误可能不会出现于在 -O4 优化级别上生成的代码中。

系统库可能会影响报告的错误

系统库是随操作系统一起预装的,无法重新编译以进行校验。Discover 为标准 C 库 (libc.so) 中的公用函数提供支持;也就是说,Discover 知道这些函数访问或修改的内存。但是,如果应用程序使用了其他系统库,可能会在 Discover 报告中出现误报。如果出现误报,可以从代码调用 Discover API 来消除误报。

自定义内存管理可能会影响数据的准确性

Discover 可以跟踪标准编程语言机制(例如 malloc()calloc()free()operator new()operator delete())分配的堆内存。

如果应用程序使用在标准函数顶层工作的自定义内存管理系统(例如,使用 malloc() 实现的池分配管理),则 Discover 仍会运行,但不能保证正确报告泄漏或对已释放内存的访问。

Discover 不支持下列内存分配器:

不支持 sigaltstack(2)() 函数。

无法检测到静态和自动数组的超出边界错误

由于检测数组边界所用的算法问题,Discover 无法检测到静态和自动(本地)数组的超出边界访问错误。只能检测动态分配数组的错误。