C和C ++程序中的内存错误很糟糕:它们很常见,并且可能导致严重的后果。 来自计算机紧急响应小组(请参阅参考资料 )和供应商的许多最严重的安全通知都是关于简单内存错误的注释。 自70年代末以来,C程序员就一直在谈论此类错误,但在2007年影响仍然很大。更糟糕的是,如果以我的印象为指导,当今的许多C和C ++编码人员似乎都将内存错误视为不可控的神秘问题,哪一个只能恢复,不能阻止。
不是这样 本文表明,可以一口气理解与内存相关的良好编码的所有要点:
正确的内存管理的重要性
具有内存错误的C和C ++程序会引起问题。 如果它们泄漏内存,它们的运行速度将逐渐变慢,并最终停止。 如果它们覆盖内存,则它们很脆弱,很容易受到恶性用户的劫持。 Rodney Bates在2004年写道:从1988年著名的Morris蠕虫到Flash Player以及最新的重要零售级别程序的最新安全警报,都依赖于缓冲区溢出:“大多数计算机安全漏洞是缓冲区溢出。”
许多其他通用语言,例如Java™,Ruby,Haskell,C#,Perl,Smalltalk等,在可能代替使用C或C ++的情况下被广泛征募,并且每种语言都有很大的爱好者和优势。 但是,计算的一部分民间传说是,与C或C ++相比,每种方法具有的大多数可用性优势都必须严格地与简化内存管理有关。 与内存相关的编程是如此重要,并且在实践中很难正确地应用它,以至于支配了面向对象,功能,高级,声明性和其他编程语言质量的所有其他变量或理论。
内存错误也可能以其他几种错误的常见方式出现隐患:难以再现,并且症状通常很难在相应的源代码中定位。 例如,内存泄漏可能会使应用程序在不透明的同时完全不可接受,无论泄漏发生在何处或何时发生。
因此,出于所有这些原因,C和C ++编程的内存方面值得特别考虑。 让我们看看您可以做些什么,而不是避免使用这些语言。
内存错误的类别
首先,不要绝望。 有解决内存挑战的方法。 首先列出所有可能的有效困难:
- 内存泄漏
- 错误分配,包括乘法
free()
d内存和未初始化的引用 - 悬空指针
- 数组界限违规
这就是整个列表。 甚至使用C ++的面向对象语言也不会显着改变类别。 无论数据是C的简单类型和struct
还是C ++的类,C和C ++中的内存管理和引用模型都基本相同。 接下来的大部分内容是在“纯C”中进行的,在很大程度上保留了对C ++的扩展。
内存泄漏
分配资源时会发生内存泄漏,但是从不回收它。 这是一个可能出问题的模型(请参见清单1 ):
清单1.简单的潜在堆内存丢失和缓冲区覆盖
void f1(char *explanation)
{
char *p1;
p1 = malloc(100);
(void) sprintf(p1,
"The f1 error occurred because of '%s'.",
explanation);
local_log(p1);
}
看到问题了吗? 除非local_log()
对将其传递的内存进行free()
负有特殊责任,否则f1
调用每次调用都会泄漏100个字节。 在内存中赠送了几兆字节作为促销品的时候,这是很小的,但是,在连续几个小时的连续运行中,即使是很小的损失也会破坏应用程序。
在实际的C和C ++编程中,仅清理malloc()
或new
使用是不够的。本节开头的句子恰恰是因为像这样的示例而提到了“ resources”而不是“ memory”(参见清单1)。 2 )。 FILE
句柄可能看起来不像是内存块,但必须谨慎对待它们:
清单2.资源管理不当可能导致的堆内存丢失
int getkey(char *filename)
{
FILE *fp;
int key;
fp = fopen(filename, "r");
fscanf(fp, "%d", &key);
return key;
}
fopen
的语义需要互补的fclose
。 尽管C标准没有指定没有fclose()
会发生什么,但它很可能会泄漏内存。 其他资源,例如信号量,网络句柄,数据库连接等,也应考虑相同的因素。
内存分配错误
较难管理的是错误分配。 这是一个示例(请参见清单3 ):
清单3.未初始化的指针
void f2(int datum)
{
int *p2;
/* Uh-oh! No one has initialized p2. */
*p2 = datum;
...
}
有关此类错误的好消息是,它们往往会产生严重的后果。 在AIX®下,分配给未初始化的指针通常会导致立即分段错误 。 这样很好,因为可以Swift发现任何此类故障。 这些错误比需要数月才能识别并且难以重现的错误便宜得多。
此类别中有几种变体。 内存比malloc()
可以更频繁地使用free()
d(请参见清单4 ):
清单4.两种错误的内存释放
/* Allocate once, free twice. */
void f3()
{
char *p;
p = malloc(10);
...
free(p);
...
free(p);
}
/* Allocate zero times, free once. */
void f4()
{
char *p;
/* Note that p remains uninitialized here. */
free(p);
}
这些错误通常也不是严重的。 尽管C标准没有在这些情况下定义行为,但是典型的实现忽略了这些错误,或者Swift,生动地标记了这些错误。 如上所述,这些都是安全的情况。
悬空指针
悬空指针更麻烦。 当程序员在释放内存后使用内存资源时,就会出现一个悬空指针(请参见清单5 ):
清单5.悬空指针
void f8()
{
struct x *xp;
xp = (struct x *) malloc(sizeof (struct x));
xp.q = 13;
...
free(xp);
...
/* Problem! There's no guarantee that
the memory block to which xp points
hasn't been overwritten. */
return xp.q;
}
传统的“调试”很难隔离悬空指针。 由于几个不同的原因,它们的可复制性很差:
- 即使影响过早释放的内存范围的代码已本地化,内存的使用也可能取决于应用程序中其他位置的执行情况,或者在极端情况下甚至取决于不同的进程。
- 悬挂指针很可能出现在代码中,该代码以微妙的方式使用内存。 结果是,即使在释放时立即覆盖了内存,并且新的指向值与预期值不同,也可能很难将新值识别为错误值。
悬空指针始终威胁着C或C ++程序的健康。
数组界限违规
违反数组边界根本不安全,这是内存管理不善的最后主要类别。 回头看看清单1 ; 如果explanation
的长度超过80,会发生什么? 答:很难预测,但可能远非如此。 更具体地说,C复制了一个字符串,该字符串不适合为其分配的100个字符。 在任何常见的实现中,“多余”字符都会覆盖内存中的其他数据。 内存中数据分配的布局非常复杂且难以复制,因此任何症状可能都很难在源代码级别上与特定错误联系起来。 这些错误经常会导致数百万美元的损失。
存储器编程策略
勤奋和纪律可以将这些错误的发生率降低到几乎为零。 让我们看一下您可以采取的几个具体步骤; 我在各种组织中使用它们的经验是,它们始终将内存错误减少至少一个数量级。
编码风格
最重要的,也是我从未见过的任何其他作者都强调过的一个编码标准。 影响资源(尤其是内存)的功能和方法需要明确说明。 以下是相关标题,注释或名称的示例(请参见清单6 )。
清单6.资源感知源代码示例
/********
* ...
*
* Note that any function invoking protected_file_read()
* assumes responsibility eventually to fclose() its
* return value, UNLESS that value is NULL.
*
********/
FILE *protected_file_read(char *filename)
{
FILE *fp;
fp = fopen(filename, "r");
if (fp) {
...
} else {
...
}
return fp;
}
/*******
* ...
*
* Note that the return value of get_message points to a
* fixed memory location. Do NOT free() it; remember to
* make a copy if it must be retained ...
*
********/
char *get_message()
{
static char this_buffer[400];
...
(void) sprintf(this_buffer, ...);
return this_buffer;
}
/********
* ...
* While this function uses heap memory, and so
* temporarily might expand the over-all memory
* footprint, it properly cleans up after itself.
*
********/
int f6(char *item1)
{
my_class c1;
int result;
...
c1 = new my_class(item1);
...
result = c1.x;
delete c1;
return result;
}
/********
* ...
* Note that f8() is documented to return a value
* which needs to be returned to heap; as f7 thinly
* wraps f8, any code which invokes f7() must be
* careful to free() the return value.
*
********/
int *f7()
{
int *p;
p = f8(...);
...
return p;
}
将这些样式元素作为例程的一部分。 有各种各样的解决内存问题的方法:
- 专用库
- 语言能力
- 软件工具
- 硬件检查器
在整个领域中,我一直认为最有用的一步,就是投资的最大回报是对源代码样式的深思熟虑的改进。 它不需要昂贵或严格的形式; 与内存无关的部分可以像往常一样不加注释,并且对内存有影响的定义肯定值得明确注释。 只需输入几个简单的词就可以清楚地说明内存的后果,从而改善您的内存编程。
我尚未进行对照实验来验证这种风格的效果。 如果您的经历像我的经历,您会发现自己不想在没有评论资源影响的政策的情况下生活。 这样做仅仅是回报太高。
检查
编码标准的补充是检查。 两种方法都可以单独提供帮助,但是它们在合作中特别有效。 机敏的C或C ++实践者甚至可以扫描不熟悉的源代码,并以非常低的成本检测内存问题。 通过少量练习和适当的文本搜索,您可以快速开发一种功能来验证平衡的*alloc()
和free()
或new
和delete
源语料库。 此类人工检查通常会引起清单7中所示的问题。
清单7.麻烦的内存泄漏
static char *important_pointer = NULL;
void f9()
{
if (!important_pointer)
important_pointer = malloc(IMPORTANT_SIZE);
...
if (condition)
/* Ooops! We just lost the reference
important_pointer already held. */
important_pointer = malloc(DIFFERENT_SIZE);
...
}
浅表使用的自动运行时工具不检测,如果情况发生内存泄漏condition
是真实的。 仔细的来源分析可以通过这些条件进行推理,以证明结论正确。 我重复我写的关于样式的文章:尽管大多数已发布的有关内存问题的描述都强调工具和语言,但对我而言,最大的收获来自于以开发人员为中心的“软”过程更改。 您在样式和检查上所做的任何改进都可以帮助您了解自动工具产生的诊断信息。
静态自动语法分析
当然,并非只有人类可以阅读源代码。 您还应该将静态语法分析作为开发过程的一部分。 静态语法分析是lint
, 严格的编译和一些商业产品的工作:扫描源文本并发现编译器可以接受的项目,但这些项目很可能是错误的征兆。
期望使您的代码不掉毛 。 尽管lint
既旧又有限,但许多不喜欢它的程序员(或其更高级的后代)却犯了一个大错误。 通常,可以编写通过lint
的优质专业质量代码,而这样做通常会导致重大错误。 其中一些会影响内存的正确性。 与让客户率先识别内存错误的成本相比,即使是此类产品中最昂贵的许可费用的支付,也失去了吸引力。 清理您的源代码。 即使使用lint
标记的编码似乎可以为您提供现在想要的功能,也很有可能存在一种更干净的方法,该方法可以满足lint
,并且更加健壮和可移植。
记忆库
最后两种补救方法与前三种不同。 前者重量轻 ; 个人可以很容易地理解和实施它们。 另一方面,内存库和工具的许可费通常较高,因此开发人员需要更多的技巧和判断力。 有效使用库和工具的程序员是那些了解轻量级静态方法的人。 可用的库和工具令人印象深刻:作为一个整体,它们的质量很高。 但是,即使是最优秀的程序员,也可能会被意志坚定的程序员忽略掉内存管理的基本原理而挫败。 从我所看到的情况来看,孤立地工作的普通程序员只有在尝试利用内存库和工具时才会感到沮丧。
出于所有这些原因,我敦促C和C ++程序员首先查看他们自己的内存问题来源。 这样做之后,该考虑库了。
几个库可以编写看起来传统的C或C ++代码,并保证改进的内存管理。 乔纳森·巴特利特(Jonathan Bartlett)在2004年的developerWorks评论中描述了主要候选人,该评论可通过下面的“ 相关主题”部分获得。 库解决了许多不同的内存问题,很难直接比较它们。 该领域中的常见专栏包括垃圾收集 , 智能指针和智能容器 。 广义上讲,这些库可以自动执行更多的内存管理,从而使程序员产生的错误更少。
我对内存库有不同的感觉。 它们应该工作,但是在我所见项目中的成功却比预期的要差,尤其是在C方面。 对于这些令人失望的结果,我还没有很好的分析。 例如,性能应该与类似的手动内存管理一样好,但这是一个灰色区域-尤其是在垃圾收集库似乎使处理速度变慢的情况下。 我在该领域的工作中得出的最明确的结论是,C ++文化似乎比以C语言为中心的编码器组更好地接受智能指针。
记忆工具
开发团队推出严肃的基于C的应用程序时,需要运行时存储工具作为其开发策略的一部分。 已经描述的技术是有价值且必要的。 除非您亲自尝试过,否则难以获得可用的存储工具的质量和功能。
本简介仅侧重于基于软件的存储工具。 还存在硬件存储器调试器。 我认为仅在非常特殊的情况下才需要它们-通常是在与不支持其他工具的专用主机一起使用时。
软件存储工具市场包括专有工具,例如IBMRational®Purify,Electric Fence和其他开源工具。 在其他操作系统中,每个都可以与AIX一起很好地工作。
所有内存工具的工作方式大致相同:构建可执行文件的特殊版本(就像您可能在编译时使用-g
标志生成调试版本一样),练习应用程序并研究由该工具自动生成的报告。 考虑一个类似于清单8的程序。
清单8.示例错误
int main()
{
char p[5];
strcpy(p, "Hello, world.");
puts(p);
}
在许多环境中,该程序可以“运行”并编译,执行并在屏幕上显示“ Hello,world。\ n”。 使用内存工具运行同一应用程序会导致在第四行报告违反数组边界的情况。 要了解一个软件故障,即已将14个字符复制到一个保证只能容纳5个字符的空间中,这比从客户那里发现故障的症状要便宜得多。 那就是存储工具的贡献。
结论
作为成熟的C或C ++程序员,您认识到内存问题值得认真关注。 通过一些计划和实践,您可以想出一种可以控制内存危险的方法。 学习正确的内存使用模式,对可能发生的错误敏感,并使本文中介绍的技术成为您日常工作的一部分。 您可以开始从应用程序中消除症状,否则可能需要几天或几周的时间进行调试。
翻译自: https://www.ibm.com/developerworks/aix/library/au-memorytechniques.html