el表达式中可以写入一个未被初始化的变量_深入理解静态变量

d2cba332b7aa8b3a2a95eb919c04c561.png 本文为看雪论坛优秀文章 看雪论坛作者ID:flag0

全局静态变量

  • 数据存储:

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

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

  • 作用域:文件作用域。

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

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

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

探测全局静态变量生命周期 首先打印出全局变量的地址。
#include #include  static int g_nTest = 0x996; int main(){    printf("%p\r\n",&g_nTest);    system("pause");    return 0;}
680906b8d7a040e1cea0f3869db36296.png   在mainCRTStartup()函数起始位置下断点,然后在内存窗口监测静态全局变量地址。   d1d16dc3db24710254589f71dfab0a50.png   单步步过,寻找影响全局静态变量内存地址的语句。   54699f6efbcec26ee6fc243827bca005.png   可以看到其在断下时,全局静态变量地址的值就已经有了,因为已初始化的全局变量的值会被写入到exe文件中,所以其在模块加载时,就已经有了值,是在mainCRTStartup()函数之前的。   我们继续测试,在C++编译器环境下,将函数的返回值赋值给全局静态变量的情况。
#include #include int GetInt(){    printf("Hello world!");    return 0x996;} int nTest1 = GetInt(); int main(){    system("pause");    return 0;}
ef407adb513420e3d10bfb33171dc94c.png   该函数在_cinit()中的第二个_initterm调用里被执行,_cinit()的作用为初始化浮点协处理器和初始化全局变量。   ea1076ffc6f44fcef90562e07cf1a77b.png   F11跟进_cinit:   39cf055635086f8d4ac264ebeadd7c2f.png   此时到了第二个_intitterm按F10(不要按F11跟进去)自动跳转到在GetInt函数头部下的断点的位置。   436c045004f27bb78a20959953bc6625.png   第一个为_initterm官方的全局变量初始化,第二个_initterm才为用户的全局变量初始化。   全局变量结束 我们继续探测全局变量的值被释放的结束的地方。   在main函数return处下断点,单步步过到进程结束的位置,查看全局静态变量值的变化。   一路F10跟到MainCRTStartup中的exit(mainret);处,全局静态变量内存的值仍未发生变动,此时单步执行exit时,程序结束。   f80305a264bc0f4b4bd201a69bcb2149.png   所以,我们可以判定,全局变量的生命周期是从所处模块装载到所处模块卸载
编译器控制跨文件访问:限制导出
全局静态变量主要用途就是限制导出,实现其函数和变量的私有化,编译器通过限制导出机制来控制其跨文件访问的。   导入:使用其他模块中的符号。   导出:提供某个符号给其他的模块用。   例如:静态函数   static void foo(),只能在本文件中使用,不可以跨文件调用,这样则有利于开发过程中的私有化,从而摘轻各自开发者的责任。   早期编译器的私有概念是通过static来实现的,后来才完善这个概念,并逐步发展为其他的面向对象语言,比如C++。   在没有面向对象概念的时候,使用static来实现私有化。     使用限制导出思想的demo main.c:
static char* msg = "Hello";char* GetMsg(){    return msg;}
Test.c:
printf("%s\r\n",GetMsg());
  控制跨文件访问 编译器编译阶段将全局静态变量进行处理,在链接阶段时候,其他文件便不能够访问本文件中的全局静态变量了,会产生报错。   6cac0e863160bb4caa3e33cb56ca4afe.png   但是仅仅是编译器层面做的处理,全局静态变量的值依旧存在内存中,可以用如下的方法进行访问。   main.cpp:
#include #include  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 extern int g_nTest2; void printFun(){    printf("%x\r\n",(&g_nTest2)[-1]);}
8ed35ff92a9593dc16055fd182685b90.png 局部静态变量
  • 数据存储:

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

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

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

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

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

