zoukankan      html  css  js  c++  java
  • 已释放的栈内存

     

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

    1 函数的"遗产"

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

     1 void Ancestor(void){
     2     int dwLegacy = 42;
     3 }
     4 void Sibling(void){
     5     int dwLegacy;
     6     printf("%d
    ", 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]= '';
    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
    ", *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
    ", 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
    ", 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
    ", szStr);
    20 #endif
    21 #ifdef CHAR_PRINT
    22     printf("pszStr = %c%c%c%c%c%c%c%c%c%c%c%c
    ", 
    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
    ", 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 ");

        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 ");

        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";则返回无效指针,输出不确定。

  • 相关阅读:
    Firefox OS 开发者预览版手机上线数小时即售罄
    7 款风格新颖的 jQuery/CSS3 导航
    大数据时代的 9 大KeyValue存储数据库
    jQuery 2.0发布,不支持IE 6/7/8
    全面理解面向对象的 JavaScript
    Hdoop入门
    Java Web 高性能开发,第 2 部分: 前端的高性能
    MySQL数据库存储引擎和分支现状
    11 个超棒的 jQuery 分步指引插件
    [扩展/新建swap]将文件或分区加载为swap
  • 原文地址:https://www.cnblogs.com/clover-toeic/p/3736560.html
Copyright © 2011-2022 走看看