Oracle Solaris Studio 12.2:使用 dbx 调试程序

第 9 章 使用运行时检查

利用运行时检查 (Runtime Checking, RTC) 可在开发阶段在本机代码应用程序中自动检测运行时错误(如内存访问错误和内存泄漏)。还可利用它监控内存使用情况。但不能对 Java 代码使用运行时检查。

本章阐述下列主题:

运行时检查功能

由于运行时检查是一种综合的调试功能,因此可在使用运行时检查功能时(使用收集器收集性能数据的情况除外)执行所有调试操作。

运行时检查:

如果编译时使用 -g 标志,则在运行时检查错误消息中提供源代码行号关联。运行时检查还可以检查使用优化 -O 标志编译的程序。对于未使用 -g 选项编译的程序,有一些特殊注意事项。

可以通过 check 命令使用运行时检查功能。

何时使用运行时检查

一种避免同时出现大量错误的方法是在开发周期中尽早(在开发程序的各个组成模块阶段)使用运行时检查。先编写一个单元测试来驱动每个模块,然后使用运行时检查以递增方式逐个检查模块。这样每次需要处理的错误数就会较少。将所有模块集成为完整的程序时,遇到的新错误可能会很少。将错误数减少为零后,就只有在对模块进行了更改时,才需要再次使用运行时检查。

运行时检查要求

要使用运行时检查必须满足下列要求:

有关运行时检查限制的信息,请参见运行时检查限制

使用运行时检查

要使用运行时检查,请在运行程序前启用要使用的检查类型。

启用内存使用和内存泄漏检查

要启用内存使用和内存泄漏检查,请键入:


(dbx) check -memuse

启用了内存使用检查或内存泄漏检查时,showblock 命令将显示有关指定地址处堆块的详细信息。这些详细信息包括块分配的位置及其大小。有关更多信息,请参见showblock 命令

启用内存访问检查

要仅启用内存访问检查,请键入:


(dbx) check -access

启用所有运行时检查

要启用内存泄漏、内存使用和内存访问检查,请键入:


(dbx) check -all

有关更多信息,请参见check 命令

关闭运行时检查

要彻底关闭运行时检查,请键入:


(dbx) uncheck -all

有关详细信息,请参见uncheck 命令

运行程序

在启用了所需类型的运行时检查之后,可运行所测试的程序(是否使用断点均可)。

程序正常运行,但速度很慢,因为每次进行内存访问前都要检查其有效性。如果 dbx 检测到无效访问,便会显示错误的类型和位置。控制权将交还给您(除非 dbx 环境变量 rtc_auto_continue 设置为 on,请参见设置 dbx 环境变量。)

然后便可发出 dbx 命令,例如执行 where 获取当前栈跟踪,或执行 print 检查变量。如果命令不是致命错误,可以使用 cont 命令继续执行程序。程序继续执行,直至遇到下一个错误或断点(无论先检测到哪一个)。有关详细信息,请参见cont 命令

如果 rtc_auto_continue 环境变量设置为 on,则运行时检查会继续查找错误,并自动保持运行。它会将错误重定向到通过 dbx 环境变量 rtc_error_log_file_name 指定的文件。(请参见设置 dbx 环境变量。)缺省的日志文件名为 /tmp/dbx.errlog.uniqueid

可以使用 suppress 命令限制报告运行时检查错误。有关详细信息,请参见suppress 命令

下面的简单示例说明了如何对名为 hello.c 的程序启用内存访问和内存使用检查。


% cat -n hello.c
     1 #include <stdio.h>
     2 #include <stdlib.h>
     3 #include <string.h>
     4
     5 char *hello1, *hello2;
     6
     7 void
     8 memory_use()
     9 {
    10      hello1 = (char *)malloc(32);
    11      strcpy(hello1, "hello world");
    12      hello2 = (char *)malloc(strlen(hello1)+1);
    13      strcpy(hello2, hello1);
    14 }
    15
    16 void
    17 memory_leak()
    18 {
    19      char *local;
    20      local = (char *)malloc(32);
    21      strcpy(local, "hello world");
    22 }
    23
    24 void
    25 access_error()
    26 {
    27      int i,j;
    28
    29      i = j;
    30 }
    31
    32 int
    33 main()
    34 {
    35      memory_use();
    36      access_error();
    37      memory_leak();
    38      printf("%s\n", hello2);
    39      return 0;
    40 }
% cc -g -o hello hello.c

% dbx -C hello
Reading ld.so.1
Reading librtc.so
Reading libc.so.1
Reading libdl.so.1

(dbx) check -access
access checking - ON
(dbx) check -memuse
memuse checking - ON
(dbx) run Running: hello
(process id 18306)
Enabling Error Checking... done
Read from uninitialized (rui):
Attempting to read 4 bytes at address 0xeffff068
     which is 96 bytes above the current stack pointer
Variable is ’j’
Current function is access_error
    29       i = j;
(dbx) cont
hello world
Checking for memory leaks...
Actual leaks report    (actual leaks:         1 total size:      32 bytes)

 Total      Num of  Leaked     Allocation call stack
 Size       Blocks  Block
                    Address
==========  ====== ==========  =======================================
        32       1    0x21aa8  memory_leak < main

Possible leaks report  (possible leaks:       0  total size:      0 bytes)

