开发者社区> dasein58> 正文

已释放的栈内存详解

简介:   (被调)函数内的局部变量在函数返回时被释放,不应被外部引用。虽然并非真正的释放,通过内存地址仍可能访问该栈区变量,但其安全性不被保证。后续若还有其他函数调用,则其局部变量可能覆盖该栈区内容。常见情况有两种:前次调用影响当前调用的局部变量取值(函数的"遗产");被调函数返回指向栈内存的指针,主调函数通过该指针访问被调函数已释放的栈区内容(召唤亡灵)。   【示例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无论何种编译方式均输出乱码。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
lua脚本世界--快速入门
我想许多人并不是困惑脚本语言和非脚本语言的不同,而是困惑脚本语言为什么叫脚本语言。 脚本是由script翻译来的,这个词在用到计算机前的意思是剧本,现在把script还原到原有的意思——“剧本”来理解其在编程中的延伸意义。(脚与非脚的不同在于执行之前是否需要编译)我们可以把“编译”对应到制作电影时的“拍摄”,就是由源代码生成可执行程序的过程。 脚本语言不需要编译,即这个剧本不需要拍摄成电影,一句一句“解释”着执行就可以了。
10 0
万字长文丨7个经典问题,助你拿下Java面试(建议收藏)
万字长文丨7个经典问题,助你拿下Java面试(建议收藏)
9 0
〖Docker指南④〗docker容器卷
Docker容器产生的数据,如果不通过docker commit生成新的镜像,使得数据做为镜像的一部分保存下来, 那么当容器删除后,数据自然也就没有了,为了能保存数据在docker中,我们使用卷。 有点类似我们Redis里面的rdb和aof文件;或者配置中心;再或者k8s里的数据卷
13 0
一篇文章搞定一个大数据组件:kudu知识点全集
一篇文章搞定一个大数据组件:kudu知识点全集
12 0
Python自动化办公之PDF拆分工具
今天我们继续分享真实的自动化办公案例,希望各位 Python 爱好者能够从中得到些许启发,在自己的工作生活中更多的应用 Python,使得工作事半功倍!
6 0
Hive小文件问题:如何产生、造成影响、解放办法
Hive小文件问题:如何产生、造成影响、解放办法
8 0
10行Python代码能做出哪些有趣的事情?
Python 凭借语法的易学性,代码的简洁性以及类库的丰富性,赢得了众多开发者的喜爱。下面我们来看看,用不超过10行代码能实现些什么有趣的功能
18 0
Pandas DateTime 超强总结
Pandas DateTime 超强总结
11 0
Java JVM知识汇总
1、JVM 是什么? Java虚拟机(Java virtual machine,JVM)是 Java 程序运行基础,Java虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。 Java虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java语言的可移植性正是建立在Java虚拟机的基础上。任何平台只要装有针对于该平台的Java虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。
6 0
+关注
919
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
OceanBase 入门到实战教程
立即下载
阿里云图数据库GDB,加速开启“图智”未来.ppt
立即下载
实时数仓Hologres技术实战一本通2.0版(下)
立即下载