利用运行时检查 (Runtime Checking, RTC) 可在开发阶段在本机代码应用程序中自动检测运行时错误(如内存访问错误和内存泄漏)。还可利用它监控内存使用情况。但不能对 Java 代码使用运行时检查。
本章阐述下列主题:
由于运行时检查是一种综合的调试功能,因此可在使用运行时检查功能时(使用收集器收集性能数据的情况除外)执行所有调试操作。
运行时检查:
检测内存访问错误
检测内存泄漏
收集内存使用情况数据
适用于所有语言环境
适用于多线程代码
无需重新编译、重新链接或进行 makefile 更改
如果编译时使用 -g 标志,则在运行时检查错误消息中提供源代码行号关联。运行时检查还可以检查使用优化 -O 标志编译的程序。对于未使用 -g 选项编译的程序,有一些特殊注意事项。
可以通过 check 命令使用运行时检查功能。
一种避免同时出现大量错误的方法是在开发周期中尽早(在开发程序的各个组成模块阶段)使用运行时检查。先编写一个单元测试来驱动每个模块,然后使用运行时检查以递增方式逐个检查模块。这样每次需要处理的错误数就会较少。将所有模块集成为完整的程序时,遇到的新错误可能会很少。将错误数减少为零后,就只有在对模块进行了更改时,才需要再次使用运行时检查。
要使用运行时检查必须满足下列要求:
与 libc 动态链接。
使用标准 libc malloc、free 和 realloc 函数或基于这些函数的分配器。运行时检查提供了一个应用编程接口 (application programming interface, API) 来处理其他分配器。请参见运行时检查应用编程接口。
可接受未完全剥离的程序和使用 strip -x 剥离的程序。
有关运行时检查限制的信息,请参见运行时检查限制。
要使用运行时检查,请在运行程序前启用要使用的检查类型。
(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() 在其返回前没有释放变量 local。memory_leak() 返回时,此变量超出作用域,在第 20 行分配的块变为泄漏。
程序使用全局变量 hello1 和 hello2,这两个变量始终处于作用域内。它们都指向动态分配的内存,这种情况报告为使用的块 (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; |
rui(请参见从未初始化的内存中读 (rui) 错误)
rua(请参见从未分配的内存中读 (rua) 错误)
rob(请参见从数组越界中读 (rob) 错误)
wua(请参见写入到未分配内存 (wua) 错误)
wro(请参见写入到只读内存 (wro) 错误)
wob(请参见写入到数组越界内存 (wob) 错误)
mar(请参见未对齐读 (mar) 错误)
maw(请参见未对齐写 (maw) 错误)
duf(请参见重复释放 (duf) 错误)
baf(请参见错误释放 (baf) 错误)
maf(请参见未对齐释放 (maf) 错误)
oom(请参见内存不足 (oom) 错误)
在 SPARC 平台上,运行时检查不执行数组边界检查,因此不会将数组边界违规按访问错误来报告。
内存泄漏是动态分配的内存块,在程序的数据空间中任何位置都没有指向它的指针。这类块是孤立内存。由于没有指向这些块的指针,程序无法引用它们,更谈不上释放它们。运行时检查会查找并报告这类块。
内存泄漏会导致占用的虚拟内存增加,且通常会导致产生内存碎片。这可能会降低程序及整个系统的性能。
通常情况下,导致出现内存泄漏的原因是未释放分配的内存,而又丢失了指向分配块的指针。下面是一些内存泄漏示例:
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.*/ } |
总是在不再需要内存时便将其释放,并密切注意返回已分配内存的库函数,便可避免内存泄漏。如果使用这类函数,记得要适当地释放内存。
有时,内存泄漏一词用于指未释放的内存块。内存泄漏的这一定义很少用到,因为常见的编程惯例是,如果程序不久便会终止,就不释放内存。如果程序仍然有一个或多个指向内存块的指针,运行时检查不会将内存块按泄漏来报告。
mel(请参见内存泄漏 (mel) 错误)
air(请参见地址位于寄存器内 (air) 错误)
aib(请参见地址位于块内 (aib) 错误)
运行时检查只查找 malloc 内存的泄漏。如果程序未使用 malloc,运行时检查便无法找到内存泄漏。
在两种情况下,运行时检查会报告“可能的”泄漏。第一种情况是没有指针指向块开始处,但有指针指向块内部时。这种情况按“地址位于块内 (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 来缩小泄漏时段。缺省情况下,showleaks 命令仅提供自上次执行 showleaks 命令以来生成的新泄漏。可以在单步执行程序的同时重复运行 showleaks 以缩小内存块泄漏时段。
有关更多信息,请参见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 是文件名,foo 和 bar 是函数,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.out 的 rui 以及共享库的 rui、aib 和 air)。可以使用 suppress 命令和 unsuppress 命令的 -d 选项更改此行为。
如果使用以下命令,运行时检查将不再抑制在无符号信息(编译时未使用 -g)的代码中从未初始化的内存中读取 (rui):
unsuppress -d rui |
有关更多信息, 请参见unsuppress 命令。
初次在大型程序上运行时,可能出现无法应付的大量错误。采取分阶段的方法可能会更好。这可以按以下方法完成:使用 suppress 命令将报告错误数减少到便于管理的数量、仅修复这些错误以及重复该循环过程,每次重复时抑制的错误越来越少。
例如,可以每次侧重处理几个类型的错误。通常遇到的最常见错误类型是 rui、rua 和 wua,而且通常是按该顺序出现。rui 错误最不严重(尽管它们可能会导致以后出现较严重的错误)。通常,程序在遇到这些错误时可能仍会正常运行。rua 和 wua 错误比较严重,因为它们是通过无效内存地址进行的访问,而且总是指示编码错误。
可以先抑制 rui 和 rua 错误。修复出现的所有 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。如果该产品安装在 /opt 目录中,则 rtcaudit.so 位于:
/opt/SUNWspro/lib/v9/rtcaudit.so(对于 64 位 SPARC 平台)
/opt/SUNWspro/lib/amd64/rtcaudit.so(对于 AMD64 平台)
/opt/SUNWspro/lib(对于 32 位平台)
% 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 可将专用分配器视为标准堆分配器。在头文件 rtc_api.h 中提供了 API 自身,并且它作为 Sun Studio 软件的一部分进行分发。手册页 rtc_api(3x) 详细介绍了运行时检查 API 入口点。
专用分配器不使用程序堆时,运行时检查访问错误报告可能会存在一些细小差别。发生有关标准堆块的内存访问错误时,错误报告通常包括堆块分配的位置。专用分配器不使用程序堆时,错误报告可能不包括分配项。
不需要使用运行时检查 API 来跟踪 libumem 中的内存分配器。运行时检查会插入 libumem 堆管理例程并将这些例程重定向至相应的 libc 函数。
bcheck 实用程序是 dbx 的运行时检查的方便的批处理接口。它在 dbx 下运行程序,并且在缺省情况下,将运行时检查错误输出放在缺省文件 program.errs 中。
bcheck 实用程序可以分别或一起执行内存泄漏检查、内存访问检查和内存使用检查。其缺省操作是只执行泄漏检查。有关其使用的更多详细信息,请参见 bcheck(1) 手册页。
在运行 64 位 Linux 操作系统的系统上运行 bcheck 实用程序之前,必须设置 _DBX_EXEC_32 环境变量。
bcheck [-V] [-access | -all | -leaks | -memuse] [-xexec32] [-o logfile] [-q] [-s script] program [args] |
-o logfile 选项用于为日志文件指定另一个名称。-s script 选项在执行程序前使用,用于在 script 文件中包含的 dbx 命令中进行读取。script 文件通常包含 suppress 和 dbxenv 等这类命令,用于调整 bcheck 实用程序的错误输出。
-q 选项可使 bcheck 实用程序处于完全静默状态,返回时的状态与程序相同。要在脚本或 makefiles 中使用 bcheck 实用程序时,此选项很有用。
要对 hello 仅执行泄漏检查,请键入:
bcheck hello |
要使用参数 5 对 mach 仅执行访问检查,请键入:
bcheck -access mach 5 |
要以静默方式对 cc 执行内存使用检查,并以正常退出状态退出,请键入:
bcheck -memuse -q cc -c prog.c |
在批处理模式下检测到运行时错误时,程序不会停止。所有错误输出都会重定向到错误日志文件 logfile 中。遇到断点或程序被中断时,程序会停止。
在批处理模式下,会生成完整的栈回溯,且其会重定向到错误日志文件。可使用 dbx 环境变量 stack_max_size 控制栈帧数。
如果文件 logfile 已存在,则 bcheck 会清除该文件的内容,然后将批处理输出重定向到该文件。
也可以通过设置 dbx 环境变量 rtc_auto_continue 和 rtc_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 设置为随 Sun Studio dbx 映像提供的 rtcaudit.so 以外的版本,可能会出现此错误。要修复此错误,请更改 LD_AUDIT 的设置。
patch area too far (8mb limitation); Access checking disabled
运行时检查找不到距装入对象足够近的修补空间以便启用访问检查。请参见下面的“运行时检查限制”。
运行时检查具有以下限制。
访问检查需要装入对象的某些符号信息。当装入对象被完全剥离时,运行时检查可能无法捕获所有的错误。从未初始化的内存错误进行读取可能会出错,因而会被抑制。可以使用 unsuppress rui 命令覆盖抑制。要保留装入对象的符合表,请在剥离装入对象时使用 -x 选项。
运行时检查无法捕获所有数组越界错误。如果没有调试信息,针对静态内存和栈内存的边界检查不可用。
运行时检查运用内存访问指令进行访问检查。这些指令由 SIGSEGV 处理程序在运行时处理。由于运行时检查需要其自己的 SIGSEGV 处理程序和信号备用栈,所以尝试安装 SIGSEGV 处理程序或 SIGALTSTACK 处理程序会导致发生 EINVAL 错误或忽略该尝试。
SIGSEGV 处理程序的调用不能被嵌套。如果嵌套,则会导致 terminating signal 11 SEGSEGV 错误。如果收到此错误,请使用 rtc skippatch 命令跳过受影响函数的检测过程。
如果 8 MB 的所有现有代码中没有足够的补丁区域,则可能会引发两个问题
缓慢
如果启用了访问检查,dbx 会用分支到一个补丁区域的分支指令来替换每一个装入和存储指令。此分支指令寻址范围为 8 MB。如果被调试程序用完了替换的特定装入或存储指令的所有 8 MB 地址空间,将没有空间来存放补丁区域。在这种情况下,dbx 会调用陷阱处理程序而不是使用分支。将控制权移交给陷阱处理程序的过程非常缓慢(最多会慢 10 倍),但不受 8 MB 限制。
V8+ 模式中的输出寄存器覆盖问题
如果同时符合以下两个条件,则陷阱处理程序限制会影响访问检查:
被调试的进程使用陷阱进行程序校验。
进程使用 V8+ 指令集。
出现问题的原因是,V8+ 体系结构中输出寄存器和输入寄存器的大小不相同。输出寄存器的长度为 64 位,而输入寄存器仅为 32 位。当调用陷阱处理程序时,输出寄存器被复制到输入寄存器,而较高的 32 位会丢失。因此,如果被调试的进程使用输出寄存器的高 32 位,则启用访问检查时该进程可能会以不正确的方式运行。
缺省情况下,创建基于 SPARC 的 32 位二进制文件时,编译器使用 V8+ 体系结构,但您可以使用 -xarch 选项来通知编译器使用 V8 体系结构。遗憾的是,重新编译应用程序不会影响系统运行时库。
dbx 会自动跳过以下函数和库(已知使用陷阱进行程序校验时不能正常工作)的检测过程:
server/libjvm.so
client/libjvm.so
`libfsu_isa.so`__f_cvt_real
`libfsu_isa.so`__f90_slw_c4
但是,跳过检测过程可能会导致不正确的 RTC 错误报告。
如果上述任一条件适用于您的程序,并且在启用访问检查时程序的行为方式开始有所不同,则可能是陷阱处理程序限制影响到您的程序。要变通克服这些限制,您可以执行以下操作:
使用 rtc skippatch 命令(请参见rtc skippatch 命令)跳过使用上面所列函数和库的程序代码的检测过程。通常,很难跟踪某个特定函数的问题,所以您可能需要跳过整个装入对象的检测过程。rtc showmap 命令可显示按地址排序的程序校验类型图。
尝试使用 64 位 SPARC-V9 而不使用 32 位 SPARC-V8
如果可能,重新编译 V9 体系结构的程序,在该体系结构中,所有的寄存器长度为 64 位。
尝试添加补丁区域目标文件。
可以使用 rtc_patch_area shell 脚本创建特殊的 .o 文件,这些文件可以链接到大型可执行文件或共享库的中间,从而可以提供更多补丁空间。请参见 rtc_patch_area(1) 手册页。
如果 dbx 达到 8 MB 限制,它会告知哪个装入对象过大(主程序还是共享库),并会输出该装入对象所需的补丁空间总量。
为了得到最佳结果,应在可执行文件或共享库中均匀分布这些特殊的补丁目标文件,并应使用缺省大小 (8 MB) 或更小的大小。另外,添加的补丁空间不要超过 dbx 指出的所需大小的 10-20%。例如,如果 dbx 指出需要 31 MB 用于 a.out,请添加使用 rtc_patch_area 脚本创建的四个目标文件,每个大小均为 8 MB,然后在可执行文件中大致平均地分布这些文件。
如果 dbx 在可执行文件中找到显式补丁区域,则会打印这些补丁区域跨越的地址范围,这有助于将它们正确地置于链接行上。
尝试将较大的装入对象分解为较小的装入对象。
将可执行文件或大型库中的目标文件分解成较小的多组目标文件。然后将它们链接成较小的部分。如果大型文件是可执行文件,则将其分解成较小的可执行文件和一系列共享库。如果大型文件是共享库,则将其重新安排成一组较小的库。
dbx 可利用这一技术在不同的共享对象间为补丁代码寻找空间。
尝试添加“填充”.so 文件。
只有当要连接已启动的进程时,才需要采用此解决方案。
运行时链接程序可能会将各库非常紧密地放置在一起,造成无法在库之间的间隙中创建补丁空间。如果 dbx 在启用了运行时检查的情况下启动可执行文件,它会要求运行时链接程序在共享库之间留出额外的间隙;但如果连接的进程不是由 dbx 在启用了运行时检查的情况下启动的进程,这些库之间可能会非常靠近。
如果运行时库之间太近,(且如果无法使用 dbx 启动程序),则可以尝试使用 rtc_patch_area 脚本创建一个共享库,然后将其链接到其他共享库之间的程序中。有关更多详细信息,请参见 rtc_patch_area(1) 手册页。
运行时检查报告的错误一般分两类,即访问错误和泄漏。
如果启用了访问检查,运行时检查会检测并报告下列类型的错误。
问题: 尝试释放尚未分配的内存。
可能的原因: 将非堆数据指针传递给了 free() 或 realloc()。
示例:
char a[4]; char *b = &a[0]; free(b); /* Bad free (baf) */
问题: 尝试释放已释放过的堆块。
可能的原因: 多次使用同一指针调用 free()。在 C++ 中,多次对同一指针使用删除操作符。
示例:
char *a = (char *)malloc(1); free(a); free(a); /* Duplicate free (duf) */
问题: 尝试释放未对齐的堆块。
可能的原因: 将未正确对齐的指针传递给了 free() 或 realloc();更改了 malloc 返回的指针。
示例:
char *ptr = (char *)malloc(4); ptr++; free(ptr); /* Misaligned free */
问题: 尝试从未正确对齐的地址中读取数据。
可能的原因: 分别从那些没有半字对齐、字对齐或双字对齐的地址中读取 2 个、4 个或 8 个字节。
示例:
char *s = “hello world”; int *i = (int *)&s[1]; int j; j = *i; /* Misaligned read (mar) */
问题: 尝试将数据写入未正确对齐的地址。
可能的原因: 将 2 个、4 个或 8 个字节分别写入没有半字对齐、字对齐或双字对齐的地址。
示例:
char *s = “hello world”; int *i = (int *)&s[1]; *i = 0; /* Misaligned write (maw) */
问题: 尝试分配超出可用物理内存的内存。
原因: 程序无法从系统获得更多的内存。查找在未检查 malloc() 的返回值是否为 NULL(一个常见编程错误)时发生的问题时会有用。
示例:
char *ptr = (char *)malloc(0x7fffffff); /* Out of Memory (oom), ptr == NULL */
问题:尝试从数组越界内存中进行读取。
可能的原因:溢出堆块边界的迷失指针。
示例:
char *cp = malloc (10); char ch = cp[10];
问题: 尝试从不存在、未分配或未映射的内存中进行读取。
可能的原因: 溢出堆块边界或访问已被释放的堆块的迷失指针。
示例:
char *cp = malloc (10); free (cp); cp[0] = 0;
问题: 尝试从未初始化的内存中进行读取。
可能的原因: 读取尚未初始化的局部数据或堆数据。
示例:
foo() { int i, j; j = i; /* Read from uninitialized memory (rui) */ }
问题:尝试写入到数组越界内存。
可能的原因:溢出堆块边界的迷失指针。
示例:
char *cp = malloc (10); cp[10] = 'a';
问题: 尝试写入到只读内存。
可能的原因: 向文本地址写入、向只读数据区 (.rodata) 写入或向已由 mmap 设置为只读的页写入。
示例:
foo() { int *foop = (int *) foo; *foop = 0; /* Write to read-only memory (wro) */ }
问题: 尝试写入到不存在、未分配或未映射的内存。
可能的原因: 溢出堆块边界或访问已被释放的堆块的迷失指针。
示例:
char *cp = malloc (10); free (cp); cp[0] = 0;
启用了泄漏检查时,运行时检查会报告下列类型的错误。
问题: 可能的内存泄漏。没有对已分配块开始处的引用,但至少有一个对块内地址的引用。
可能的原因: 指向块开始处的唯一指针增加。
示例:
char *ptr; main() { ptr = (char *)malloc(4); ptr++; /* Address in Block */ }
问题: 可能的内存泄漏。尚未释放已分配块,程序内存中不存在对块的引用,但寄存器中存在引用。
可能的原因: 如果编译器只将程序变量保留在寄存器中,而不保留在内存中,自然会出现这种错误。编译器常常会在启用了优化功能的情况下这样处理局部变量和函数参数。如果在未启用优化功能的情况下出现这种错误,则可能是真正的内存泄漏。如果指向已分配块的唯一指针在块被释放前超出作用域,便会出现这种情况。
示例:
if (i == 0) { char *ptr = (char *)malloc(4); /* ptr is going out of scope */ } /* Memory Leak or Address in Register */
问题: 尚未释放已分配块,程序中不存在对块的引用。
可能的原因: 程序未能释放不再使用的块。
示例:
char *ptr;
ptr = (char *)malloc(1); ptr = 0; /* Memory leak (mel) */