Checking for memory use...
Blocks in use report   (blocks in use:        2  total size:      44 bytes

 Total     % of Num of  Avg    Allocation call stack
 Size      All  Blocks  Size
========== ==== ====== ======  =======================================
        32  72%      1     32  memory_use < main
        12  27%      1     12  memory_use < main

execution completed, exit code is 0

函数 access_error() 在变量 j 被初始化前便读取它。运行时检查将此访问错误报告为从尚未初始化项读取 (rui)。

函数 memory_leak() 在其返回前没有释放变量 localmemory_leak() 返回时,此变量超出作用域,在第 20 行分配的块变为泄漏。

程序使用全局变量 hello1hello2,这两个变量始终处于作用域内。它们都指向动态分配的内存,这种情况报告为使用的块 (biu)。

使用访问检查

访问检查通过监视每个读取、写入、分配和释放操作来检查程序是否正确访问内存。

程序可能会以各种方式错误读取或写入内存,这些都称为内存访问错误。例如,程序可能会引用已通过 free() 调用针对堆块释放的内存块。另外,函数可能会将指针返回给局部变量,这样,访问该指针时,便会出现错误。访问错误可能会导致程序中指针混乱,并可导致程序行为异常,包括输出错误和段违规。某些类型的内存访问错误很难跟踪。

运行时检查保存有一个跟踪程序使用的每个内存块状态的表。运行时检查会将每个内存操作与其涉及的内存块的状态进行对照,然后确定相应操作是否有效。可能的内存状态包括:

使用运行时检查来查找内存访问错误与使用编译器查找程序中的语法错误没有什么不同。在这两种情况下,都会生成错误列表,并提供与每个错误对应的错误消息,说明出错原因和程序中的出错位置。在这两种情况下,应该从错误列表的顶部开始依次向下修复程序中的错误。在链锁反应下,一个错误可导致其他错误发生。因此,链中的第一个错误是“首要原因”,修复该错误后,便可能会修复一些后续的错误。

例如,从未初始化的内存区进行读取会创建不正确的指针,这样,取消其引用时,便会导致出现其他无效的读取或写入,而这又会导致出现另一个错误。

理解内存访问错误报告

运行时检查会输出以下有关内存访问错误的信息:

错误  

信息  

type

错误类型。 

access

尝试访问的类型(读取或写入)。 

size

尝试访问的大小。 

address

尝试访问的地址。 

size

泄漏块的大小。 

detail

有关地址的更多详细信息。例如,如果地址在邻近栈中,便会提供其相对当前栈指针的位置。如果地址在堆中,便会提供最近堆块的地址、大小和相对位置。 

stack

出错时调用栈(批处理模式)。 

allocation

如果地址在堆中,便会提供最近堆块的分配跟踪。 

location

出错位置。如果有行号信息,则此信息包括行号和函数。如果无行号,运行时检查会提供函数和地址。 

以下示例显示的是一个典型的访问错误。


Read from uninitialized (rui):
Attempting to read 4 bytes at address 0xefffee50
    which is 96 bytes above the current stack pointer
Variable is ”j’
Current function is rui
   12           i = j;

内存访问错误

运行时检查会检测下列内存访问错误:

使用内存泄漏检查

内存泄漏是动态分配的内存块,在程序的数据空间中任何位置都没有指向它的指针。这类块是孤立内存。由于没有指向这些块的指针,程序无法引用它们,更谈不上释放它们。运行时检查会查找并报告这类块。

内存泄漏会导致占用的虚拟内存增加,且通常会导致产生内存碎片。这可能会降低程序及整个系统的性能。

通常情况下,导致出现内存泄漏的原因是未释放分配的内存,而又丢失了指向分配块的指针。下面是一些内存泄漏示例:


void
foo()
{
    char *s;
    s = (char *) malloc(32);

    strcpy(s, "hello world");

    return; /* no free of s. Once foo returns, there is no     */
            /* pointer pointing to the malloc’ed block,         */
            /* so that block is leaked.                         */
}

API 使用不当会导致泄漏。


void
printcwd()
{

    printf("cwd = %s\n", getcwd(NULL, MAXPATHLEN));

    return; /* libc function getcwd() returns a pointer to     */
            /* malloc’ed area when the first argument is NULL, */
            /* program should remember to free this. In this   */
            /* case the block is not freed and results in leak.*/
}

总是在不再需要内存时便将其释放,并密切注意返回已分配内存的库函数,便可避免内存泄漏。如果使用这类函数,记得要适当地释放内存。

有时,内存泄漏一词用于指未释放的内存块。内存泄漏的这一定义很少用到,因为常见的编程惯例是,如果程序不久便会终止,就不释放内存。如果程序仍然有一个或多个指向内存块的指针,运行时检查不会将内存块按泄漏来报告。

检测内存泄漏错误

运行时检查检测下列内存泄漏错误:

可能的泄漏

在两种情况下,运行时检查会报告“可能的”泄漏。第一种情况是没有指针指向块开始处,但有指针指向块内部时。这种情况按“地址位于块内 (aib)”错误来报告。如果指针是指向块内部的迷失指针,便是真正的内存泄漏。但是,某些程序会根据需要有意反复移动指向数组的唯一指针来访问其条目。这种情况便不是内存泄漏。由于运行时检查无法区分这两种情况,因此会将这两种情况都按可能的泄漏来报告,由您来确定哪一种情况是真正的内存泄漏。

数据空间中没有指向内存块的指针,但寄存器中有指针时,便会出现第二种类型的可能泄漏。这种情况按“地址位于寄存器内 (air)”错误来报告。如果寄存器意外指向内存块或寄存器是后来丢失了的内存指针的旧副本,便是真正的泄漏。不过,编译器可以优化引用以及将指向内存块的唯一指针放入寄存器中,而不必将指针写入内存。这类情况便不是真正的泄漏。因此,如果程序经过优化,且报告是执行 showleaks 命令的结果,很可能不是真正的泄漏。所有其他情况便可能是真正的泄漏。有关更多信息,请参见showleaks 命令


注 –

运行时泄漏检查要求使用标准 libc malloc/free/realloc 函数或基于这些函数的分配器。有关其他分配器,请参见运行时检查应用编程接口。.


检查泄漏

如果启用了内存泄漏检查,则会在所测试的程序即将退出之前,自动执行内存泄漏扫描。检测到的所有泄漏都会报告出来。不应使用 kill 命令中止程序。下面是一个典型的内存泄漏错误消息:


Memory leak (mel):
Found leaked block of size 6 at address 0x21718
At time of allocation, the call stack was:
    [1] foo() at line 63 in test.c
    [2] main() at line 47 in test.c

UNIX 程序有一个 main 过程(在 f77 中称为 MAIN),它是程序的顶级用户函数。程序通常以两种方式终止:一种是调用 exit(3),另一种是从 main 返回。在后一种情况下,main 的所有局部变量都会在返回后超出作用域,而它们指向的所有堆块都会按泄漏来报告(除非有全局变量也指向这些块)。

常见的编程惯例是不释放分配给 main 中的局部变量的堆块,因为程序即将终止并从 main 返回,而不必调用 exit()。要防止运行时检查将这种块按内存泄漏来报告,请在 main 中最后一个可执行源代码行设置一个断点,以在 main 返回前停止程序。当程序在该处停止时,使用 showleaks 命令报告所有真正的泄漏,而忽略仅由 main 中的变量超出作用域导致的泄漏。

有关更多信息,请参见showleaks 命令

理解内存泄漏报告

在启用了泄漏检查时,程序退出时会自动生成泄漏报告。所有可能的泄漏都会报告出来,但前提是程序不是使用 kill 命令中止的。报告中的详细程度由 dbx 环境变量 rtc_mel_at_exit 控制(请参见设置 dbx 环境变量)。缺省情况下,会生成简短的泄漏报告。

报告按泄漏的合并大小排序。先报告真正的内存泄漏,然后报告可能的泄漏。详细报告包含详细的栈跟踪信息,其中包括行号和可用的源文件。

两种报告都包括内存泄漏错误的下列信息:

信息  

说明  

大小 

泄漏块的大小。 

位置 

泄漏块被分配到的位置。 

地址 

泄漏块的地址。 

栈 

分配时调用栈,由 check -frames

以下是相应的简短内存泄漏报告。


Actual leaks report    (actual leaks:    3 total size:    2427 bytes)

 Total      Num of  Leaked      Allocation call stack
 Size       Blocks  Block
                    Address
==========  ====== ==========  =======================================
      1852       2      -      true_leak < true_leak
       575       1    0x22150  true_leak < main

Possible leaks report  (possible leaks:  1  total size:       8 bytes)

 Total      Num of  Leaked      Allocation call stack
 Size       Blocks  Block
                    Address
==========  ====== ==========  =======================================
         8       1    0x219b0  in_block < main

以下是一个典型的详细泄漏报告。


Actual leaks report    (actual leaks:         3  total size:    2427 bytes)

Memory Leak (mel):
Found 2 leaked blocks with total size 1852 bytes
At time of each allocation, the call stack was:
    [1] true_leak() at line 220 in "leaks.c"
    [2] true_leak() at line 224 in "leaks.c"

Memory Leak (mel):
Found leaked block of size 575 bytes at address 0x22150
At time of allocation, the call stack was:
    [1] true_leak() at line 220 in "leaks.c"
    [2] main() at line 87 in "leaks.c"

Possible leaks report  (possible leaks:       1  total size:       8 bytes)

Possible memory leak -- address in block (aib):
Found leaked block of size 8 bytes at address 0x219b0
At time of allocation, the call stack was:
    [1] in_block() at line 177 in "leaks.c"
    [2] main() at line 100 in "leaks.c"

生成泄漏报告

可以随时通过使用 showleaks 命令获取泄漏报告,其中会报告自上次执行 showleaks 命令以来的新内存泄漏。有关更多信息,请参见showleaks 命令

合并泄漏

由于单个泄漏的数量可能会非常大,因此运行时检查会自动将同一位置分配的泄漏合并到一个合并泄漏报告中。是合并泄漏还是分别报告泄漏由 number-of-frames-to-match 参数控制,该参数通过 check -leaks 中的 -match m 选项或 showleaks 命令的 -m 选项指定。如果两个或更多泄漏分配时的调用栈与 m 帧在严格程序计数器等级匹配,便会在一个合并泄漏报告中报告这些泄漏。

假设有下列三个调用序列:

块 1  

块 2  

块 3  

[1] malloc

[1] malloc

[1] malloc

[2] d() at 0x20000

[2] d() at 0x20000

[2] d() at 0x20000

[3] c() at 0x30000

[3] c() at 0x30000

[3] c() at 0x31000

[4] b() at 0x40000

[4] b() at 0x41000

[4] b() at 0x40000

[5] a() at 0x50000

[5] a() at 0x50000

[5] a() at 0x50000

如果所有这些块均导致内存泄漏,则 m 值决定泄漏按单独泄漏还是一个重复泄漏来报告。如果 m 为 2,则块 1 和块 2 按一个重复泄漏来报告,因为两个调用序列 malloc() 上的 2 个栈帧相同。块 3 将按单独泄漏来报告,因为 c() 的跟踪与其他块不匹配。如果 m 大于 2,运行时检查会将所有泄漏按单独泄漏来报告。(malloc 不显示在泄漏报告中。)

一般情况下,m 值越小,生成的单个泄漏报告越少,合并泄漏报告越多。m 值越大,生成的合并泄漏报告越少,单个泄漏报告越多。

修复内存泄漏

获得内存泄漏报告后,按下列指导修复内存泄漏。

有关更多信息,请参见showleaks 命令

利用内存使用检查

利用内存使用检查可了解所有使用中的堆内存。可以通过此信息大致了解程序中内存的分配位置或程序的哪些部分在使用动态性最强的内存。另外,在减少程序占用的动态内存时,此信息很有用,并且在进行性能优化时,此信息也可能有用。

内存使用检查在性能优化过程中或对控制虚拟内存使用很有用。程序退出时,便可生成内存使用报告。也可以在执行程序过程中,随时使用 showmemuse 命令(执行该命令会显示内存使用信息)来获得内存使用信息。有关信息,请参见showmemuse 命令

启用内存使用检查便同时启用了泄漏检查。除程序退出时的泄漏报告外,还会获得使用的块 (biu) 报告。缺省情况下,程序退出时会生成简短的使用的块报告。内存使用报告中的详细程度由 dbx 环境变量 rtc_biu_at_exit 控制请参见设置 dbx 环境变量)。

