查看原文
其他

深入理解静态变量

flag0 看雪学院 2021-03-07
本文为看雪论坛优秀文章
看雪论坛作者ID:flag0


全局静态变量


  • 数据存储:

    • 已初始化的存储在数据区中的已初始化变量区。

    • 未初始化的存储在数据区中的未初始化变量区。

  • 作用域:文件作用域。

  • 本质:是受编译器按语法约束的全局变量。

  • 作用:私有化某些变量和方法,以文件为单位对源码进行控制和管理。

  • 生命周期:从所处模块装载到所处模块卸载。



探测全局静态变量生命周期


首先打印出全局变量的地址。

#include <stdio.h>#include <stdlib.h> static int g_nTest = 0x996; int main(){ printf("%p\r\n",&g_nTest); system("pause"); return 0;}

 
在mainCRTStartup()函数起始位置下断点,然后在内存窗口监测静态全局变量地址。
 
 
单步步过,寻找影响全局静态变量内存地址的语句。
 
 
可以看到其在断下时,全局静态变量地址的值就已经有了,因为已初始化的全局变量的值会被写入到exe文件中,所以其在模块加载时,就已经有了值,是在mainCRTStartup()函数之前的。
 
我们继续测试,在C++编译器环境下,将函数的返回值赋值给全局静态变量的情况。

#include <stdio.h>#include <stdlib.h>int GetInt(){ printf("Hello world!"); return 0x996;} int nTest1 = GetInt(); int main(){ system("pause"); return 0;}

 
该函数在_cinit()中的第二个_initterm调用里被执行,_cinit()的作用为初始化浮点协处理器和初始化全局变量。
 
 
F11跟进_cinit:
 
 
此时到了第二个_intitterm按F10(不要按F11跟进去)自动跳转到在GetInt函数头部下的断点的位置。
 
 
第一个为_initterm官方的全局变量初始化,第二个_initterm才为用户的全局变量初始化。

 
全局变量结束


我们继续探测全局变量的值被释放的结束的地方。
 
在main函数return处下断点,单步步过到进程结束的位置,查看全局静态变量值的变化。
 
一路F10跟到MainCRTStartup中的exit(mainret);处,全局静态变量内存的值仍未发生变动,此时单步执行exit时,程序结束。
 
 
所以,我们可以判定,全局变量的生命周期是从所处模块装载到所处模块卸载

编译器控制跨文件访问:限制导出


全局静态变量主要用途就是限制导出,实现其函数和变量的私有化,编译器通过限制导出机制来控制其跨文件访问的。
 
导入:使用其他模块中的符号。
 
导出:提供某个符号给其他的模块用。
 
例如:静态函数
 
static void foo(),只能在本文件中使用,不可以跨文件调用,这样则有利于开发过程中的私有化,从而摘轻各自开发者的责任。
 
早期编译器的私有概念是通过static来实现的,后来才完善这个概念,并逐步发展为其他的面向对象语言,比如C++。
 
在没有面向对象概念的时候,使用static来实现私有化。
 
 
使用限制导出思想的demo


main.c:

static char* msg = "Hello";char* GetMsg(){ return msg;}

Test.c:

printf("%s\r\n",GetMsg());

 
控制跨文件访问


编译器编译阶段将全局静态变量进行处理,在链接阶段时候,其他文件便不能够访问本文件中的全局静态变量了,会产生报错。
 
 
但是仅仅是编译器层面做的处理,全局静态变量的值依旧存在内存中,可以用如下的方法进行访问。
 
main.cpp:

#include <stdio.h>#include <stdlib.h> static int g_nTest = 0x996;int g_nTest2 = 0x123;void printFun(); int main(){ printFun(); //printf("%p\r\n",&g_nTest2); system("pause"); return 0;}

Test.cpp:

#include <stdio.h>extern int g_nTest2; void printFun(){ printf("%x\r\n",(&g_nTest2)[-1]);}



局部静态变量


  • 数据存储:

    • 已初始化的存储在数据区中的已初始化变量区。

    • 未初始化的存储在数据区中的未初始化变量区。

  • 作用域:与所在函数作用域相同。

  • 生命周期:与全局静态变量相同。

  • 作用:局部静态变量可以在过程或函数重复运行的时候保留上次运行的值。



名称粉碎


名称粉碎(Name-mangling)又名命名粉碎或命名重组,是指在目标文件符号表和连接过程中使用的名字通常与编译目标文件的源程序中的名字不一样,编译器将目标源文件中的名字进行了调整。
 
编译器对局部静态变量使用了名称粉碎机制。
 