名称粉碎 名称粉碎(Name-mangling)又名命名粉碎或命名重组,是指在目标文件符号表和连接过程中使用的名字通常与编译目标文件的源程序中的名字不一样,编译器将目标源文件中的名字进行了调整。   编译器对局部静态变量使用了名称粉碎机制。   首先将其声明成全局变量,然后将其作用域插入到全局变量名称中去,类似于snTest_fooD通过这种方式将全局变量限制为在某函数里面才可以访问。   不同编译器厂商对局部静态变量的名称粉碎机制存在差异,有些会将参数和返回值也加入到重组后的名称中,名称粉碎和编译器厂商的习惯相关,不属于标准,所以,不同的厂商不同的版本,甚至不同的版本规则都不一样。     编译器的名称粉碎机制测试方法 修改各项函数属性,编译后,打开对应的obj文件,搜索局部静态变量名,查看不同属性参数的修改对于名称粉碎后的局部静态变量名的影响。
#include #include  void TestLocal(){    static int nTest1 = 0x996;    printf("%d\r\n",nTest1);}  int main(){    TestLocal();    system("pause");    return 0;}
将以上代码编译称为obj文件。   打开obj文件,搜索局部静态变量名nTest1:   4c7c7b96b8046a623b18cd0f099827e3.png   其在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。   静态局部变量定义处没有产生赋值的汇编代码,所以在函数执行时不会被赋值。   ed2811dc3d0e1b06ad76cb992ebf909d.png
局部静态变量初始化为常量的值
静态局部变量如果赋初值,则会和已初始化的全局变量一样被写入到文件中,存储在数据区中的已初始化的全局变量区。   6bc3b3f65005b89e6371a651d8fcede4.png   查看exe文件26a30处:   05600f8f679281007a2110a715cc17a9.png   如果未赋初值,则会存储在未初始化的全局变量区,都不会产生赋值的汇编指令。   7984aea97f1cf9a68fed8e9380d4d0ee.png
局部静态变量初始化为变量的值
void fooD(int n){    static int nTest = n;}
在C编译器下报错error C2099: initializer is not a constant   在C++编译器环境下   c++的语法允许局部静态变量初始化为变量的值,c语言不允许。   当采用C++编译器时,名称粉碎规则会发生改变。   调用方式、返回值、函数参数、及函数参数的数量均会影响到其名称粉碎规则的改变。   8d783c89fc85d96b2c7bc3e4b73aa066.png   _?nTest1@?1??TestLocal@@YAXH@Z@4HA   VC++6.0 Debug中watch窗口解析名称粉碎bug   watch窗口用的C编译器的名称粉碎规则,所以其无法正常显示cpp文件中的局部静态变量信息。   cccc0d8b93d57c3670b1c4b4a35dbcb2.png   当静态局部变量赋初值为变量时,储存在未初始化区,会产生代码。   会产生汇编代码:   2aa693046068347063b610747b59d4d9.png   存储在未初始化全局变量区:   dca9c196da2c8dcf0a814d14e9d3eb8a.png
如何判断静态局部变量是否被赋初值
当静态全局变量赋值为变量之后,VC++6.0编译器会在其存储位置附近增加一个字节来存储是否赋初值的状态。 4e76a526f6beba20ecdfae21ad147d32.png   VC++6.0中,一个位存储一个静态全局变量是否被赋初值的状态。   其他编译器存储状态的位置和大小可能不一样,但是思路一样。
#include #include  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;将这个标志位的值给修改掉了,所以导致了静态变量重复赋初值。 49c5a94d5908c4406366e90dd115c78e.png 在VC++6.0编译器中,当赋初值为函数参数的局部静态变量超过8个时,会新增加一个字节来记录状态: 72a0ac2a6e5ee65ce873e5259fd895e7.png

致谢

科锐逆向 钱林松老师   by : 科锐37期学员 08f06e8c2c5b87bcf47700820c39db55.gif - End - 4104efa918e21a345b30221441189ae6.png

看雪ID:flag0

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

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

推荐文章++++

5bd6f16e9268c776c0f7e89f2309590f.png

*  短视频某手sig3算法调用

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

*  ptmalloc代码研究

*  深入窥探动态链接

*  PspCidTable引起的思考和探索

好书推荐d2ae243c2c820871b942606eb69fdf4e.png

﹀ ﹀ ﹀ 69759db745d651b28e9cb07a21e6c677.png 公众号ID:ikanxue 官方微博:看雪安全 商务合作:wsc@kanxue.com 9fb6f067357f92322296ef6b742c8482.gif 戳 “阅读 原文 ” 一起来充电吧!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值