过大的自动变量,但其调用的系统库函数或第三方接口内使用了较大的堆栈空间(如printf调用就要使用2k字节的栈空间)。此时也会导致堆栈溢出,并且不易排查。
?
? ? ?此外,直接使用接口模块定义的数据结构或表征数据长度的宏时也存在堆栈溢出的风险,如:
?
复制代码
1 typedef struct{
2 ? ? unsigned short wVid;
3 ? ? unsigned char aMacAddr[6];
4 ? ? unsigned char ucMacType;
5 }T_MAC_ADDR_ENTRY;
6 typedef struct{
7 ? ? unsigned int dwTotalAddrNum;
8 ? ? T_MAC_ADDR_ENTRY tMacAddrEntry[MAX_MACTABLE_SIZE];
9 }T_MAC_ADDR_TABLE;
复制代码
? ? ?上层模块在自行定义的T_MAC_ADDR_TABLE结构中,使用底层接口定义的MAX_MACTABLE_SIZE宏指定MAC地址表最大条目数。接口内可能会将该宏定义为较大的值(如8000个条目),上层若直接在栈区使用TABLE结构则可能引发堆栈溢出。
?
? ? ?在多线程环境下,所有线程栈共享同一虚拟地址空间。若应用程序创建过多线程,可能导致线程栈的累计大小超过可用的虚拟地址空间。在用pthread_create反复创建一个线程(每次正常退出)时,可能最终因内存不足而创建失败。此时,可在主线程创建新线程时指定其属性为PTHREAD_CREATE_DETACHED,或创建后调用pthread_join,或在新线程内调用pthread_detach,以便新线程函数返回退出或pthread_exit时释放线程所占用的堆栈资源和线程描述符。
?
? ? 【对策】应该清楚所用平台的资源限制,充分考虑函数自身及其调用所占用的栈空间。对于过大的自动变量,可用全局变量、静态变量或堆内存代替。此外,嵌套调用最好不要超过三层。
?
2.2.3 内存越界
? ? ?因其作用域和生存期限制,发生在栈区的内存越界相比数据区更易发现和排查。
?
? ? ?下面的例子存在内存越界,并可能导致段错误:
?
复制代码
1 int bIsUniCommBlv = 1;
2 int main(void)
3 {
4 ? ? char szWanName[] = "OAM_WAN_VOIP";
5 ? ? if(bIsUniCommBlv)
6 ? ? ? ? strcpy(szWanName, "OAM_WAN_MNGIP");
7?
8 ? ? return 0;
9 }
复制代码
? ? ?但该例的另一写法则更为糟糕:
?
复制代码
?1 int bIsUniCommBlv = 1;
?2 int main(void)
?3 {
?4 ? ? char szWanName[] = ""; //字符数组szWanName仅能容纳1个元素('\0')!
?5 ? ? if(bIsUniCommBlv)
?6 ? ? ? ? strcpy(szWanName, "OAM_WAN_MNGIP");
?7 ? ? else
?8 ? ? ? ? strcpy(szWanName, " OAM_WAN_VOIP");
?9?
10 ? ? return 0;
11 }
复制代码
? ? ?函数传递指针参数时也可能发生内存越界:
?
复制代码
?1 typedef struct{
?2 ? ? int dwErrNo;
?3 ? ? int aErrInfo[6];
?4 }T_ERR_INFO;
?5 int PortDftDot1p(int dwPort, int dwDot1p, void *pvOut)
?6 {
?7 ? ? int dwRet = 0;
?8 ? ? T_ERR_INFO *ptErrInfo = (T_ERR_INFO *)pvOut;
?9 ? ? //dwRet = DoSomething();
10 ? ? ptErrInfo->dwErrNo ? ? = dwRet;
11 ? ? ptErrInfo->aErrInfo[0] = dwPort;
12 ? ? return dwRet;
13 }
14?
15 int main(void)
16 {
17 ? ? int dwOut = 0;
18 ? ? PortDftDot1p(0, 5, &dwOut);
19 ? ? return 0;
20 }
复制代码
? ? ?上例中,接口函数PortDftDot1p使用T_ERR_INFO结构向调用者传递出错信息,但该结构并非调用者必知和必需。出于隐藏细节或其他原因,接口将出参指针声明为void*类型,而非T_ERR_INFO*类型。这样,当调用者传递的相关参数为其他类型时,编译器也无法发现类型不匹配的错误。此外,接口内未对pvOut指针判空就进行类型转换,非常危险(即使判空依旧危险)。从安全和实用角度考虑,该接口应该允许pvOut指针为空,此时不向调用者传递出错信息(调用方也许并不想要这些信息);同时要求传入pvOut指针所指缓冲区的字节数,以便在指针非空时安全地传递出错信息。
?
? ? ?错误的指针偏移运算也常导致内存越界。例如,指针p+n等于(char*)p + n * sizeof(*p),而非(char*)p + n。若后者才是本意,则p+n的写法很可能导致内存越界。
?
? ? ?栈区内存越界还可能导致函数返回地址被改写,详见《缓冲区溢出详解》一文。
?
? ? ?两种情况可能改写函数返回地址:1) 对自动变量的写操作超出其范围(上溢);2) 主调函数和被调函数的参数不匹配或调用约定不一致。
?
? ? ?函数返回地址被改写为有效地址时,通过堆栈回溯可看到函数调用关系不符合预期。当返回地址被改写为非法地址(如0)时,会发生段错误,并且堆栈无法回溯:
?
1 Program received signal SIGSEGV, Segmentation fault.
2 0x00000000 in ?? ()
? ? ?这种故障从代码上看特征非常明显,即发生在被调函数即将返回的位置。
?
? ? 【对策】与数据区内存越界对策相似,但更注重代码走查而非越界检测。
?
2.2.4 返回栈内存地址
? ? ?(被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。详见《已释放的栈内存》一文。
?
复制代码
?1 const static char *paMsgNameMap[] = {
?2 ? ? /* 0 */ ? ? "0",
?3 ? ? /* 1 */ ? ? "1",
?4 ? ? /* 2 */ ? ? "2",
?5 ? ? /* 3 */ ? ? "3",
?6 ? ? /* 4 */ ? ? "Create",
?7 ? ? /* 5 */ ? ? "5",
?8 ? ? /* 6 */ ? ? "Delete",
?9 ? ? /* 7 */ ? ? "7",
10 ? ? /* 8 */ ? ? "Set",
11 ? ? /* 9 */ ? ? "Get",
12 ? ? //...