roger 发表于 2020-5-4 00:15:55

深入理解静态变量

全局静态变量




[*]数据存储:

[*]已初始化的存储在数据区中的已初始化变量区。
[*]未初始化的存储在数据区中的未初始化变量区。
[*]作用域:文件作用域。
[*]本质:是受编译器按语法约束的全局变量。
[*]作用:私有化某些变量和方法,以文件为单位对源码进行控制和管理。
[*]生命周期:从所处模块装载到所处模块卸载。
探测全局静态变量生命周期

  首先打印出全局变量的地址。
  #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());  
  控制跨文件访问

  编译器编译阶段将全局静态变量进行处理,在链接阶段时候,其他文件便不能够访问本文件中的全局静态变量了,会产生报错。
  
  
  但是仅仅是编译器层面做的处理,全局静态变量的值依旧存在内存中,可以用如下的方法进行访问。
  
 #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表示层级。
  
  

  名称粉碎识别关键参数

页: [1]
查看完整版本: 深入理解静态变量