以下是一个典型的简短内存使用报告。


Blocks in use report   (blocks in use: 5   total size:   40 bytes)

 Total     % of Num of  Avg     Allocation call stack
 Size       All Blocks  Size
========== ==== ====== ======  =====================================
        16  40%      2      8  nonleak < nonleak
         8  20%      1      8  nonleak < main
         8  20%      1      8  cyclic_leaks < main
         8  20%      1      8  cyclic_leaks < main

Blocks in use report   (blocks in use: 5   total size:   40 bytes)

Block in use (biu):
Found 2 blocks totaling 16 bytes (40.00% of total; avg block size 8)
At time of each allocation, the call stack was:
     [1] nonleak() at line 182 in "memuse.c"
     [2] nonleak() at line 185 in "memuse.c"

Block in use (biu):
Found block of size 8 bytes at address 0x21898 (20.00% of total)
At time of allocation, the call stack was:
     [1] nonleak() at line 182 in "memuse.c"
     [2] main() at line 74 in "memuse.c"

Block in use (biu):
Found block of size 8 bytes at address 0x21958 (20.00% of total)
At time of allocation, the call stack was:
     [1] cyclic_leaks() at line 154 in "memuse.c"
     [2] main() at line 118 in "memuse.c"

Block in use (biu):
Found block of size 8 bytes at address 0x21978 (20.00% of total)
At time of allocation, the call stack was:
     [1] cyclic_leaks() at line 155 in "memuse.c"
     [2] main() at line 118 in "memuse.c"
