【转载】C语言内存使用的常见问题及解决之道

一  前言

    本文所讨论的“内存”主要指(静态)数据区、堆区和栈区空间(详细的布局和描述参考《Linux虚拟地址空间布局》一文)。数据区内存在程序编译时分配,该内存的生存期为程序的整个运行期间,如全局变量和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(NAME_SIZE, '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(&gtChkMem, 0, sizeof(T_CHK_MEM));
33     INIT_CHECK(&gtChkMem);
34     
35     memset(&gtChkMem, 11, 6);
36     CHK_OVERRUN(&gtChkMem);
37     strcpy(gtChkMem.szName, "Elizabeth");
38     CHK_OVERRUN(&gtChkMem);
39 
40     return 0;
41 }

 

     执行结果如下,可见被检查的szName数组其头尾地址均发生越界:

1 [test.c(177)<main>]Memory Overrun(dwChkHead:0xB0B0B0B,dwChkTail:0xABCDDCBA)!
2 [test.c(179)<main>]Memory Overrun(dwChkHead:0xB0B0B0B,dwChkTail:0xABCD0068)!

     若模块提供有全局数据的访问函数,则可将越界检查置于访问函数内:

 

 1 #ifdef CHK_GLOBAL_OVERRUN
 2     #define CODE_SIZE       4  //越界保护码的字节数
 3     #if (1 == CODE_SIZE)
 4         #define CODE_TYPE   char
 5         #define CHK_CODE    (CODE_TYPE)0xCC       //除0外的特殊值
 6     #elif (2 == CODE_SIZE)
 7         #define CODE_TYPE   short
 8         #define CHK_CODE    (CODE_TYPE)0xCDDC     //除0外的特殊值
 9     #else
