一些良好的和内存相关的编码实践

来源:www.zhonghengtong.com 发布时间:2020-06-27

本文将向您展示一些良好的内存相关编码实践,以控制内存错误。内存错误是C和C编程的祸根:它们非常常见,并且已经被认识了20多年,但是还没有完全解决。它们可能会严重影响应用程序,很少有开发团队为它们制定明确的管理计划。但好消息是它们并不神秘。

介绍

C和C程序中的内存错误是非常有害的:它们很常见,会导致严重的后果。计算机应急响应团队(参见参考资料)和供应商发布的许多最严重的安全公告都是由简单的内存错误引起的。自20世纪70年代末以来,c程序员一直在讨论这种错误,但是直到今年它的影响仍然很大。更糟糕的是,如果我想一想,今天许多C和C程序员可能会认为内存错误是不可控制和神秘的顽疾,只能纠正而不能预防。

但事实并非如此。这篇文章将使你在短时间内理解与良好记忆相关的编码的所有本质:

正确的内存管理的重要性

带有内存错误的C和C程序会导致各种问题。如果它们泄漏内存,运行速度将逐渐减慢,最终停止运行。如果内存被覆盖,它将变得非常脆弱,容易受到恶意用户的攻击。从1988年著名的莫里斯蠕虫攻击到最近关于闪存播放器和其他关键零售程序的安全警报,缓冲区溢出都是相关的:“大多数计算机安全漏洞都是缓冲区溢出,”罗德尼贝茨在2004年写道。