The following is the corresponding verbose memory use report:

可以随时使用 showmemuse 命令获得内存使用报告。

抑制错误

运行时检查提供了一个强大的错误抑制工具,利用它可以非常灵活地限制所报告错误的数量和类型。如果发生被抑制的错误,则不会生成任何报告,程序会继续执行,就像没有发生错误一样。

可以使用 suppress 命令抑制错误(请参见suppress 命令)。

可以使用 unsuppress 命令撤消对错误的抑制(请参见unsuppress 命令)。

抑制在同一调试会话期间内的各 run 命令中有效,但在各 debug 命令之间,抑制作用无关。

抑制的类型

下面介绍了一些抑制类型:

按作用域和类型抑制

必须指定要抑制的错误类型。可以指定要抑制的程序部分。选项有:

选项  

说明  

全局 

缺省值,应用于整个程序。 

装入对象 

应用于整个装入对象(如共享库)或主程序。 

文件 

应用于特定文件中的所有函数。 

功能 

应用于特定函数。 

行 

应用于特定源代码行。 

地址 

应用于某地址处的特定指令。 

抑制上一错误

缺省情况下,运行时检查会抑制最近的错误,以防止对相同的错误生成重复报告。这由 dbx 环境变量 rtc_auto_suppress 控制。当 rtc_auto_suppress 设置为 on(缺省值)时,在特定位置出现的特定访问错误只在首次出现时报告,此后抑制报告。例如,要防止因多次执行的循环中出现某一错误而生成多份同一错误的报告时,这很有用。

限制报告的错误数

可以使用 dbx 环境变量 rtc_error_limit 限制将报告的错误数。错误限制分别用于访问错误和泄漏错误。例如,如果将错误限制设置为 5,那么,运行结束时生成的泄漏报告中和发出的每个 showleaks 命令报告的结果中,均显示最多五个访问错误和最多五个内存泄漏。缺省值为 1000。

抑制错误示例

在下面的示例中,main.cc 是文件名,foobar 是函数,a.out 是可执行文件的名称。

不报告在函数 foo 中发生分配的内存泄漏。


suppress mel in foo

抑制报告从 libc.so.1 分配的使用的块。


suppress biu in libc.so.1

抑制在 a.out 的所有函数中从尚未初始化的项读取。


suppress rui in a.out

不报告在文件 main.cc 中从未分配项读取。


