【更多软件使用问题请点击亿道电子官方网站】
一、文档背景
在编写代码时,内存检查的重要性不容忽视。内存是程序运行的核心资源之一,负责存储程序运行所需的数据。由于内存资源的有限性和操作的复杂性,内存错误常常会直接导致程序崩溃、性能下降甚至安全漏洞的产生,因此,内存检查对于确保程序的稳定性、性能和安全性具有至关重要的作用。那么工程师对于这样的问题,经常处理起来很头疼。因为大量的变量定义、指针使用以及内存的分配和释放,导致工程师可能需要一个一个地去排查。这样的工作量无疑是十分庞大的。可能会严重耽误项目进展。
二、 问题分析
为了更好的规避这样的问题,那么我们就需要借助一下Run-Time Check功能。"Run-Time Check" 是一种在程序运行时执行的检查机制,旨在帮助开发人员识别和修复可能的问题,如内存泄漏、数组越界、空指针引用等。这些检查通常在开发或调试阶段启用,以帮助捕获和定位潜在的错误。GHS就存在这样的功能,可以通过下面的内容来学习GHS的Run-Time Check功能要如何使用。
(注意:因为GHS的Run-time检查是Build选项。所以,所有的选项都需要重新构建后,配置选项才会真正地生效。)
三、 GHS的Run-Time Error Check的使用
-
Run-Time Error Check是GHS中用于控制运行时错误检查的一个选项,这在调试会话期间可能很有帮助,但会增加应用程序的大小并降低速度。这个选项有多个检查类型,可以自由地搭配不同的检查类型去实现在代码编写上存在的内存隐患。
-
Run-Time Error Check有哪些选项并且要如何配置
-
Run-Time Error Check选项主要针对的检查类型为:
-
Assignment Bounds (-check=assignbound) :赋值范围检查
-
Array Bounds(-check=bounds):数组越界检查
-
Case Label Bounds(-check=switch):Case标签边界检查
-
Divide by Zero(-check=zerodivide):除零操作检查
-
Nil Pointer Dereference(-check=nilderef):空指针解引用检查
-
Write to Watchpoint(-check=watch):地址写入检查
-
-
图3-1中可以在Value一栏中表示的是该检查类型的使能情况。“+”表示使能该检查类型;“-”表示失能该检查类型;“.”表示清除该检查类型的值。工程师可以通过直接检查value栏中图标进行逐一修改。也可以通过下面"All: - . +"去对于所有的类型进行一键配置。
-
图3-1
(三)不同选项的具体分析和实际效果
1.Assignment Bounds (-check=assignbound)赋值范围检查:如果赋给整型或枚举对象的值超出该对象的范围,则生成错误。(例如,将long long赋值给int对象。)只有当枚举值小于最小枚举值或大于最大枚举值时,才会发出对枚举赋值的错误。当启用这种检查时,诊断68 (integer_sign_change)和诊断69 (integer_truncated)的某些实例将被发出默认的严重级别warning,而不是remark。并且在出现该类型的错误的时候,报错信息为 Assignment out of range of integral/enum type(赋值超出整型/枚举类型的范围)和Value outside of type(类型之外的值)。
补充:(注意:对于变量直接赋值的时候,如果超过了赋值范围,该检查不会进行报错。初步推断,该检测仅限于变量使用过程中出现的赋值问题)
例子:在程序中定义了一个char类型的变量temp。在代码中有一个Add的函数,可以看到这个函数所有的结果都被直接加上300。所以,该函数的返回值一定的大于300的。而接收返回值的temp变量的赋值范围是-128~127。
a.没启用Assignment Bounds检查的时候,在Debug运行过程中的结果如图3-2-1-1。代码是正常运行的,没有显示任何的运行错误。
图3-2-1-1
b.启用Assignment Bounds检查的时候,在Debug运行过程中的结果如图3-2-1-2。在运行代码后,在 输出窗口会提示“Stopped by runtime error: Assignment out of range of integral/enum type on line 6 of function main in file hello.c”,提示在hello.c文件中的第6行的main函数中存在赋值越界的报错。
图3-2-1-2
2.Array Bounds (-check=bounds)数组越界检查:如果使用无效索引访问数组,则生成错误。只检查在编译时边界已知的数组;不检查不完整和可变长度数组。编译器分别检查每个单独的数组索引。在运行的时候检查到这个错误会提示:Array index out of bounds(数组索引越界)
例子:在工程中定义了一个int类型的二维数组temp[2][3]。在下面的赋值语句中可以看到对于temp[0][3]进行赋值,这个赋值语句是不对的,数组定义了3列,则最大的列索引值为2。这个语句的索引值为3。这样就会产生数组越界的情况。
a.没启用Array Bounds检查的时候,在Debug运行过程中的结果如图3-2-2-1。运行过程中没有出现任何报错信息。
图3-2-2-1
b.启用Array Bounds检查的时候,在Debug运行过程中的结果如图3-2-2-2。在输出窗口中可以看到报错信息:“Stopped by runtime error: Array index out of bounds on line 8 of function main in file hello.c”。表示在hello.c文件中的第8行的语句存在数组越界的情况。
图3-2-2-2
3.Case Label Bounds(-check=switch):Case标签边界检查:如果开关表达式与任何大小写标签不匹配,则生成错误。当使用默认的case标签时,此操作不适用。即在Switch语句中输入值和所有的case值均不匹配就会产生错误。但是,如果Switch语句中有default语句的话,该选项就不会生效。如果检查到这个类型的错误会提示的信息为:Case/switch index out of bounds(Case/switch索引越界)
例子:在工程中定义了一个int类型的变了num。然后,编写了一个Switch语句,num作为表达式进行输入。switch语句中有3个case值,分别是0、1、2。而num初始化的时候赋值为3。在代码运行的时候就会出现没有对应的case值匹配的问题。
a.没启用Case Label Bounds检查的时候,在Debug运行过程中的结果如图3-2-3-1。运行过程中没有出现任何报错信息。
图3-2-3-1
b.启用Case Label Bounds检查的时候,在Debug运行过程中的结果如图3-2-3-2。在输出窗口中就可以看到报错信息:“Stopped by runtime error: Case/switch index out of bounds on line 19 of function main in file hello.c ”,表示文件hello.c中函数main的第19行索引越界。
图3-2-3-2
c.Divide by Zero(-check=zerodivide)除零操作检查:检查代码中是否发生了除零操作,如果出现了除零操作操作,就会终止程序。该选项同时会记录整数除法和浮点除法。在运行的时候检查到这个错误会提示:Divide by 0(除以0)
例子:在代码中定义了一个char类型的变量temp,并且赋值为0。定义了一个int类型的变量num,并且赋值为3。然后,temp会被作为除数,num作为被除数进行计算。因为temp为0,所以在运算过程中就会出现除零操作。
4.没启用Divide by Zero检查的时候,在Debug运行过程中的结果如图3-2-4-1。运行过程中没有出现任何报错信息。
图3-2-4-1
a.启用Divide by Zero检查的时候,在Debug运行过程中的结果如图3-2-4-2。在输出窗口中可以看到报错信息:“Stopped by runtime error: Divide by 0 on line 5 of function main in file hello.c”,表示hello.c中函数main的第5行发现除零的操作。
图3-2-4-2
5.Nil Pointer Dereference(-check=nilderef)空指针解引用检查:对空指针的解引用产生错误。在运行的时候检查到这个错误会提示:Nil pointer dereference(空指针解引用)
例子:在代码中定义了一个int类型的指针ptr,并赋值NULL,即为空指针。然后,定义了一个int类型的变量value,并将指针ptr赋值给到变量。那么在这里就会出现错误地将一个指针变量赋值为NULL,而后续使用该指针传递,就会发生空指针解引用。
a.没启用Nil Pointer Dereference检查的时候,在Debug运行过程中的结果如图3-2-5-1。运行过程中没有出现任何报错信息。
图3-2-5-1
b.启用Nil Pointer Dereference检查的时候,在Debug运行过程中的结果如图3-2-5-2。
图3-2-5-2
6.Write to Watchpoint(-check=watch)地址写入检查:检查代码以监视对内存中任意地址的写入。当您的目标不支持硬件断点时使用此检查。可以使用watchpoint命令在运行时指定观察点地址。在运行的时候检查到该操作时会提示:Write to watchpoint(写入观察点)
例子:在代码中,定义了一个int类型的指针ptr,并且让指针指向0x200F0FF这个地址。然后,对于指针进行赋值。这样就会对于0x200F0FF产生一个写操作了。
a.在启用了Write to Watchpoint这个选项后,在下面的输出窗口中输入"watchpoint 0x200F0FF",在0x200F0FF设置一个观察点。在运行过程中,一旦对于0x200F0FF这个出现了写操作就会停止运行。并输出相关信息。(如图3-2-6-1)
图3-2-6-1
以上就是对于在代码本身编写上存在的内存问题进行的检查机制,这些类型可以有效地将代码编写造成的内存问题进行有效的规避。并且为了使用运行时错误检查,您必须能够在不触发中断的情况下单步执行您的目标。
四、GHS的Run-Time Memory Checks的使用
(一)Run-Time Memory Checks可以跟踪涉及误用标准库函数malloc()和free()的bug,并且会检查错误地释放了没有使用malloc()分配的内存,或者尝试使用已经释放的内存的情况,以确保进程不会以不可预测的方式运行。
(二)Run-Time Memory Checks的选项(如图4-2)
图4-2-1
1.None (-check=nomemory) -- 禁用所有由内存检查启用的检查。这个是Run-Time Memory Checks选项的默认值。
2.General (-check=alloc)(仅支持内存分配 ): 选择一个特殊版本的内存分配库例程,包括 malloc()函数和 free()函数。这些库例程会检查到有限的内存分配问题,例如释放一个尚未分配的对象。请注意,这个设置等价于 -malloc_version=memcheck。这种形式的运行时内存检查将大大增加堆大小,并略微降低进程速度。主要检查的问题有:
a.Memory that has not been allocated with malloc() is subsequently freed.(未使用malloc()分配的内存随后被释放)
b.The same area of memory is freed twice.(同一区域的内存被释放两次)
c.Recently freed memory has been written to (may be detected when calls to the malloc library are made).(最近释放的内存已经被写入(可能在调用malloc库时被检测到))
3.Intensive (-check=memory):在用户应用程序的每次指针解引用时引入额外的代码,以检查包括由Run-Time Error Check->Nil Pointer Dereference(-check=nilderef)选项检查到的各种问题在内的一系列内存分配问题。此设置隐含了General (-check=alloc),因为需要该选项所选的特定库例程。要对应用程序的一小部分进行完整的内存检查,请在编译相关文件时指定 -check=memory,或者在源代码中使用 #pragma ghs check=memory,并在链接整个程序时指定 -check=alloc。主要检查的问题有:
-
Memory that has not been allocated with malloc () is subsequently freed.(未使用malloc()分配的内存随后被释放)
-
The same area of memory is freed twice.(同一区域的内存被释放两次。)
-
Recently freed memory has been written to.(最近释放的内存已被写入)
-
Attempts are made to dereference NULL.(尝试解引用NULL)
-
Accesses are attempted past the end of a heap-allocated data structure, causing array overflowerrors.(访问超出了堆分配的数据结构的末尾,导致数组溢出错误。)
4.下面两个例子展示了出现内存错误的现象之一
-
使用一个同一区域的内存被释放两次的代码,在开启了General (-check=alloc)选项后,现象如图4-2-2。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = NULL; //malloc(sizeof(int)); // 第一次分配内存
int value = *ptr;
if (ptr == NULL) {
perror("malloc failed");
return 1;
}
*ptr = 42; // 初始化内存
free(ptr); // 第一次释放内存
//ptr = NULL; // 将指针设为NULL,防止悬挂指针
// 错误的部分:尝试第二次释放同一块内存
free(ptr); // 此时ptr为NULL,但假设它不是NULL
return 0;
}
图4-2-2
-
使用一个会产生访问超出了堆分配的数据结构的末尾,导致数组溢出错误的代码,在开启了Intensive (-check=memory)这个选项后,现象如图4-4-2所示
-
#include <stdio.h> #include <stdlib.h> int main() { // 分配一个大小为5的整数数组 int *array = (int *)malloc(5 * sizeof(int)); if (array == NULL) { perror("Memory allocation failed"); return EXIT_FAILURE; } // 初始化数组 for (int i = 0; i < 5; i++) { array[i] = i; } // 打印数组内容 printf("Original array:\n"); for (int i = 0; i < 5; i++) { printf("%d ", array[i]); } printf("\n"); // 尝试访问超出数组末尾的索引(数组溢出) // 注意:这是一个错误的行为,应该避免 // 在这里,我们尝试访问array[5],但是array只有从array[0]到array[4]的5个元素 printf("Trying to access out-of-bounds element: %d\n", array[5]); // 数组溢出 // 释放内存 free(array); return 0; }
图4-2-3
(三)General (-check=alloc)、Intensive (-check=memory)这两种形式的内存检查都需要一个目标可遍历堆栈。如果启用内存检查,则在编译任何可能调用malloc(直接或间接)的函数时必须使能Build options中的Advanced->Debugging Options->Generate Target-Walkable Stack选项(如图4-3),这个选项会控制允许目标遍历其自身堆栈的代码的生成。请注意,当启用-check=alloc或-check=memory时,-gtws是默认值。因此,如果将其中一个传递给整个项目,-gtws将自动启用。
图4-3
五、GHS的Number of Callers to Track with Run-Time Memory Checks的使用
(一)Number of Callers to Track with Run-Time Memory Checks:是用于在启用运行时内存检查时,指定应该为每个分配跟踪和报告的调用者数量。这个选项需要搭配Run-Time Memory Checks这个选项进行使用。
(二)在配置上的话,一般建议value的值在5以上。默认的值是8个。并且显示的caller数量将限于MULTI所支持的数量。使用multi7,任何超过5的额外caller将不会显示。
六、讨论分析
-
Number of Callers to Track with Run-Time Memory Checks选项中Callers的概念是什么?
答:这里的“caller”指的是在程序执行过程中调用某个函数或变量的上层调用者。换句话说,“caller”是指那些在程序中进行函数调用的代码位置或上下文。当启用运行时内存检查时,系统会跟踪并报告哪些代码(即哪些调用者)导致了内存分配。这有助于开发者了解内存分配的来源,从而更好地进行调试和优化内存使用。
-
解引用是什么?
答:解引用(dereferencing)是指在编程中通过指针访问指针所指向的内存地址上的数据或对象的过程。简单来说,就是通过指针找到它指向的具体值或对象。解引用是指针操作的核心,允许程序通过指针来访问和操作内存中的数据。正确地使用解引用可以有效地管理和操作复杂的数据结构,如链表、树和图等。但错误的解引用,尤其是空指针解引用,则会导致程序崩溃或未定义行为,因此在解引用前进行空指针检查是良好的编程实践。
-
为什么在switch语句中开关表达式与任何大小写标签不匹配,会产生内存问题?
答:在 switch 语句中,如果没有 default 标签,当开关表达式与任何 case 标签都不匹配时,程序会跳过 switch 语句内部的代码,继续执行 switch 语句后的代码。虽然这不会直接引发内存问题,但可能会导致程序的逻辑错误或未定义行为,这些错误可能会间接引发内存问题。
七、结论
通过GHS的Run-Time检查功能,可以有效地避免内存泄漏问题的产生。防止出现变量赋值越界、数据越界、空指针解引用、内存重复释放、数组溢出错误等会直接导致程序崩溃的内存问题的出现。在调试阶段和构建阶段就可以直接将问题点给指出来,不需要一个一个地调试确认,重复运行程序确认内存泄漏触发的位置。利用好GHS的Run-Time检查功能可以大大地提高工作效率,进一步地规范代码编写的标准,也提供了更优的内存问题的检查方式。
————————————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
关于亿道电子
亿道电子技术有限公司(www.emdoor.cn)是国内资深的研发工具软件提供商,公司成立于2002年,面向中国广大的制造业客户提供研发、设计、管理过程中使用的各种软件开发工具,致力于帮助客户提高研发管理效率、缩短产品设计周期,提升产品可靠性。