一 前言
本文所讨论的“内存”主要指(静态)数据区、堆区和栈区空间(详细的布局和描述参考《Linux虚拟地址空间布局》一文),
C语言内存使用的常见问题及解决之道
。数据区内存在程序编译时分配,该内存的生存期为程序的整个运行期间,如全局变量和static关键字所声明的静态变量。函数执行时在栈上开辟局部自动变量的储存空间,执行结束时自动释放栈区内存。堆区内存亦称动态内存,由程序在运行时调用malloc/calloc/realloc等库函数申请,并由使用者显式地调用free库函数释放。堆内存比栈内存分配容量更大,生存期由使用者决定,故非常灵活。然而,堆内存使用时很容易出现内存泄露、内存越界和重复释放等严重问题。二 内存问题
2.1 数据区内存
2.1.1 内存越界
内存越界访问分为读越界和写越界。读越界表示读取不属于自己的数据,如读取的字节数多于分配给目标变量的字节数。若所读的内存地址无效,则程序立即崩溃;若所读的内存地址有效,则可读到随机的数据,导致不可预料的后果。写越界亦称“缓冲区溢出”,所写入的数据对目标地址而言也是随机的,因此同样导致不可预料的后果。
内存越界访问会严重影响程序的稳定性,其危险在于后果和症状的随机性。这种随机性使得故障现象和本源看似无关,给排障带来极大的困难。
数据区内存越界主要指读写某一数据区内存(如全局或静态变量、数组或结构体等)时,超出该内存区域的合法范围。
写越界的主要原因有两种:1) memset/memcpy/memmove等内存覆写调用;2) 数组下标超出范围。
复制代码
1 #define NAME_SIZE 5
2 #define NAME_LEN NAME_SIZE-1/*Terminator*/
3 char gszName[NAME_SIZE] = "Mike";
4 char *pszName = "Jason";
5 int main(void)
6 {
7 memset(gszName, 0, NAME_SIZE+1); //越界1
8 gszName[NAME_SIZE] = 0; //越界2
9
10 if(strlen(pszName) <= NAME_SIZE) //越界3(注意'='号)
11 strcpy(gszName, pszName);
12
13 int dwSrcLen = strlen(pszName);
14 if(dwSrcLen < NAME_SIZE)
15 memcpy(gszName, pszName, dwSrcLen); //未拷贝结束符('\0')
16
17 return 0;
18 }
复制代码
使用数组时,经常发生下标“多1”或“少1”的操作,特别是当下标用于for循环条件表达式时。此外,当数组下标由函数参数传入或经过复杂运算时,更易发生越界。
复制代码
1 void ModifyNameChar(unsigned char ucCharIdx, char cModChar)
2 {
3 gszName[ucCharIdx] = cModChar; //写越界
4 }
5 int main(void)
6 {
7 ModifyNameChar(5, 'L');
8 unsigned char ucIdx = 0;
9 for(; ucIdx <= NAME_SIZE; ucIdx++) //'='号导致读越界
10 printf("NameChar = %c\n", gszName[ucIdx]);
11
12 return 0;
13 }
复制代码
对于重要的全局数据,可将其植入结构体内并添加CHK_HEAD和CHK_TAIL进行越界保护和检查:
复制代码
1 #define CODE_SIZE 4 //越界保护码的字节数
2 #if (1 == CODE_SIZE)
3 #define CODE_TYPE char
4 #define CHK_CODE 0xCC //除0外的特殊值
5 #elif (2 == CODE_SIZE)
6 #define CODE_TYPE short
7 #define CHK_CODE 0xCDDC //除0外的特殊值
8 #else
9 #define CODE_TYPE int
10 #define CHK_CODE 0xABCDDCBA //除0外的特殊值
11 #endif
12 #define CHK_HEAD CODE_TYPE ChkHead;
13 #define CHK_TAIL CODE_TYPE ChkTail;
14 #define INIT_CHECK(ptChkMem) do{ \
15 (ptChkMem)->ChkHead = CHK_CODE; \
16 (ptChkMem)->ChkTail = CHK_CODE; \
17 }while(0)
18 #define CHK_OVERRUN(ptChkMem) do{ \
19 if((ptChkMem)->ChkHead != CHK_CODE || (ptChkMem)->ChkTail != CHK_CODE) { \
20 printf("[%s(%d)<%s>]Memory Overrun(ChkHead:0x%X,ChkTail:0x%X)!\n", __FILE__, __LINE__, FUNC_NAME, \
21 (ptChkMem)->ChkHead, (ptChkMem)->ChkTail); \
22 } \
23 }while(0)
24 typedef struct{
25 CHK_HEAD;
26 char szName[NAME_SIZE];
27 CHK_TAIL;
28 }T_CHK_MEM;
29 T_CHK_MEM gtChkMem;
30 int main(void)
31 {
32 memset(>ChkMem, 0, sizeof(T_CHK_MEM));
33 INIT_CHECK(>ChkMem);
34
35 memset(>ChkMem, 11, 6);
36 CHK_OVERRUN(>ChkMem);
37 strcpy(gtChkMem.szName, "Elizabeth");