suppress rua in main.cc

抑制在 main.cc 的第 10 行重复释放。


suppress duf at main.cc:10

抑制报告函数 bar 中的所有错误。


suppress all in bar

有关更多信息,请参见suppress 命令

缺省抑制

为了检测所有错误,运行时检查不要求使用 - g 选项(符号)编译程序。但是,为保证准确检测某些错误(主要是 rui 错误),有时会需要符号信息。为此,如果没有符号信息,缺省情况下,会抑制某些错误(a.outrui 以及共享库的 ruiaibair)。可以使用 suppress 命令和 unsuppress 命令的 -d 选项更改此行为。

如果使用以下命令,运行时检查将不再抑制在无符号信息(编译时未使用 -g)的代码中从未初始化的内存中读取 (rui):


unsuppress -d rui

有关更多信息, 请参见unsuppress 命令

使用抑制来管理错误

初次在大型程序上运行时,可能出现无法应付的大量错误。采取分阶段的方法可能会更好。这可以按以下方法完成:使用 suppress 命令将报告错误数减少到便于管理的数量、仅修复这些错误以及重复该循环过程,每次重复时抑制的错误越来越少。

例如,可以每次侧重处理几个类型的错误。通常遇到的最常见错误类型是 ruiruawua,而且通常是按该顺序出现。rui 错误最不严重(尽管它们可能会导致以后出现较严重的错误)。通常,程序在遇到这些错误时可能仍会正常运行。ruawua 错误比较严重,因为它们是通过无效内存地址进行的访问,而且总是指示编码错误。

可以先抑制 ruirua 错误。修复出现的所有 wua 错误后,再次运行程序,这次只是抑制 rui 错误。修复出现的所有 rua 错误后,再次运行程序,这次不抑制错误。修复所有 rui 错误。最后,再一次运行程序,确保无残余错误。

如果要抑制上一次报告的错误,请使用 suppress -last

对子进程使用运行时检查

要对子进程使用运行时检查,必须将 dbx 环境变量 rtc_inherit 设置为 on。缺省情况下,该变量设置为 off。(请参见设置 dbx 环境变量。)

如果针对父进程启用了运行时检查,且 dbx 环境变量 follow_fork_mode 设置为 child,则 dbx 支持对子进程使用运行时检查(请参见设置 dbx 环境变量)。

发生派生时,dbx 会自动对子进程使用运行时检查。如果程序调用 exec(),则调用 exec() 的程序的运行时检查设置会传递给该程序。

在任一时刻,运行时检查只能控制一个进程。下面是一个示例。


% cat -n program1.c
     1 #include <sys/types.h>
     2 #include <unistd.h>
     3 #include <stdio.h>
     4
     5 int
     6 main()
     7 {
     8      pid_t child_pid;
     9      int parent_i, parent_j;
    10
    11      parent_i = parent_j;
    12
    13      child_pid = fork();
    14
    15      if (child_pid == -1) {
    16          printf("parent: Fork failed\n");
    17          return 1;
    18      } else if (child_pid == 0) {
    19          int child_i, child_j;
    20
    21          printf("child: In child\n");
    22          child_i = child_j;
    23          if (execl("./program2", NULL) == -1) {
    24              printf("child: exec of program2 failed\n");
    25              exit(1);
    26          }
    27      } else {
    28          printf("parent: child’s pid = %d\n", child_pid);
    29      }
    30      return 0;
    31 }

 % cat -n program2.c
     1
     2 #include <stdio.h>
     3
     4 main()
     5 {
     6      int program2_i, program2_j;
     7
     8      printf ("program2: pid = %d\n", getpid());
     9      program2_i = program2_j;
    10
    11      malloc(8);
    12
    13      return 0;
    14 }
%

 % cc -g -o program1 program1.c
 % cc -g -o program2 program2.c
 % dbx -C program1
 Reading symbolic information for program1
 Reading symbolic information for rtld /usr/lib/ld.so.1
 Reading symbolic information for librtc.so
 Reading symbolic information for libc.so.1
 Reading symbolic information for libdl.so.1
 Reading symbolic information for libc_psr.so.1
 (dbx) check -all
 access checking - ON
 memuse checking - ON
 (dbx) dbxenv rtc_inherit on
 (dbx) dbxenv follow_fork_mode child
 (dbx) run
 Running: program1
 (process id 3885)
 Enabling Error Checking... done
RTC reports first error in the parent, program1
 Read from uninitialized (rui):
 Attempting to read 4 bytes at address 0xeffff110
     which is 104 bytes above the current stack pointer
 Variable is ’parent_j’
 Current function is main
   11       parent_i = parent_j;
(dbx) cont
 dbx: warning: Fork occurred; error checking disabled in parent
 detaching from process 3885
 Attached to process 3886
Because  follow_fork_mode is set to child, when the fork occurs error checking is switched from the parent
to the child process
 stopped in _fork at 0xef6b6040
 0xef6b6040: _fork+0x0008:    bgeu    _fork+0x30
 Current function is main
    13       child_pid = fork();
 parent: child’s pid = 3886
 (dbx) cont
 child: In child
 Read from uninitialized (rui):
 Attempting to read 4 bytes at address 0xeffff108
     which is 96 bytes above the current stack pointer
