(被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。后续若还有其他函数调用,则其局部变量可能覆盖该栈区内容。常见情况有两种:前次调用影响当前调用的局部变量取值(函数的"遗产");被调函数返回指向栈内存的指针,主调函数通过该指针访问被调函数已释放的栈区内容(召唤亡灵)。
【示例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 }
若使用普通编译(如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 }
若使用普通编译,则输出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};}
若使用普通编译,则输出(乱码) abc abc abc;若打开优化开关,则输出(空串) abc abc abc。
若在两次调用中间插入其他函数调用(如FuncInsert),则使用普通编译时输出(乱码) abc (空串) abc;若打开优化开关时仍输出(空串) abc abc abc(FuncInsert函数被优化掉)。
【示例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 }
若使用普通编译,则输出 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 }
调用GetString函数时,将只读数据段存放的字符串常量"Hello World"拷贝至堆栈临时分配的字符数组szStr,即szStr指向该字符串的可读写副本。函数返回szStr地址,同时栈顶指针下移以保证堆栈指针平衡。此时若有函数调用或文凭单步跟踪(软中断也使用堆栈),则可能覆盖szStr所指向的内存。为保留和查看栈区szStr处的内容,可采用示例中的LOOP_COPY、MEMCOPY_CALL或CHAR_PRINT方法(为避免相互影响,三者中应任选一个)。
若使用普通编译,则三种方法均可输出"Hello World";若打开优化开关且在GetString函数返回前添加输出szStr内容的语句(以防赋值被跳过),则三种方法仍可输出"Hello World"。这也证明GetString函数调用返回后,堆栈内存szStr处的内容并未清除。
注意,JUNK_PRINT无论何种编译方式均输出乱码。