首页 > 代码库 > 已释放的栈内存

已释放的栈内存

 

     (被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。后续若还有其他函数调用,则其局部变量可能覆盖该栈区内容。常见情况有两种:前次调用影响当前调用的局部变量取值(函数的"遗产");被调函数返回指向栈内存的指针,主调函数通过该指针访问被调函数已释放的栈区内容(召唤亡灵)。

 

1 函数的"遗产"

    【示例1】先后连续调用Ancestor和Sibling函数,注意函数内的dwLegacy整型变量。

 1 void Ancestor(void){
 2     int dwLegacy = 42;
 3 }
 4 void Sibling(void){
 5     int dwLegacy;
 6     printf("%d\n", dwLegacy);
 7 }
 8 int main(void){
 9     Ancestor();
10     Sibling();
11     return 0;
12 }
View Code

     若使用普通编译(如gcc test.c),则输出42,因为编译器重用之前函数的调用栈;若打开优化开关(如gcc -O test.c),则输出一个随机的垃圾数,因为Ancestor函数将被优化为空函数,也不会被main函数调用。

     因此,为避免这种干扰,建议声明自动局部变量时对其显式赋初值(初始化)。

    【示例2】先后调用Ancestor和Sibling函数,注意函数内的aLegacy数组变量。

 1 void Ancestor(void){
 2     int aLegacy[10], dwIdx = 0;
 3     for(dwIdx = 0; dwIdx < 10; dwIdx++)
 4         aLegacy[dwIdx] = dwIdx;
 5 }
 6 void Sibling(void){
 7     int aLegacy[10], dwIdx = 0;
 8     for(dwIdx = 0; dwIdx < 10; dwIdx++)
 9         printf("%d ", aLegacy[dwIdx]);
10 }
View Code

     若使用普通编译,则输出0 1 2 3 4 5 6 7 8 9(Ancestor函数内的数组赋值会影响Sibling函数的数组初值);若打开优化开关,则输出一串随机的垃圾数。

    【示例3】连续调用两次Func函数。

1 void Func(void){
2     char acArr[25];
3     printf("%s ", acArr); //注意此句打印结果
4     acArr[0]= a; acArr[1] = b; acArr[2] = c; acArr[3]= \0;
5     printf("%s ", acArr);
6 }
7 void FuncInsert(void){char acArr[25] = {0};}
View Code

     若使用普通编译,则输出(乱码) abc abc abc;若打开优化开关,则输出(空串) abc abc abc。

     若在两次调用中间插入其他函数调用(如FuncInsert),则使用普通编译时输出(乱码) abc (空串) abc;若打开优化开关时仍输出(空串) abc abc abc(FuncInsert函数被优化掉)。

 

2 召唤亡灵

    【示例4】Specter函数返回局部变量dwDead的地址,main函数试图打印该地址内容。

1 int *Specter(void){
2     int dwDead = 1;
3     return &dwDead;  //编译器将提出警告,如function returns address of local variable
4 }
5 int main(void){ 
6     int *pAlive = Specter();
7     printf("*pAlive = %d\n", *pAlive);
8     return 0;
9 }
View Code

     若使用普通编译,则输出* pAlive = 1;若打开优化开关,则Specter函数跳过赋值语句直接返回dwDead变量地址,故输出*p = (随机的垃圾数)。

     注意,Specter函数返回值(地址)存放在%eax寄存器内,main函数读取寄存器值,将其作为内存地址访问该地址处的存储内容——该内容很可能并未初始化,或即将被新的调用栈覆盖!

    【示例5】GetString函数返回局部字符数组szStr的地址,main函数试图打印该地址内容。

 1 char *GetString(void){
 2     char szStr[] = "Hello World";  //此句后增加printf("%s\n", szStr);可防止赋值被优化掉
 3     return szStr;   //编译器将提出警告,如function returns address of local variable
 4 }
 5 int main(void){
 6     char *pszStr = GetString();  //pszStr指向"Hello World"的副本
 7 
 8     //GetString函数返回后,尝试输出GetString函数内局部字符数组szStr的内存内容
 9 #ifdef LOOP_COPY
10     unsigned char ucIdx = 0;
11     char szStackStr[sizeof("Hello World")] = {0};
12     for(ucIdx = 0; ucIdx < sizeof("hello world"); ucIdx++)
13        szStackStr[ucIdx] = pszStr[ucIdx]; 
14     printf("szStackStr = %s\n", szStackStr);  //原szStr处的内容,"Hello World"
15 #endif
16 #ifdef MEMCOPY_CALL  //当内存拷贝函数内部无局部或临时变量时,可用该法
17     char szStr[sizeof("Hello World")] = {0};
18     memcpy(szStr, pszStr, sizeof(szStr));
19     printf("szStr = %s\n", szStr);
20 #endif
21 #ifdef CHAR_PRINT
22     printf("pszStr = %c%c%c%c%c%c%c%c%c%c%c%c\n", 23            pszStr[0],pszStr[1],pszStr[2],pszStr[3],pszStr[4],pszStr[5], 24            pszStr[6],pszStr[7],pszStr[8],pszStr[9],pszStr[10],pszStr[11]);
25 #endif
26 #ifdef JUNK_PRINT
27     printf("pszStr = %s\n", pszStr);   //当前pszStr处的内容,垃圾
28 #endif
29     return 0;
30 }
View Code

     调用GetString函数时,将只读数据段存放的字符串常量"Hello World"拷贝至堆栈临时分配的字符数组szStr,即szStr指向该字符串的可读写副本。函数返回szStr地址,同时栈顶指针下移以保证堆栈指针平衡。此时若有函数调用或单步跟踪(软中断也使用堆栈),则可能覆盖szStr所指向的内存。为保留和查看栈区szStr处的内容,可采用示例中的LOOP_COPY、MEMCOPY_CALL或CHAR_PRINT方法(为避免相互影响,三者中应任选一个)。

     若使用普通编译,则三种方法均可输出"Hello World";若打开优化开关且在GetString函数返回前添加输出szStr内容的语句(以防赋值被跳过),则三种方法仍可输出"Hello World"。这也证明GetString函数调用返回后,堆栈内存szStr处的内容并未清除。

     注意,JUNK_PRINT无论何种编译方式均输出乱码。

 

     另见下面的代码片段:

测试1

测试2

测试3

//采用return返回动态内存地址

char* GetMemory1(char *p, int size){

    p = (char *)malloc(size);*

    return p;

}

void Test1(void){

    char *str = NULL;

    str = GetMemory1(str, 100);

    strcpy(str, "Hello\n");

    printf(str);

    free(str);

}

//采用二级指针返回动态内存地址

void GetMemory2(char **p,int size){

    *p = (char *)malloc(size);

}

void Test2(void){

    char *str = NULL;

    GetMemory2(&str, 100);

    strcpy(str, "Hello");

    printf(str);

    free(str);

    if(str != NULL)*

         strcpy(str,"World\n");

    printf(" %s", str);

}

//正确返回只读字符串地址,但无意义(无法修改内容)

char* GetMemory3(void){

char *p = "Hello World";*

    return p;

}

void Test3(void){

    char *str = NULL;

    str = GetMemory3();

    printf(str);

}

Test1输出Hello

【注*】malloc函数返回void*指针,但C++不允许void*隐式转换到任意类型指针(需要static_cast)。故建议如下兼容写法:

T* p = (T*)malloc(size * sizeof(*p));或

T* p = (T*)malloc(size * sizeof(T));

Test2输出Hello World

【注*】进程中内存管理由库函数完成。当释放内存时,通常不会将内存归还给操作系统,故可继续访问该地址。但因其已被”回收”,若输出语句前再次分配内存,则同段空间可能被重新分配给其他变量,造成错误。

Test3输出Hello World

【注*】此处若写为char p[] = "Hello World";则返回无效指针,输出不确定。