RTC reports an error in the child
 Variable is ’child_j’
 Current function is main
    22       child_i = child_j;
 (dbx) cont
 dbx: process 3886 about to exec("./program2")
 dbx: program "./program2" just exec’ed
 dbx: to go back to the original program use "debug $oprog"
 Reading symbolic information for program2
 Skipping ld.so.1, already read
 Skipping librtc.so, already read
 Skipping libc.so.1, already read
 Skipping libdl.so.1, already read
 Skipping libc_psr.so.1, already read
When the exec of program2 occurs, the RTC settings are inherited by program2 so access and memory use checking
are enabled for that process
 Enabling Error Checking... done
 stopped in main at line 8 in file "program2.c"
     8       printf ("program2: pid = %d\n", getpid());
(dbx) cont
 program2: pid = 3886
 Read from uninitialized (rui):
 Attempting to read 4 bytes at address 0xeffff13c
     which is 100 bytes above the current stack pointer
RTC reports an access error in the executed program, program2
 Variable is ’program2_j’
 Current function is main
     9       program2_i = program2_j;
 (dbx) cont
 Checking for memory leaks...
RTC prints a memory use and  memory leak report for the process that exited while under RTC control, program2
Actual leaks report   (actual leaks:      1  total size:   8
 bytes)

 Total      Num of  Leaked     Allocation call stack
 Size       Blocks  Block
                    Address
==========  ====== ========== ====================================
         8       1    0x20c50  main
 Possible leaks report  (possible leaks:   0  total size:   0
 bytes)

 execution completed, exit code is 0

对连接的进程使用运行时检查

除非因已分配受影响的内存而无法检测到 RUI,否则可以对连接的进程使用运行时检查。但是,该进程在启动时必须预装入了rtcaudit.so。如果连接的进程是 64 位进程,请使用相应的 64 位 rtcaudit.so,该文件位于:

/installation_directory/lib/dbx/sparcv9/runtime/rtcaudit.so(64 位 SPARC 平台)

/installation_directory/lib/dbx/amd64/runtime/rtcaudit.so(AMD64 平台)

/installation_directory/lib/dbx/runtime/rtcaudit.so(32 位平台)

预装入 rtcaudit.so,请键入:


% setenv LD_AUDIT path-to-rtcaudit/rtcaudit.so

LD_AUDIT 环境变量设置为仅在需要时预装入 rtcaudit.so,而不要一直将其保持为装入状态。例如:


% setenv LD_AUDIT...
% start-your-application
% unsetenv LD_AUDIT

连接到进程后,便可以启用运行时检查。

如果要连接的程序从某一其他程序派生或执行,则需要为主程序(派生者)设置 LD_AUDIT。在派生和执行中会继承 LD_AUDIT 的设置。

LC_AUDIT 环境变量既适用于 32 位程序也适用于 64 位程序,这导致很难为运行 64 位程序的 32 位程序或运行 32 位程序的 64 位程序选择正确的库。某些版本的 Solaris 操作系统支持 LD_AUDIT_32 环境变量和 LD_AUDIT_64 环境变量(它们分别仅影响 32 位程序和 64 位程序)。请参见针对您所运行 Solaris 操作系统的版本的《链接程序和库指南》,确定是否支持这些变量。

结合使用修复并继续功能与运行时检查

可以将运行时检查与修复并继续功能一起使用,以便快速查出并修复编程错误。修复并继续功能提供了强大的组合功能,可以为您节省大量调试时间。以下是一个示例。


% cat -n bug.c
     1 #include stdio.h
     2 char *s = NULL;
     3
     4 void
     5 problem()
     6 {
     7      *s = ’c’;
     8 }
     9
    10 main()
    11 {
    12      problem();
    13      return 0;
    14 }
% cat -n bug-fixed.c
     1 #include stdio.h
     2 char *s = NULL;
     3
     4 void
     5 problem()
     6 {
     7
     8      s = (char *)malloc(1);
     9      *s = ’c’;
    10 }
    11
    12 main()
    13 {
    14      problem();
    15      return 0;
    16 }
yourmachine46: cc -g bug.c
yourmachine47: dbx -C a.out
Reading symbolic information for a.out
Reading symbolic information for rtld /usr/lib/ld.so.1
Reading symbolic information for librtc.so
Reading symbolic information for libc.so.1
Reading symbolic information for libintl.so.1
Reading symbolic information for libdl.so.1
Reading symbolic information for libw.so.1
(dbx) check -access
access checking - ON
(dbx) run
Running: a.out
(process id 15052)
Enabling Error Checking... done
Write to unallocated (wua):
Attempting to write 1 byte through NULL pointer
Current function is problem
    7       *s = ’c’;
(dbx) pop
stopped in main at line 12 in file "bug.c"
   12       problem();
(dbx) #at this time we would edit the file; in this example just copy  
the correct version
(dbx) cp bug-fixed.c bug.c
(dbx) fix
fixing "bug.c" ......
pc moved to "bug.c":14
stopped in main at line 14 in file "bug.c"
   14       problem();
(dbx) cont

execution completed, exit code is 0
(dbx) quit
The following modules in \Qa.out’ have been changed (fixed):
bug.c
Remember to remake program.

有关使用修复和继续功能的更多信息,请参见内存泄漏 (mel) 错误

运行时检查应用编程接口

泄漏检测和访问检查都要求使用共享库 libc.so 中的标准堆管理例程,这样,运行时检查便可跟踪程序中所有内存分配和释放情况。许多应用程序中都有在 malloc()free() 函数的基础上或独立编写而成的自己的内存管理例程。如果您使用自己的分配器(称为专用分配器),运行时检查便无法自动跟踪它们,这样您就无从知晓由于不当使用它们而导致的泄漏和内存访问错误。

