SoRoMan

人若无名,便可专心练剑。

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
接上一篇,继续记录。

读书笔记:Writing Solid Code (1):
http://www.cnblogs.com/soroman/archive/2007/08/06/845465.html


--------------------------
Chapter 3 Fortify Your Subsystem
增强/巩固你的子系统
--------------------------

Summary:
Assertions wait quietly until bugs show up. Even more powerful are subsystem integrity checks that actively
validate subsystems and alert you to bugs before the bugs affect the program. The integrity checks for the standard C memory manager can detect dangling pointers, lost memory blocks,and illegal use of memory that has not been initialized or that has already been released. Integrity checks can also be used to eliminate rare behavior,which is responsible for untested scenarios, and to force subsystem bugs to be reproducible so that they can be tracked down and fixed.

概述:
断言只能静静地等待直到bugs出现。然而更加强大的是能主动验证子系统并能在bugs影响系统之前做出警报的子系统完整性检查措施。
对于标准的C内存管理器完整性检查能够察觉:悬挂指针,丢失的内存块,以及对于未初始化的或者已经释放的内存的非法使用。完整性
检查也能够消除很少发生的行为(这些行为可能导致无法测试的情形),也能够强制子系统的bugs重现,从而可以追踪并修复它们。

【注】作者举了个设计C内存管理器子系统的例子,这个例子贯穿整本书。关于如何实现相对健壮的系统,有个好建议:在你写代码的时
候,问自己,别人会怎样误用这个系统?你怎样检测到这个问题?比如对于内存管理系统,使用者会做以下的事情:
1.allocate a block and use the uninitialized contents
2.free a block, but continue to reference the contents
3.call realloc to expand a block and when it moves continue ro reference the contents at the old location
4.allocate a block but "lose" it because the pointer isn't saved
5.read or write beyond the boundaries of block
6.failed to notice error conditions

Guidelines:


3.1.Eliminate random behavior. Force bugs to be reproducible.
消除随机行为。强制bugs能够重现。

【注】首先作者举了个内存管理系统的中的内存分配函数fNewMemory(类似于malloc)的例子,关于内存分配有两个地方需要注意:
1.分配0字节的内存块-没有意义
2.分配后的内存内容没有初始化

