C/C++全局静态变量初始化

C/C++全局静态变量初始化

C/C++代码中经常会使用到全局变量和静态变量,很多时候大家搞不清楚全局变量和静态变量的初始化。本文的目的是探讨一下全局变量和静态变量的初始化的几种情况和一些危险的未定义行为。

全局变量

全局变量的定义有以下几种情况:

  1. 定义的时候指定了初始化值
  2. 定义的时候未指定初始化值
  3. 使用动态初始化
  4. thread_local,,

示例代码如下:

// 定义的时候指定了初始化值
int a = 0xEF;

int b;

int c = _calc_value_of_c();

指定了初始化值

// 定义的时候指定了初始化值
int a = 0;

对于指定了初始化值,在编译的时全局变量会被放到 .data (.rdata) 段(section),在进程被创建(模块/镜像{image}被加载)时由加载器初始化,值为定义时的初始化值。

如果变量的类型为 非 POD 类型,全局变量的初始化使用的是动态初始化,类似如下的代码:

std::string str = "hello world";

未指定初始化值

// 定义的时候未指定初始化值
void* ptr;

如上代码,全局变量在定义的时候没有给出初始化值,编译的时候全局变量会在 .bss 段里。当进程被创建时他的值是未初始化的(未定义行为,可能有的编译器会出null,0x00,0xDEDA, 0xEF等值填充)。这种没有给出初始化值的全局变量是很多程序BUG的根源,稍后会给出解释。

动态初始化

int _calc_value_of_c() {
    return GetTickCount();
}

int c = _calc_value_of_c();

动态初始化指全局变量的值是动态计算出来,这种全局变量的初始化比较特殊可以非未两种情况:

  1. 使用新标准中的 constexpr关键字定义的函数
  2. 使用普通的函数

使用constexpr的全局变量和指定了初始化值的行为一致,全局变量会被放到 .data(.rdata) 段中。

使用普通函数的返回值来初始化的全局变量,是要重点关注的。

使用动态初始化的全局变量会由crt负责初始化,初始化的原理是编译器生成类型如下代码:

// `dynamic initializer for 'b''
void __cdecl dynamic_initializer_for_b() {
    b = _calc_value_of_c();
}

static extern "C" __declspec(allocate(".CRT$XCM")) _PVFV __dynamic_initializer_for_b = dynamic_initializer_for_b;

调用过程:

void mainCRTStartup(LPVOID) {
    __scrt_common_main();
}

void __scrt_common_main() {
    //...
    __scrt_common_main_seh();
}

extern "C" _CRTALLOC(".CRT$XCA") _PVFV __xc_a[] = {nullptr};
extern "C" _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[] = {nullptr};

void __scrt_common_main_seh() {
    // ...
    
     _initterm(__xc_a, __xc_z);
}
    

具体的实现可以参考:

crt\src\vcruntime\exe_main.cpp

crt\src\vcruntime\exe_common.inl

crt\src\vcruntime\initialzers.cpp

BUG的根源:

  1. 全局变量的初始化顺序是未定义行为
  2. 全局变量的初始化早已我们普遍认为的代码入口 (main, wWinMain)

下面随手构造一个BUG,这个例子中的 c的值是一个未定的行为。

extern int b;

int v2() {
  return b;
}

int c = v2();

int v() {
  return ::GetTickCount();
}

int b = v();

int main() {
    printf ("c= %d: b= %d", c, b);
}

静态变量

静态变量的初始化分按作用域为两种情况:

  1. 全局
  2. 函数

全局的静态变量与全局变量的初始化行为一致,函数作用域下的静态变量的动态初始化是关注的重点:

类似于全局变量的初始化,如果指定的初始化值,或者未指定值),行为和全局变量指定了初始化值行为一致。

编译器会隐式的生成一个符号, 只是符号的属性未static,符号的可见范围为当前函数:

int function() {
    static int b = 0;    
    // ....
}

int function() {
    static int b;     // 未定义行为
    // ....
}

动态变量的初始化:


int function() {
    static int b = get_value_of_b();
    // ....
}

这种情况又可以分为:启用 /Zc:threadSafeInit 和未启用 /Zc:threadSafeInit-

再原来未启用 threadSafeInit 的情况下,多线程调用函数 静态变量的初始化是非线程安全的(ps:这里要说以下,这也是为什么原来很多使用函数作用域实现单例模式需要对单例的初始化使用加锁等互斥的原因,使用C11后,C++标准要求函数作用域的静态变量的初始化做到线程安全。

未启用/Zc:threadSafeInit的伪码,可以看出对全局的静态变量的赋值过程没有互斥等操作,故而会导致多线程不安全的情况

int static_i_flags_of_static_value_b = 0;
int static_value_of_b;

void function() {
    
  if (static_i_flags_of_static_value_b != 1) {
      static_value_of_b = get_value_of_b();
      static_i_flags_of_static_value_b = 1;
  }
    
  // use .. static_value_of_b
  //
}

再启用了/Zc:threadSafeInit的情况下会生成如下伪码:

int static_i_flags_of_static_value_b = Uninitialized; // tss
int static_value_of_b;

void function() {
  if (static_i_flags_of_static_value_b == Uninitialized) {
	_Init_thread_header(&static_i_flags_of_static_value_b);
    if (static_i_flags_of_static_value_b == BeingInitialized) {
        static_value_of_b = get_value_of_b();        
        _Init_thread_footer(&static_i_flags_of_static_value_b);
    }
  }  
   // use static_value_of_b
}

src\vcruntime\thread_safe_statics.cpp

thread_local

// tls
// TODO
posted @ 2025-06-20 09:12  吱吱的笔记  阅读(297)  评论(0)    收藏  举报