10         #define CODE_TYPE   int
11         #define CHK_CODE    (CODE_TYPE)0xABCDDCBA //除0外的特殊值
12     #endif
13     #define CHK_HEAD        CODE_TYPE ChkHead
14     #define CHK_TAIL        CODE_TYPE ChkTail
15     #define HEAD_VAL(pvGlblAddr)             (*(CODE_TYPE*)(pvGlblAddr))
16     #define TAIL_VAL(pvGlblAddr, dwGlbSize)  (*(CODE_TYPE*)((char*)pvGlblAddr+dwGlbSize-sizeof(CODE_TYPE)))
17 
18     #define INIT_CHECK(pvGlblAddr, dwGlbSize) do{\
19         HEAD_VAL(pvGlblAddr) = TAIL_VAL(pvGlblAddr, dwGlbSize) = CHK_CODE;}while(0)
20     #define CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine) do{\
21         if((HEAD_VAL(pvGlblAddr) != CHK_CODE) || (TAIL_VAL(pvGlblAddr, dwGlbSize) != CHK_CODE)) {\
22             printf("[%s(%d)]Memory Overrun(ChkHead:0x%X,ChkTail:0x%X)!\n", pFileName, dwCodeLine, \
23             HEAD_VAL(pvGlblAddr), TAIL_VAL(pvGlblAddr, dwGlbSize)); \
24         }}while(0)
25 
26     #define INIT_GLOBAL(pvGlblAddr, dwInitVal, dwGlbSize) \
27             InitGlobal(pvGlblAddr, dwInitVal, dwGlbSize, __FILE__, __LINE__)
28     #define SET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) \
29             SetGlobal(pvGlblAddr, pvGlblVal, dwGlbSize, __FILE__, __LINE__)
30     #define GET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) \
31             GetGlobal(pvGlblAddr, pvGlblVal, dwGlbSize, __FILE__, __LINE__)
32 #else
33     #define CHK_CODE    0
34     #define CHK_HEAD
35     #define CHK_TAIL
36     #define HEAD_VAL(pvGlblAddr)               0
37     #define TAIL_VAL(pvGlblAddr, dwGlbSize)    0
38     #define INIT_CHECK(pvGlblAddr, dwGlbSize)
39     #define CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine)
40 
41     #define INIT_GLOBAL(pvGlblAddr, dwInitVal, dwGlbSize) do{\
42             memset(pvGlblAddr, dwInitVal, dwGlbSize);}while(0)
43     #define SET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) do{\
44             memcpy(pvGlblAddr, pvGlblVal, dwGlbSize);}while(0)
45     #define GET_GLOBAL(pvGlblAddr, pvGlblVal, dwGlbSize) do{\
46             memcpy(pvGlblVal, pvGlblAddr, dwGlbSize);}while(0)
47 #endif
48 
49 void InitGlobal(void* pvGlblAddr, int dwInitVal, unsigned int dwGlbSize,
50                 const char* pFileName, INT32U dwCodeLine)
51 {
52     if(NULL == pvGlblAddr) //理论上pFileName必不为空
53     {
54         printf("[%s(%d)]Arg1 Null!\n", pFileName, dwCodeLine);
55         return;
56     }
57 
58     memset(pvGlblAddr, dwInitVal, dwGlbSize);
59     INIT_CHECK(pvGlblAddr, dwGlbSize);
60 }
61 void SetGlobal(void* pvGlblAddr, void* pvGlblVal, unsigned int dwGlbSize,
62                const char* pFileName, INT32U dwCodeLine)
63 {
64     if((NULL == pvGlblAddr) || (NULL == pvGlblVal))
65     {
66         printf("[%s(%d)]Arg1(%p) or Arg2(%p) Null!\n", pFileName, dwCodeLine, pvGlblAddr, pvGlblVal);
67         return;
68     }
69 
70     memcpy(pvGlblAddr, pvGlblVal, dwGlbSize);
71     CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine);
72 }
73 void GetGlobal(void* pvGlblAddr, void* pvGlblVal, unsigned int dwGlbSize,
74                const char* pFileName, INT32U dwCodeLine)
75 {
76     if((NULL == pvGlblAddr) || (NULL == pvGlblVal))
77     {
78         printf("[%s(%d)]Arg1(%p) or Arg2(%p) Null!\n", pFileName, dwCodeLine, pvGlblAddr, pvGlblVal);
79         return;
80     }
81 
82     memcpy(pvGlblVal, pvGlblAddr, dwGlbSize);
83     CHK_OVERRUN(pvGlblAddr, dwGlbSize, pFileName, dwCodeLine);
84 }
85 
86 int main(void)
87 {
88     INIT_GLOBAL(&gtChkMem, 0, sizeof(T_CHK_MEM));
89     printf("[%d]ChkHead:0x%X,ChkTail:0x%X!\n", __LINE__, HEAD_VAL(&gtChkMem), TAIL_VAL(&gtChkMem, sizeof(T_CHK_MEM)));    
90     T_CHK_MEM tChkMem;
91     GET_GLOBAL(&gtChkMem, &tChkMem, sizeof(T_CHK_MEM));
92 
93     strcpy(tChkMem.szName, "Elizabeth");
94     SET_GLOBAL(&gtChkMem, &tChkMem, sizeof(T_CHK_MEM));
95 
96     return 0;
97 }

 

     其中,TAIL_VAL宏假定系统为1字节对齐(否则请置CODE_SIZE为4字节)。因0xCC默认为四字节(对应于0xFFFFFFCC),故需用(CODE_TYPE)0xCC做类型转换,否则CHK_OVERRUN宏内if判断恒为真。

     该检查机制的缺点是仅用于检测写越界,且拷贝和解引用次数增多,访问效率有所降低。读越界后果通常并不严重,除非试图读取不可访问的区域,否则难以也不必检测。

     数据区内存越界通常会导致相邻的全局变量被意外改写。因此若已确定被越界改写的全局变量,则可通过工具查看符号表,根据地址顺序找到前面(通常向高地址越界)相邻的全局数据,然后在代码中排查访问该数据的地方,看看有哪些位置可能存在越界操作。

     有时,全局数据被意外改写并非内存越界导致,而是某指针(通常为野指针)意外地指向该数据地址,导致其内容被改写。野指针导致的内存改写往往后果严重且难以定位。此时,可编码检测全局数据发生变化的时机。若能结合堆栈回溯(Call Backtrace),则通常能很快地定位问题所在。

     修改只读数据区内容会引发段错误(Segmentation Fault),但这种低级失误并不常见。一种比较隐秘的缺陷是函数内试图修改由指针参数传入的只读字符串,详见《关于Linux系统basename函数缺陷的思考》一文。

     因其作用域限制,静态局部变量的内存越界相比全局变量越界更易发现和排查。

    【对策】某些工具可帮助检查内存越界的问题,但并非万能。内存越界通常依赖于测试环境和测试数据,甚至在极端情况下才会出现,除非精心设计测试数据,否则工具也无能为力。此外,工具本身也有限制,甚至在某些大型项目中,工具变得完全不可用。

     与使用工具类似的是自行添加越界检测代码,如本节上文所示。但为求安全性而封装检测机制的做法在某种意义上得不偿失,既不及Java等高级语言的优雅,又损失了C语言的简洁和高效。因此,根本的解决之道还是在于设计和编码时的审慎周密。相比事后检测,更应注重事前预防。

     编程时应重点走查代码中所有操作全局数据的地方,杜绝可能导致越界的操作,尤其注意内存覆写和拷贝函数memset/memcpy/memmove和数组下标访问。

     在内存拷贝时,必须确保目的空间大于或等于源空间。也可封装库函数使之具备安全校验功能,如:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值