给你的模块设防
我们设计任何一个模块,都应当对其实现细节尽可能的隐藏。只留下有限的入口和外部通讯。这些入口如何定义是重中之重。大多数情况下,我们都在模仿已有的系统来设计,所以对貌似理所当然的接口定义不以为然,以为天生就应该是那样,而把过多精力放在了如何做更好(更优化)的实现上。对接口设计方面缺乏深度的思考,使得在面对新领域时,或是随心所欲,或是不知所措。
即使是有成熟设计的模块,用户依然可能使用错误。模块的设计者不能要求模块的使用者对其内部实现了然于胸。不能指望自己能写出完善的文档去告诫使用者,当你怎样用的时候会用错。即使写出了完善的文档,也不能指望每个人都仔细的读过。就算读了,也有百密一疏的时候。越是专有的模块,越不能指望文档或是口述的教导,更不能指望程序员去精读源码。人生苦短,如无特别的理由,逐行去读同僚的代码,何不自己重写一遍?你设计的系统若用出了问题,与其怪别人用错,不如怪自己没设计好。MSDN 洋洋洒洒文字以 G 计算,也属无奈之举。依然有无数人犯下那些被人提及的错误。
现举一个一切 C 语言程序员都用过的模块的设计:内存管理模块。
标准 API 为三个:
malloc
free
realloc
大多数程序员都以为这理所当然。直到接触到 gc 的方式管理内存,方知另有一片天地。即使在 C 库中引为标准,也并不是所有内存管理器都承认这种简洁的。比如在 Windows 的 API 中,HeapAlloc 系列的内存管理模块的 API 就更复杂一些。
当我们考察 malloc 这组 API ,我们会发现其实它并不是绝对可靠的。稍有经验的 C 程序员就会指出:我们不能给 free 传入随意的指针。一个典型的错误就是对一个由 malloc 分配出的地址,调用 free 两次。
次要一点的一个问题是,分配了一块内存,但丢失了地址指针。导致了无法释放内存。
还有一些问题,比如分配了一个内存块,没有初始化内容,而直接引用了里面不可预知的数据;或是释放了一块内存,但依旧引用里面的数据。读写一个内存地址时,超过了分配时的边界;没有检查分配失败的情况;realloc 扩展内存后,地址发生变化,但有地方依旧引用老的地址。
为了防备这些问题,最常用的方法不是反复告戒程序员规避这些错误,而是给模块本身设防。让程序员犯错误时及早发现。当然,如果条件允许,应该更谨慎的定义模块接口,甚至在发现设计问题后,重新设计。
内存管理模块,很多情况下是由第三方提供的。我们不太会去研究其源代码并做出修改。即使由自己开发,也希望其足够独立,在软件的发行版中提供最高的性能(而不因为过多的错误检查而拖慢系统)。
给 api 加个壳是个很不错的方案。
malloc ,分配一块内存,返回这块内存的内存地址。按照 malloc 的 ISO 标准,当分配失败的时候,返回空指针。当试图请求分配 0 字节长的内存块时,返回值依赖实现:可以返回空指针,或是返回一个有效内存地址。返回的内存块里的数据未定义。
根据我们自己项目的事情情况,以及开发团队的开发规范。我们可以加强 api 的定义。对于一些无定义的特性,应该尽量去除,保证其不会被用到(使用断言)。以我个人的经验,我所开发的软件中,软件使用的内存总量可以被预估出来,不可能超过系统能提供的内存上限。这时,我不需要 malloc 对分配失败返回空指针。一旦出现这种情况,一定是我错误的传递了请求的长度值。这时,我会在自定义的壳中检查返回值,并断言它一定不为空指针。
注:每次内存分配都成功的断言,不一定在每个项目中都成立。有些情况下,允许内存分配失败,并判断返回失败情况还是有必要的。
对于分配零字节长度内存的情况,我倾向于统一使用一致的设定,即返回一个唯一有效的内存地址,让 malloc(0) 等价于 malloc(1) 。这可以很好的保持于 C++ 的 new 行为一致。( C++ 中,使用 operator new 时,若申请的长度为 0 ,按标准所定,是不会返回空指针的)
对于内存块的内容,我倾向于做一次填充。这可以减少出错的随机性。但不要填充成 0 。这极有可能掩盖掉没有正确初始化内存的问题。在 x86 的机器下,可以考虑填充 0xCC 。0xCC 在 IA32 指令集中是 int3 中断的机器码。万一函数指针错误跳转到数据区,可以自动触发一次调试中断。而且 0xCCCCCCCC 在大多数操作系统下,是个无效地址。
free ,释放一块由 malloc 或 realloc 分配的内存。按标准,free 可以接受空指针,并不做任何事情。这一点在自己设计内存管理器时,一定需要遵循。free 后的内存块的数据处于未定义状态。如果我们给 free 加上壳的话,我的个人建议是对 free 后的内存块做数据填充。这样可以帮助提前发现程序中引用以释放的内存。
realloc ,改变已分配的内存块的大小。如果内存块无法在原地址扩展,在搬移到新地址上时,realloc 会复制原有的数据。根据标准的定义,当 realloc 扩展内存失败时,返回空指针,并保留由有的内存块。这反而会导致程序员的一些疏忽,在某些对内存管理比较苛刻的库设计中,让内存泄露。(忘记释放 realloc 扩展失败后的原地址)有些系统(例如 freeBSD)提供了另一个相似的 api ,reallocf 来回避这个问题。
所幸,很多软件中,我们可以按上面 malloc 的处理方案来规避这个问题。扩展内存失败往往意味着别的地方出了 bug 。在壳中断言 realloc 一定成功即可。
如何找到多次 free 同一块内存,如何找到内存的写越界 ?虽没有万无一失的方案,但我们可以在壳中多做一些工作来尽可能的提前发现问题。
典型的方法是给分配出来的内存块前后加上狗牌。例如:
如何检测内存泄露?即申请的内存块没有释放。
这个问题应该分类讨论。
以我个人的习惯,有些内存块在整个软件的生命期中,只会被申请一次,而随程序退出而由系统回收。主动调用 free 是一种额外的负担,更容易引起错误。这种做法,在 C++ 程序中是不推荐的。但 C++ 软件的诸多常见 bug 和难以解决的问题,正在于单件的构造析构次序。但在基于 C 的软件中,这类做法却很常见。
我的个人喜好是,增加一个 api 专门用来分配不会动态释放的内存。这类内存打上特殊的 tag ,一旦 free 它就会断言出错。
而另一些动态管理的内存,则双向链表链起来。软件退出的时,检查这个链表。很多人在 MFC 的框架中已经见识了这个做法,这里不再列出详细的代码。
如何给已有的内存管理器加壳?
有些 C 库,比如 GNU C 库,提供了钩子。设置这些钩子可以加上壳帮助你调试代码。如果你用的 C 库没提供这个能力,通常的解决方案是在头文件中使用宏替换。如果是 C++ 代码,可以重定义全局的 new / delete 运算符。在 MFC 中,采用的就是宏替换 new 和 delete 运算符的作法。这个做法是为了记录使用这些运算符的源代码位置。但这种做法有一些弊病,下面我会推荐一个在基于 C 开发的软件中更灵活的方案。
首先说说直接宏替换,可能存在的问题:
#define malloc(sz) malloc_proxy(sz)
这样做在大多数情况下都是有效的。但当我们想把 malloc 作为函数指针传递时会有些麻烦。其实我们可以采用另一个方案。
如果我们希望在调用 malloc 时,记录调用的源代码位置,并记录在 cookie 中,或是断言出错的时候输出到标准错误设备中。可以这样。
这里采用了全局变量来记录源码信息,而没有通过参数传递。在多线程环境可能会有一点小问题,但是无伤大雅。也容易回避。自定义的壳从全局变量去获得源代码信息,也更容易保持接口一致。
接下来我们只需要扩展 cookie 结构,来提供更多调试信息。
注:在设计 cookie 时,可以考虑把 cookie 前面第一项放无关紧要的信息,或空出来。至少不要把狗牌放在最前面。这是因为,free 对内存块的破坏是不可预知的。如果想检测出多同一内存的多次释放,需要对内存管理器有一定的了解,尽量回避 free 对内存内容的改写。大多数高效的内存管理模块都采用了 freelist 技术。这种技术会利用已释放的内存块的开头区域存放 freelist 指针。如果你把狗牌设在这个位置,可能会被改写掉。
Comments
Posted by: Anonymous | (21) May 11, 2013 02:35 AM
Posted by: zuhd | (20) May 22, 2010 02:09 PM
Posted by: cloud | (19) May 14, 2010 01:24 PM
Posted by: mickalpeng | (18) May 14, 2010 10:16 AM
Posted by: wendy | (17) May 12, 2010 12:14 PM
Posted by: 粉丝淘 | (16) May 11, 2010 12:22 PM
Posted by: Anonymous | (15) May 11, 2010 12:00 AM
Posted by: houapple | (14) May 10, 2010 09:37 PM
Posted by: Cloud | (13) May 9, 2010 03:32 AM
Posted by: Raymond | (12) May 8, 2010 04:50 PM
Posted by: abia | (11) May 8, 2010 12:59 PM
Posted by: noopy | (10) May 8, 2010 09:21 AM
Posted by: leesoft | (9) May 8, 2010 05:32 AM
Posted by: Cloud | (8) May 8, 2010 03:24 AM
Posted by: leesoft | (7) May 8, 2010 02:41 AM
Posted by: Davy.xu | (6) May 7, 2010 10:18 PM
Posted by: 雷勇 | (5) May 7, 2010 09:15 PM
Posted by: Anonymous | (4) May 7, 2010 08:17 PM
Posted by: Anonymous | (3) May 7, 2010 05:49 PM
Posted by: devoc | (2) May 7, 2010 05:38 PM
Posted by: 池中物 | (1) May 7, 2010 05:25 PM