首先将其声明成全局变量,然后将其作用域插入到全局变量名称中去,类似于snTest_fooD通过这种方式将全局变量限制为在某函数里面才可以访问。
 
不同编译器厂商对局部静态变量的名称粉碎机制存在差异,有些会将参数和返回值也加入到重组后的名称中,名称粉碎和编译器厂商的习惯相关,不属于标准,所以,不同的厂商不同的版本,甚至不同的版本规则都不一样。
 
 
编译器的名称粉碎机制测试方法


修改各项函数属性,编译后,打开对应的obj文件,搜索局部静态变量名,查看不同属性参数的修改对于名称粉碎后的局部静态变量名的影响。

#include <stdio.h>#include <stdlib.h> void TestLocal(){ static int nTest1 = 0x996; printf("%d\r\n",nTest1);} int main(){ TestLocal(); system("pause"); return 0;}

将以上代码编译称为obj文件。
 
打开obj文件,搜索局部静态变量名nTest1:
 
 
其在vc6.0的c编译器下的名称粉碎为:
 
_?nTest1@?1??TestLocal@@9@9
 
将其局部静态变量放入函数内的代码块中,编译后观察名称粉碎的变化:

void TestLocal(){ { static int nTest1 = 0x996; printf("%d\r\n",nTest1); }}

其名称粉碎后的结果为
 
_?nTest1@?2??TestLocal@@9@9
 
可以看到由?1变成了?2这里大致可以推测,?x表示层级。
 

名称粉碎识别关键参数


  • 变量名

  • 作用域名

  • 作用域的层级编号

全局静态变量不进行名称粉碎不影响从标识符到内存地址的识别,局部静态变量不名称粉碎会影响。
 
编译器通过名称粉碎的方式做语法检查,关键是集成了变量名、作用域名、作用域的层级编号。

局部静态变量只能被赋一次初值的原因


static int snTest = 999;


上述代码是给编译器看的,告诉编译器全局变量的snTest的初值为999。
 
静态局部变量定义处没有产生赋值的汇编代码,所以在函数执行时不会被赋值。
 


局部静态变量初始化为常量的值


静态局部变量如果赋初值,则会和已初始化的全局变量一样被写入到文件中,存储在数据区中的已初始化的全局变量区。
 
 
查看exe文件26a30处:
 
 
如果未赋初值,则会存储在未初始化的全局变量区,都不会产生赋值的汇编指令。
 


局部静态变量初始化为变量的值


void fooD(int n){ static int nTest = n;}


在C编译器下报错error C2099: initializer is not a constant
 
在C++编译器环境下
 
c++的语法允许局部静态变量初始化为变量的值,c语言不允许。
 
当采用C++编译器时,名称粉碎规则会发生改变。
 
调用方式、返回值、函数参数、及函数参数的数量均会影响到其名称粉碎规则的改变。
 
 
_?nTest1@?1??TestLocal@@YAXH@Z@4HA
 
VC++6.0 Debug中watch窗口解析名称粉碎bug
 
watch窗口用的C编译器的名称粉碎规则,所以其无法正常显示cpp文件中的局部静态变量信息。
 
 
当静态局部变量赋初值为变量时,储存在未初始化区,会产生代码。
 
会产生汇编代码:
 
 
存储在未初始化全局变量区:
 


如何判断静态局部变量是否被赋初值


当静态全局变量赋值为变量之后,VC++6.0编译器会在其存储位置附近增加一个字节来存储是否赋初值的状态。

 
VC++6.0中,一个位存储一个静态全局变量是否被赋初值的状态。
 
其他编译器存储状态的位置和大小可能不一样,但是思路一样。

#include <stdio.h>#include <stdlib.h> void TestLocal(int n){ static int nTest2 = n; printf("%p:",&nTest2); printf("%d\r\n",nTest2); (&nTest2)[1] = 0; nTest2++;} int main(){ TestLocal(10); TestLocal(20); TestLocal(30); system("pause"); return 0;}

(&nTest2)[1] = 0;将这个标志位的值给修改掉了,所以导致了静态变量重复赋初值。



在VC++6.0编译器中,当赋初值为函数参数的局部静态变量超过8个时,会新增加一个字节来记录状态:


致谢


科锐逆向 钱林松老师
 by科锐37期学员



- End -





看雪ID:flag0

https://bbs.pediy.com/user-873556.htm 

*这里由看雪论坛 flag0 原创,转载请注明来自看雪社区。


推荐文章++++

*  短视频某手sig3算法调用

*  Android逆向之一款有缘的apk 第一篇

*  ptmalloc代码研究

*  深入窥探动态链接

*  PspCidTable引起的思考和探索


好书推荐






公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存