不过,运行时检查提供了一个 API 以便使用专用分配器。使用此 API 可将专用分配器视为标准堆分配器。该 API 本身在头文件 rtc_api.h 中提供,并作为 Oracle Solaris Studio 软件的一部分进行分发。手册页 rtc_api(3x) 详细介绍了运行时检查 API 入口点。

专用分配器不使用程序堆时,运行时检查访问错误报告可能会存在一些细小差别。发生有关标准堆块的内存访问错误时,错误报告通常包括堆块分配的位置。专用分配器不使用程序堆时,错误报告可能不包括分配项。

不需要使用运行时检查 API 来跟踪 libumem 中的内存分配器。运行时检查会插入 libumem 堆管理例程并将这些例程重定向至相应的 libc 函数。

在批处理模式下使用运行时检查

bcheck 实用程序是 dbx 的运行时检查的方便的批处理接口。它在 dbx 下运行程序,并且在缺省情况下,将运行时检查错误输出放在缺省文件 program.errs 中。

bcheck 实用程序可以分别或一起执行内存泄漏检查、内存访问检查和内存使用检查。其缺省操作是只执行泄漏检查。有关其使用的更多详细信息,请参见 bcheck(1) 手册页。


注 –

在运行 64 位 Linux 操作系统的系统上运行 bcheck 实用程序之前,必须设置 _DBX_EXEC_32 环境变量。


bcheck 语法

bcheck 的语法如下:


bcheck [-V] [-access | -all | -leaks | -memuse] [-xexec32] [-o logfile] [-q]
[-s script] program [args]

-o logfile 选项用于为日志文件指定另一个名称。-s script 选项在执行程序前使用,用于在 script 文件中包含的 dbx 命令中进行读取。script 文件通常包含 suppressdbxenv 等这类命令,用于调整 bcheck 实用程序的错误输出。

-q 选项可使 bcheck 实用程序处于完全静默状态,返回时的状态与程序相同。要在脚本或 makefiles 中使用 bcheck 实用程序时,此选项很有用。

bcheck 示例

要对 hello 仅执行泄漏检查,请键入:


bcheck hello

要使用参数 5mach 仅执行访问检查,请键入:


bcheck -access mach 5

要以静默方式对 cc 执行内存使用检查,并以正常退出状态退出,请键入:


bcheck -memuse -q cc -c prog.c

在批处理模式下检测到运行时错误时,程序不会停止。所有错误输出都会重定向到错误日志文件 logfile 中。遇到断点或程序被中断时,程序会停止。

在批处理模式下,会生成完整的栈回溯,且其会重定向到错误日志文件。可使用 dbx 环境变量 stack_max_size 控制栈帧数。

如果文件 logfile 已存在,则 bcheck 会清除该文件的内容,然后将批处理输出重定向到该文件。

直接在 dbx 中启用批处理模式

也可以通过设置 dbx 环境变量 rtc_auto_continuertc_error_log_file_name 直接在 dbx 中启用类似于批处理的模式(请参见设置 dbx 环境变量)。

如果 rtc_auto_continue 设置为 on,则运行时检查会继续查找错误,并自动保持运行。它会将错误重定向到 dbx 环境变量 rtc_error_log_file_name 指定的文件。(请参见设置 dbx 环境变量。)缺省的日志文件名为 /tmp/dbx.errlog.uniqueid。要将所有错误都重定向到终端,请将 rtc_error_log_file_name 环境变量设置为 /dev/tty

缺省情况下,rtc_auto_continue 设置为 off。

疑难解答提示

为程序启用了错误检查并运行程序后,可能会检测到下列错误之一:

librtc.so and dbx version mismatch; Error checking disabled

对连接的进程使用运行时检查时,如果 LD_AUDIT 未设置为 Oracle Solaris Studio dbx 映像文件所附带的 rtcaudit.so 的版本,可能会出现此错误。要修复此错误,请更改 LD_AUDIT 的设置。

patch area too far (8mb limitation); Access checking disabled

运行时检查找不到距装入对象足够近的修补空间以便启用访问检查。请参见下面的“运行时检查限制”。

运行时检查限制

运行时检查具有以下限制。

具有更多符号和调试信息时工作效果会更好

访问检查需要装入对象的某些符号信息。当装入对象被完全剥离时,运行时检查可能无法捕获所有的错误。从未初始化的内存错误进行读取可能会出错,因而会被抑制。可以使用 unsuppress rui 命令覆盖抑制。要保留装入对象的符合表,请在剥离装入对象时使用 -x 选项。

运行时检查无法捕获所有数组越界错误。如果没有调试信息,针对静态内存和栈内存的边界检查不可用。

SIGSEGVSIGALTSTACK 信号在 x86 平台上受限制

运行时检查运用内存访问指令进行访问检查。这些指令由 SIGSEGV 处理程序在运行时处理。由于运行时检查需要其自己的 SIGSEGV 处理程序和信号备用栈,所以尝试安装 SIGSEGV 处理程序或 SIGALTSTACK 处理程序会导致发生 EINVAL 错误或忽略该尝试。

SIGSEGV 处理程序的调用不能被嵌套。如果嵌套,则会导致 terminating signal 11 SEGSEGV 错误。如果收到此错误,请使用 rtc skippatch 命令跳过受影响函数的检测过程。

当 8 MB 的所有现有代码中具有足够的补丁区域时工作效果会更好(仅限 SPARC 平台)。

