已释放的栈内存详解

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

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

  【示例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无论何种编译方式均输出乱码。

目录
相关文章
|
7月前
|
网络协议 安全 Unix
深入剖析进程间通信:Unix 套接字、共享内存与IP协议栈的性能比较
深入剖析进程间通信:Unix 套接字、共享内存与IP协议栈的性能比较
212 2
|
16天前
|
存储
栈内存
栈内存归属于单个线程,也就是每创建一个线程都会分配一块栈内存,而栈中存储的东西只有本线程可见,属于线程私有。 栈的生命周期与线程一致,一旦线程结束,栈内存也就被回收。 栈中存放的内容主要包括:8大基本类型 + 对象的引用 + 实例的方法
21 1
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
72 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
5月前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
66 0
|
4月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
4月前
|
存储 程序员 编译器
堆和栈内存的区别是什么
【8月更文挑战第23天】堆和栈内存的区别是什么
294 4
|
5月前
|
存储 Java
深入理解Java中的堆内存与栈内存分配
深入理解Java中的堆内存与栈内存分配
|
5月前
|
存储 监控 Java
深入剖析堆和栈的区别及其在内存管理中的影响
深入剖析堆和栈的区别及其在内存管理中的影响
|
6月前
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
67 2
|
5月前
|
存储 Rust JavaScript
Rust 问题之TypeScript 代码,变量 s 存储在栈内存中还是堆内存中如何解决
Rust 问题之TypeScript 代码,变量 s 存储在栈内存中还是堆内存中如何解决