已釋放的棧內存

 

     (被調)函數內的局部變量在函數返回時被釋放,不該被外部引用。雖然並不是真正的釋放,經過內存地址仍可能訪問該棧區變量,但其安全性不被保證。後續若還有其餘函數調用,則其局部變量可能覆蓋該棧區內容。常見狀況有兩種:前次調用影響當前調用的局部變量取值(函數的"遺產");被調函數返回指向棧內存的指針,主調函數經過該指針訪問被調函數已釋放的棧區內容(召喚亡靈)。數組

 

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函數調用。ide

     所以,爲避免這種干擾,建議聲明自動局部變量時對其顯式賦初值(初始化)。函數

    【示例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函數。spa

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函數試圖打印該地址內容。code

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";則返回無效指針,輸出不肯定。

相關文章
相關標籤/搜索