如果 8 MB 的所有现有代码中没有足够的补丁区域,则可能会引发两个问题

如果上述任一条件适用于您的程序,并且在启用访问检查时程序的行为方式开始有所不同,则可能是陷阱处理程序限制影响到您的程序。要变通克服这些限制,您可以执行以下操作:

运行时检查错误

运行时检查报告的错误一般分两类,即访问错误和泄漏。

访问错误

如果启用了访问检查,运行时检查会检测并报告下列类型的错误。

错误释放 (baf) 错误

问题: 尝试释放尚未分配的内存。

可能的原因: 将非堆数据指针传递给了 free()realloc()

示例:

char a[4];
char *b = &a[0];

free(b);                    /* Bad free (baf) */

重复释放 (duf) 错误

问题: 尝试释放已释放过的堆块。

可能的原因: 多次使用同一指针调用 free()。在 C++ 中,多次对同一指针使用删除操作符。

示例:

char *a = (char *)malloc(1);
free(a);
free(a);                    /* Duplicate free (duf) */

未对齐释放 (maf) 错误

问题: 尝试释放未对齐的堆块。

可能的原因: 将未正确对齐的指针传递给了 free()realloc();更改了 malloc 返回的指针。

示例:

char *ptr = (char *)malloc(4);
ptr++;
free(ptr);                    /* Misaligned free */

未对齐读 (mar) 错误

问题: 尝试从未正确对齐的地址中读取数据。

可能的原因: 分别从那些没有半字对齐、字对齐或双字对齐的地址中读取 2 个、4 个或 8 个字节。

示例:

char *s = “hello world”;
int *i = (int *)&s[1];
int j;

j = *i;                    /* Misaligned read (mar) */

未对齐写 (maw) 错误

问题: 尝试将数据写入未正确对齐的地址。

可能的原因: 将 2 个、4 个或 8 个字节分别写入没有半字对齐、字对齐或双字对齐的地址。

示例:

char *s = “hello world”;
int *i = (int *)&s[1];

*i = 0;                    /* Misaligned write (maw) */

内存不足 (oom) 错误

问题: 尝试分配超出可用物理内存的内存。

原因: 程序无法从系统获得更多的内存。查找在未检查 malloc() 的返回值是否为 NULL(一个常见编程错误)时发生的问题时会有用。

示例:

char *ptr = (char *)malloc(0x7fffffff);
/* Out of Memory (oom), ptr == NULL */

从数组越界中读 (rob) 错误

问题:尝试从数组越界内存中进行读取。

可能的原因:溢出堆块边界的迷失指针。

示例:

char *cp = malloc (10);
char ch = cp[10];

从未分配的内存中读 (rua) 错误

问题: 尝试从不存在、未分配或未映射的内存中进行读取。

可能的原因: 溢出堆块边界或访问已被释放的堆块的迷失指针。

示例:

char *cp = malloc (10);
free (cp);
cp[0] = 0;

从未初始化的内存中读 (rui) 错误

问题: 尝试从未初始化的内存中进行读取。

可能的原因: 读取尚未初始化的局部数据或堆数据。

示例:

foo()
{   int i, j;
    j = i;    /* Read from uninitialized memory (rui) */
}

写入到数组越界内存 (wob) 错误

问题:尝试写入到数组越界内存。

可能的原因:溢出堆块边界的迷失指针。

示例:

char *cp = malloc (10);
cp[10] = 'a';

写入到只读内存 (wro) 错误

问题: 尝试写入到只读内存。

可能的原因: 向文本地址写入、向只读数据区 (.rodata) 写入或向已由 mmap 设置为只读的页写入。

示例:

foo()
{   int *foop = (int *) foo;
    *foop = 0;                /* Write to read-only memory (wro) */
}

写入到未分配内存 (wua) 错误

问题: 尝试写入到不存在、未分配或未映射的内存。

可能的原因: 溢出堆块边界或访问已被释放的堆块的迷失指针。

示例:

char *cp = malloc (10);
free (cp);
cp[0] = 0;

内存泄漏错误

启用了泄漏检查时,运行时检查会报告下列类型的错误。

地址位于块内 (aib) 错误

问题: 可能的内存泄漏。没有对已分配块开始处的引用,但至少有一个对块内地址的引用。

可能的原因: 指向块开始处的唯一指针增加。

示例:

char *ptr;
main()
{
   ptr = (char *)malloc(4);
   ptr++;    /* Address in Block */
}

地址位于寄存器内 (air) 错误

问题: 可能的内存泄漏。尚未释放已分配块,程序内存中不存在对块的引用,但寄存器中存在引用。

可能的原因: 如果编译器只将程序变量保留在寄存器中,而不保留在内存中,自然会出现这种错误。编译器常常会在启用了优化功能的情况下这样处理局部变量和函数参数。如果在未启用优化功能的情况下出现这种错误,则可能是真正的内存泄漏。如果指向已分配块的唯一指针在块被释放前超出作用域,便会出现这种情况。

示例:

if (i == 0) {
       char *ptr = (char *)malloc(4);
       /* ptr is going out of scope */
}
  /* Memory Leak or Address in Register */

内存泄漏 (mel) 错误

问题: 尚未释放已分配块,程序中不存在对块的引用。

可能的原因: 程序未能释放不再使用的块。

示例:

char *ptr;

    ptr = (char *)malloc(1);
    ptr = 0;
/* Memory leak (mel) */