文章目录
第三章 为子系统设防
一个实际的工程往往是有许多的模块组成的,既对应于作者这里说的子系统;比如文件操作相关的文件管理子系统,涉及到文件的打开、关闭、读写和创建。又如内存管理的模块,涉及到内存分配和释放等等。
通常,子系统都要对其实现细节进行隐藏,所隐藏的实现细节可能相当复杂。当子系统编写完成之后,要问自己:“程序员什么情况下会错误地使用这个子系统,在这个子系统中怎样才能自动地检查出这些问题?”在正常情况下,当开始编码排除设计中的危险因素时就应该问过了这个问题。但不管怎样,还应该再问一次。
上面摘抄的是书的原文,注意这里的自动地检查出问题这一点,另外需要强调的是,这本书写于1992年,当时的编译器还没这么强大,现在的编译器已经讲这些问题帮我们解决了,比如非法的指针访问(例如我们使用C++的vector时,如果索引值超过数组边界,编译器会报错,从而提醒我们程序编写地有问题)。
C的内存管理过程中会出现下面的问题,这一章节根据下面的问题进行范例教学:
- 分配一个内存块并使用其中未经初始化的内容;
- 释放一个内存块但继续引用其中的内容;
- 调用 realloc 对一个内存块进行扩展,因此原来的内容发生了存储位置的变化,但程序引用的仍是原来存储位置的内容;
- 分配一个内存块后即“失去”了它,因为没有保存指向所分配内存块的指针;
- 读写操作越过了所分配内存块的边界;
- 没有对错误情况进行检查。
1.若隐若现,时有时无
这里使用malloc来举例子
/* fNewMemory ─── 分配一个内存块 */
flag fNewMemory(void** ppv, size_t size)
{
byte** ppb = (byte**)ppv;
*ppb = (byte*)malloc(size);
return(*ppb != NULL); /* 成功 */
}
问题:
如果 malloc 分配成功,那么它返回的内存块的内容无定义,内容随机(PS: C中malloc后一般都会有一个初始化操作)
针对这个问题:
- 第二章说到的断言能够解决吗?显然不能
- 用零填充(增加了交付程序的负担,隐瞒错误)
- 不填充(可能有随机错误,难以发现)
给出的解决办法:
调试版本中填充特点的值(不增加程序负担,消除随机性,暴露错误)
下面是书中给出的解决方案代码
#define bGarbage 0xA3
// 不同的机器填充内容不同,填写容易出错的值,对于 Macintosh 程序,可以使用值 0xA3
flag fNewMemory(void** ppv, size_t size)
{
byte** ppb = (byte**)ppv;
ASSERT(ppv!=NULL && size!=0);
*ppb = (byte*)malloc(size);
#ifdef DEBUG
{
if( *ppb != NULL )
memset(*ppb, bGarbage, size); //填充 *************************就是这一部分的代码
}
#endif
return(*ppb != NULL);
}
说明:这本书写于1992年,当时作者开发使用的系统是Macintosh
在一些 Macintosh 机上,用户使用奇数的指针不能引用 16 或 32 位的值。由此可知,新选择的填充值应该是奇数。另外,如果非法的计数器或索引值较大。就会引起明显的延迟, 或者会使系统的行为显得不正常,从而增大发现这类错误的可能性。因此,所选择的填充值应该是用一个字节能够表示的、看起来很奇怪的较大奇数。我选择 0xA3 不仅因为它能够满足上述的要求,而且因为它还是一条非法的机器语言指令。
而现在已经是2021年了,现在的编译器已经帮助我们做了这样一个工作,下面是VS studio中进行地一个测试
可以看到分配内存的时候填充了cd,而释放这块内存后填充了dd,下面给出
Visual Studio C++中内存分配的各个填充字符的意义:参考链接
- 0xABABABAB : Used by Microsoft’s HeapAlloc() to mark “no man’s land” guard bytes after allocated heap memory
- 0xABADCAFE : A startup to th