在可以使用C或C的地方,还有许多其他的通用语言(比如Java?Ruby、Haskell、C#、Perl、Smalltalk等。),每种语言都有许多粉丝和自己的优势。然而,从计算的角度来看,每种编程语言相对于C或C的主要优势都与内存管理的便利性密切相关。与内存相关的编程非常重要,在实践中很难正确应用,以至于它主宰了面向对象编程语言、函数式编程语言、高级编程语言、声明式编程语言和其他编程语言的所有其他变量或理论。

像一些其他类型的常见错误一样,内存错误也是一个隐藏的危险:它们很难重现,并且通常在相应的源代码中找不到症状。例如,无论何时何地发生内存泄漏,应用程序都可能完全无法接受,并且内存泄漏并不明显。

因此,出于所有这些原因,应该特别注意C和C编程的内存问题。让我们看看如何解决这些问题,更不用说是哪种语言了。

记忆错误的类别

?1.内存泄漏2。分配不当,包括释放的空闲内存和未初始化的引用大量增加。暂停指针4。数组边界违规

这是所有类型。即使您迁移到C面向对象语言,这些类型也不会发生显著变化。无论数据是简单的C语言类型或结构,还是C语言类,C语言和C语言中的内存管理和引用模型原则上是相同的。下面的内容大部分是“纯C”语言,主要是在扩展到C语言时留给实践使用的.

内存泄漏

分配资源时会发生内存泄漏,但不会被回收。下面是一个可能出错的模型(见清单1):

?清单1。简单的潜在堆内存丢失和缓冲区覆盖

你看到问题了吗?除非local_log()对free()释放的内存有异常的响应,否则对f1的每次调用都会泄漏100字节。当记忆棒递增地分配几兆字节的内存时,一个漏洞是微不足道的,但是即使是这样一个小漏洞也会在几个小时的连续操作后削弱应用程序。

在实际的C和C编程中,这不足以影响您对malloc()或new的使用。本节开头的句子指的是“资源”,而不仅仅是“内存”,因为有类似于下面的例子(见清单2)。文件句柄可能不同于内存块,但它们必须得到同等重视:

清单2。资源错误管理可能导致堆内存丢失?

Fopen的语义需要互补的fclose。在没有fclose()的情况下,当C标准不能指定发生什么时,很可能是内存泄漏。其他资源(如信号量、网络句柄、数据库连接等。)也值得考虑。

内存分配错误

分配不当的管理并不十分困难。下面是一个例子(见清单3):

清单3。未初始化的指针

以下是参考片段:

void f2(int基准){ int * p2* p2=基准;}

关于这些错误的好消息是,它们通常会产生显著的结果。在AIX下,未初始化指针的分配通常会立即导致分段错误。它的优点是任何这样的错误都会被很快发现。与需要数月才能确定且难以重现的错误相比,检测此类错误的成本要低得多。

此错误类型有多种变体。Free()比malloc()释放更多的内存(参见清单4):

?清单4。两次错误的内存释放

以下是参考片段:

void F3(){ char * p;p=malloc(10);免费(p);免费(p);} void F4(){ char * p;免费(p);}

这些错误通常不太严重。虽然C标准没有定义这些情况下的特定行为,但是典型的实现会忽略错误或者快速清晰地标记它们。简而言之,这些都是安全情况。悬浮指针

悬挂指针很棘手。当程序员在释放内存资源后使用资源时,会出现挂起的指针(参见清单5):

?清单5。悬浮指针

以下是参考片段:

void F8(){ struct x * XP;XP=(struct x *)malloc(size of(struct x));xp.q=13.免费(XP);xp.q}

传统的“调试”很难隔离浮动指针。它们难以繁殖有两个明显的原因:

即使影响内存范围早期发布的代码是本地化的,内存的使用可能仍然取决于应用程序,甚至(在极端情况下)不同进程中的其他执行位置。

挂起的指针可能出现在以微妙方式使用内存的代码中。结果,即使存储器在被释放后立即被重写,并且新指向的值不同于预期值,也难以识别新值是错误值。挂起的指针不断威胁着C或C程序的运行状态。

数组边界冲突

违反阵列边界非常危险,是内存错误管理的最后一个主要类别。回头看看清单1。如果解释的长度超过80,会发生什么?回答:很难预测,但可能远非好事。特别是,C复制一个不适合分配给它的100个字符的字符串。在任何常规实现中,“超出”字符会覆盖内存中的其他数据。内存中数据分配的布局非常复杂且难以重现,因此任何症状都无法追溯到源代码级别的特定错误。这些错误通常会导致数百万美元的损失。

记忆编程策略

勤奋和自律可以将这些错误的影响降到最低。这里有一些你可以采取的具体步骤。我在各种组织中处理这些问题的经验是,记忆错误至少可以减少一定数量级。

编程风格

编码风格是最重要的,我从未见过其他作者强调过。需要明确解释影响资源(尤其是内存)的函数和方法。这里有一些标题、注释或名称的例子(见清单6)。

清单6。识别资源的源代码示例

以下是参考片段:

FILE *protected_file_read(字符*文件名){ FILE * fpfp=fopen(文件名,“r”);if (fp) {.}其他{.}返回FP;} char *get_message(){静态char this _ buffer[400];sprintf(this_buffer,);返回此缓冲区;} int F6(char * item 1){ my _ class C1;int结果;c1=新my _ class(item 1);结果=c1.x删除C1;返回结果;} int * F7(){ int * p;p=f8(.);返回p;}

特殊的图书馆语言软件工具

硬件检查在整个领域,我总是认为最有用和最大的投资回报是考虑改进源代码的风格。它不需要昂贵的价格或严格的形式;您总是可以取消与内存无关的段的注释,但是影响内存的定义当然需要显式注释。添加几个简单的单词可以使记忆结果更清晰,并且可以改进记忆编程。

我没有做对照实验来验证这种风格的效果。如果你的经历和我的一样,你会发现那些不能解释资源影响的策略简直无法忍受。这很简单,但是它带来了太多的好处。侦查

检测是编码标准的补充。两者都是有益的,但联合使用尤其有效。聪明的C或C专业人员甚至可以以非常低的成本浏览不熟悉的源代码并检测内存问题。通过少量的练习和适当的文本搜索,您可以快速验证平衡的*alloc()和free()或新的和删除的源体。手动查看这些内容通常会导致与清单7中相同的问题。

清单7。困难的内存泄漏

以下是参考片段:

静态字符*重要指针=空;void f9(){if(!重要指针=malloc(IMPORTANT _ 件)重要_指针=malloc(不同_大小);}

如果一个推理得出的结论被证明是正确的。让我重复一下我写的关于风格的内容:尽管大量公开发表的内存问题描述强调工具和语言,但对我来说,最大的收获来自于“软”的以开发人员为中心的过程变化。您在风格和测试方面所做的任何改进都可以帮助您理解由自动化工具生成的诊断。

静态自动语法分析

当然,不仅人类可以阅读源代码。您还应该将静态语法分析作为开发过程的一部分。静态语法分析是lint的内容,严格编译和执行几种商业产品:扫描编译器接受的源文本和目标项,但这可能是错误的征兆。

希望您的代码不会掉毛。尽管lint已经过时并且有一定的局限性,但是许多没有使用它的程序员(或者它的高级后代)都犯了很大的错误。总的来说,你可以编写优秀的专业质量代码,忽略掉lint,但是这样做经常会导致严重的错误。其中一些错误会影响内存的正确性。与让客户首先发现内存错误的成本相比,即使为这类产品支付最昂贵的许可费也毫无意义。清除源代码。现在,尽管lint标签的编码可能会为您提供所需的功能,但可能有一种更简单的方法可以满足lint的要求,并且具有更强的可移植性。

?记忆库

后两类补救措施与前三类有很大不同。前者重量轻;人们很容易理解和认识它们。另一方面,内存库和工具通常有很高的许可费,对于一些开发人员来说,它们需要进一步改进和调整。有效使用库和工具的程序员是理解轻量级静态方法的人。可用的库和工具令人印象深刻:它们作为一个群体的质量非常高。然而,即使是最好的程序员也可能会被那些无视内存管理基本原则的任性程序员所困扰。根据我的观察,普通程序员只有在尝试使用内存库和工具进行隔离工作时才会感到气馁。

出于这些原因,我们敦促C和C程序员在解决内存问题之前了解自己的源代码。只有在此完成后,该库才会被考虑。

使用几个库可以编写常规的C或C代码,并确保改进的内存管理。Jonathan Bartlett在developerWorks的2004年评论专栏中介绍了主要候选人,可以在下面的参考资料部分找到。库可以解决许多不同的内存问题,所以很难直接比较它们。该领域的常见主题包括垃圾收集、智能指针和智能容器。一般来说,库可以自动管理更多的内存,所以程序员可以少犯错误。

我对记忆库有各种各样的感觉。他们正在努力工作,但我看到他们在项目中的成功没有预期的那么多,尤其是在C区。我还没有仔细分析这些令人失望的结果。例如,性能应该与相应的手动内存管理一样好,但这是一个灰色区域,尤其是当垃圾收集库处理速度较慢时。从这个实践中得出的最明确的结论是,与C所关注的代码组相比,C似乎更好地接受了智能指针。

?记忆工具

开发真正基于C的应用程序的开发团队需要运行时内存工具作为他们开发策略的一部分。引进的技术是有价值的,不可或缺的。你可能不知道记忆工具的质量和功能,直到你亲自尝试使用它。

本文主要讨论基于软件的内存工具。还有一个硬件内存调试器。只有在非常特殊的情况下才会考虑它们(主要是在使用不支持其他工具的专用主机时)。

市场上的软件内存工具包括专有工具(如小发猫 Rational Purify和Electric Fence)和其他开源工具。其中许多可以很好地与AIX和其他操作系统一起使用。

所有内存工具的功能基本上是相同的:构建可执行文件的特定版本(非常类似于编译时使用-g标记生成的调试版本),练习相关的应用程序,以及研究工具自动生成的报告。考虑清单8所示的过程。

清单8。样本误差

以下是参考片段:

int main(){ char p[5];“你好,世界”);puts(p);}

这个程序可以在许多环境中“运行”。它编译、执行并将“你好,世界网”打印到屏幕上。使用内存工具运行相同的应用程序将在第四行生成一个数组边界违规报告。就理解软件错误而言(将14个字符复制到一个只能容纳5个字符的空间),这种方法比在客户处寻找错误症状的成本低得多。这是由于记忆工具。结束语

作为一名成熟的C或C程序员,你会意识到内存问题值得特别关注。通过制定一些计划和实践,我们可以找到控制记忆错误的方法。学习正确的内存使用模式,快速发现可能的错误,并将本文中描述的技术融入到您的日常工作中。您可以在一开始就消除应用程序中的症状,否则可能需要几天或几周的时间来调试。