由前面的方针:要么消除未定义的行为,要么使用断言来抓住这种行为。按照此方针,问题1容易解决,加上Assert即可,但是2呢,怎
么保证分配后的内存中的内容合法?未定义的值造成未定义的行为,从而造成bug随机出现,这给fix造成很大的麻烦。所以消除未定义
的行为是很重要的。一种做法是填充固定值如0。但是这可能隐藏bug。如果一个结构体在申请内存后忘了初始化,但内存中的0很可能给
人一种已经初始化的感觉。较好的做法是在debug版本中填充固定值,在release版本不填充。这样既可以消除未定义(随机)行为
(debug下),又可以不隐藏bug(release 下)。而且填充的值有讲究,比如在intelx86机器上,用0xCC(软件中断指令)。在Mac机
器上,用0xA3(0xA3A3 #ff0000 A - Line trap),这样,在代码执行到这块内存的时候,会触发异常,返回到调试器或者在系统调
试器里得到一个"#ff0000 A-Line trap" 错误。

示例代码如下:
 1#define bGarbage 0xA3
 2fNewMemory(void **ppv, size_t size)
 3{
 4 Assert(ppv != NULL && size != 0)
 5 byte **ppb = (byte **)ppv;
 6
 7 *ppb = (byte *) malloc( size );
 8 #ifdef DEBUG
 9 {
10  if (*ppb != NULL)
11  mernset(*ppb, bGarbage, size ) ;
12 }

13 #endif
14 return (*ppb != NULL);
15}


3.2.Destroy your garbage so that it's not misused.
清除你的垃圾以防被误用。

【注】再看看内存释放函数fFreeMeomory的实现。
一个简单的实现如下:
void fFreeMemory(void *pv)
{
 free(pv);
}

问题1:根据ANSI C标准,如果你传入的是非法的指针,那么free的行为是未定义的。怎么判断指针合法?怎么保证pv指向the start
of an allocated block?答案是:不能。假设我们可以通过fValidPointer来验证pointer的合法。

问题2:如现在有个树,删除节点的时候会调用fFreeMemory。如果发生bug,造成删除节点的时候周围节点的指针没有更新,即你拥有了
指向已不属于这个指针的内存块的指针(dangling pointer)。显然,你就会有个包含free node的树。而且free node的内存的值还是
合法的,因为free不会填充free node的值(节省时间)。这时,就可能会造成bug。一种解决方案是释放内存的时候填充固定值,比如
0xCC,0xA3。但是你不知道指针所指内存的大小。所以无法实现。假设现在我们能通过sizeofBlock来获得指针所指内存块的大小。

一个可能的改进如下,记住释放内存的时候摧毁内存中的垃圾以防被误用:
 1void fFreeMemory(void *pv)
 2{
 3 ASSERT(fValidPointer(pv, sizeofBlock(pv)));
 4
 5 #ifdef DEBUG
 6 {
 7  memset(pv, bGarbage. sizeofBlock(pv)) ;
 8 }

 9 #endif
10
11 free(pv);
12}

3.3.If something happens rarely, force it to happen often.
如果某些事情极少发生,强制它经常发生。

【注】再看看内存重新分配函数fResizeMeomory的实现。
还是上面树结构的例子,现在需要扩展树节点以存放更多的数据。如果fResizeMeomory在扩充节点的时候移动了节点的起始地址的话,
那么你就会拥有两个节点了:一个在新的位置,一个在老的位置(其中的值不变)。如果使用子系统的人没有意识到fResizeMeomory会
移动节点,那么就会发生很多周围节点指向原先的已释放的内存块(dangling pointer),而新的内存块没有指针指向(memory leak)。
fResizeMemory如下:

 1flag fResizeMemory(void **ppv, size_t sizeNew)
 2{
 3 byte **ppb = ( byte **)ppv;
 4 byte *pbNew;
 5 #ifdef DEBUG
 6 size_t sizeOld;
 7 #endif
 8 ASSERT(ppb != NULL && sizeNew != 0 ) ;
 9
10 #ifdef DEBUG
11 {
12  sizeOld = sizeofBlock ( *ppb ) ;
13  /* If shrinking . erase the tail contents . */
14  if (sizeNew < sizeOld )
15   memset((*ppb)+sizeNew, bGarbage. sizeOld - sizeNew);
16 }

17 #endif
18
19 pbNew = ( byte* ) realloc ( * ppb , sizeNew);
20 if(pbNew != NULL)
21 {
22  #ifdef DEBUG
23  {
24  /* If expanding, initialize the new tail . */
25  if (sizeNew > sizeOld)
26   memset(pbNew+sizeOld. bGarbage, sizeNew - sizeOld ) :
27  }

28  #endif
29
30 *ppb = pbNew;
31 }

32 return (pbNew != NULL);
33}

一个可能的解决方法是如果内存块被移动了,就让fResizeMeomory清除原先的内存块中的垃圾。如下:

 1flag fResizeMemory(void **ppv, size_t sizeNew)
 2{
 3 
 4
 5 pbNew = (byte*)realloc( *ppb, size_t sizeNew)
 6 if (pbNew != NULL)
 7 {
 8  #ifdef DEBUG
 9  {
10   /* If the block moved, destroy the old one. */
11   if (pbNew != *ppb)
12   memset(*ppb, bGarbage, size0ld ) ;
13   /* If expanding, initialize the new tail . */
14   if(sizeNew > size0ld)
15   memset(pbNew+sizeOld, bGarbage, sizeNew-size0ld);
16  }

17  #endif
18
19  *ppb = pbNew;
20 }

21 return (pbNew != NULL);
22}

不幸的是,你不能这么做,因为你不再拥有已经释放的内存块,这块内存系统可能另有它用。
象这种类似情形很少发生,但不可忽视,说不定什么时候就会造成bug。怎么办?强制它不会发生,即先做检查,对需要移动内存块的情
形做特殊处理:先create一个新的内存块,然后拷贝内容,最后释放旧内存块。

3.4.Keep debug information to allow stronger error checking.
保留debug信息用作更强的错误检查

【注】从debug的角度去看内存管理器存在的问题,就是在创建的时候你知道内存块的大小,但是之后你就一无所知了,除非你在什么地
方记录了相关信息。实际上,前面提到的关于获得指针所指内存块的大小的方法sizeofBlock以及验证指针是否合法的fValidPointer方
法都可以借此实现。这样,在填充内存之前就可以先验证一下指针:

1void fFillMemory(void *pv, byte b, size_t size)
2{
3 ASSERT(fValidPointer(pv, size));
4 memset(pv, b, size);
5}

创建一个日志log记录内存信息的一个可行的办法是:在创建(fNewMemory),释放(fFreeMemory)和重分配内存(fResizeMemory)的
时候记录内存的变化情况.如下:

1/* Create a memory record for the new block. */
2flag fCreateBlockInfo(byte *pbNew. size_t sizeNew);
3/* Release the information stored about a block. */
4void FreeBlockInfo(byte *pb);
5/* Update the information about an existing block. */
6void UpdateBlockInfo(byte *pbOld, byte *pbNew, size_t sizeNew);

3.5.Create thorough subsystem checks,and use them often.
创建彻底的子系统检查方案,并且经常使用它们。

【注】主动地做检查,不要总是等到bugs出现了,才去采取措施。对于内存管理器子系统,你可以将存在数据结构中的指针列表和存放
在记录日志log中的分配的内存块信息比较。如果你发现有指针没有指向任何分配的块(dangling pointer)或者存在任何内存块没有
指针指向它(memory link),问题就被提早发现了。一个可能的实现如下:

1/* Mark all blocks as "unreferenced." */
2void ClearMemoryRefs(void
3/* Note that the block pointed to by "pv" has a reference. */
4void NoteMemoryRef (void *pv)
5/* Scan the reference flags looking for lost blocks. */
6void CheckMemoryRefs(void)

首先,调用ClearMemoryRef设置调试信息为一个已知的状态。然后,扫描你的全局数据结构并为每个指向分配内存块的指针调用
NoteMemoryRef ,从而验证指针以及对那块被引用的内存块做标记。一旦你完成这些,每个指针都已经被验证了并且每个内存块也有了
个引用标记。最终,调用CheckMemoryRefs来验证所有的内存块都被标记了。如果发现有一个未做标记的块,Assert就会触发,警告有
丢失的内存块。

什么时候调用检查取决于你自己,一般在使用这个子系统之前应该调用一次,更好的是在系统等待输入的时候调用。

3.6.Design your tests carefully.Nothing should be arbitrary.
仔细地设计你的测试。没有任何事情是可以任意而为的。

3.7.Strive to implement transparent integrity checks.
争取实现透明的完整性检查。

【注】争取做到这些检查不影响别人使用你的子系统,一切尽在幕后运行。

3.8.Don't apply ship version constraints to the debug version.
Trade size and speed for error detection.
不要把ship版本的那些约束套在debug版本上。牺牲空间和时间来换取错误侦测。

【注】做那么多额外的检查什么的肯定对空间以及时间有额外的开销,但是,这无关紧要,因为debug版本就是用来除错的,这些额外的
东西不会出现在ship版本中。





posted on 2007-12-22 20:24  SoRoMan  阅读(1225)  评论(1编辑  收藏  举报
free web counters
Vistaprint Discount Codes