C++ static关键字引发的思考

admin 2024年1月2日22:28:22评论21 views字数 6427阅读21分25秒阅读模式



基本用法


在面向对象中的用法


在类中,可以使用static关键字修饰成员函数和变量,被修饰后的函数或变量被称为静态成员函数或变量。它们属于整个类,不属于某一个对象,这意味着无需创建对象即可访问静态成员函数或变量。最常见的一个用法就是单例模式(整个类仅可只有一个对象),例:

class Singleton
{
public:
static Singleton& instance() // 静态方法
{
static Singleton inst; // 静态对象在instance中声明
return inst;
}
int& get() { return value_; }
private:
Singleton() : value_(0) { std::cout << "Singleton::Singleton()" << std::endl; }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
~Singleton() { std::cout << "Singleton::~Singleton()" << std::endl; }
private:
int value_; // 非静态成员变量
};

int value = Singleton::instance().get() // Singleton::instance()返回静态对象inst


在面向过程中的用法


此处的static主要用于限制修饰对象的作用域。

◆静态函数,该函数仅可在当前文件中使用。
◆静态变量,该变量仅可在当前文件中使用,而全局变量可导出给别的文件使用,虽然两者都是将变量存储在.data区域,且在程序整个声明周期都有效。
◆静态局部变量,该变量只能被初始化一次,仅可在当前函数中使用。




新的问题


面向过程中的静态局部变量"只能被初始化一次"是如何实现的?

考虑到静态变量有可能被多次初始化且使用变量初始化,使用的例子如下:

#include <iostream>
#include <thread>
#include <chrono> // std::chrono::seconds

using namespace std;

void func_a();
void func_b(int a, int b);

int main()
{
func_a();
thread t1(func_b, 1, 2);
thread t2(func_b, 3, 4);
thread t3(func_b, 5, 6);
thread t4(func_b, 7, 8);

std::this_thread::sleep_for(std::chrono::seconds(1));
getchar();
t1.join();t2.join();t3.join();t4.join();
return 0;
}

void func_a()
{
static int func_a_value = 0x1234;
cout << "func_a_value => " << func_a_value << ", current function = > " << __FUNCTION__ << endl;
}

void func_b(int a, int b)
{
static int res = a + b;
cout << "res => " << res << ", current thread id => " << std::this_thread::get_id() << endl;
}


Windows下的MSVC编译器的实现


静态局部变量func_a_value


func_a函数中是以常量初始化静态局部变量func_a_value,因此编译器直接将0x1234写入.data对应的位置,这与单(多)线程无关,与单(多)次初始化无关。

C++ static关键字引发的思考

若函数以变量形式初始化静态局部变量,则实现初始化一次的原理见静态局部变量res

静态局部变量res


存在多个线程初始化res的情况,因此需要进行线程同步。IDA反汇编func_b函数之后的结果如下:

C++ static关键字引发的思考

对应的C++代码如下:

if(pOnce > *(int *)(NtCurrentTeb()->ThreadLocalStoragePointer[0] + 0x104) // tls中存储的全局 局部静态变量初始化计数器, 初始值为INT_MIN(0x80000000)
{
_Init_thread_header(&pOnce)
if ( pOnce == -1 )
{
res = b + a; // 初始化代码
_Init_thread_footer(&pOnce);
}
}

_Init_thread_header的源代码如下:

// 代码所在的文件为thread_safe_statics.cpp
extern "C" void __cdecl _Init_thread_header(int* const pOnce) noexcept
{
_Init_thread_lock(); // 进入临界区

if (*pOnce == uninitialized) // uninitialized = 0
{
*pOnce = being_initialized; // being_initialized = -1
}
else
{
while (*pOnce == being_initialized)
{
// Timeout can be replaced with an infinite wait when XP support is
// removed or the XP-based condition variable is sophisticated enough
// to guarantee all waiting threads will be woken when the variable is
// signalled.
_Init_thread_wait(xp_timeout);

if (*pOnce == uninitialized)
{
*pOnce = being_initialized;
_Init_thread_unlock();
return;
}
}
_Init_thread_epoch = _Init_global_epoch; // _Init_global_epoch = INT_MIN
}

_Init_thread_unlock(); // 离开临界区
}

_Init_thread_footer的源代码如下:

// 代码所在的文件为thread_safe_statics.cpp
extern "C" void __cdecl _Init_thread_footer(int* const pOnce) noexcept
{
_Init_thread_lock();
++_Init_global_epoch;
*pOnce = _Init_global_epoch;
_Init_thread_epoch = _Init_global_epoch;
_Init_thread_unlock();
_Init_thread_notify();
}

从上述代码来看,事情非常明了。pOnce指向的内存初始值为0,通过临界区来实现线程同步,同时只能有一个线程进入到_Init_thread_header和_Init_thread_footer函数。

◆CASE ONE

若t1线程首先进入func_b,执行顺序如下:_Init_thread_header -> res = b + a -> _Init_thread_footer,此时pOnce = INT_MIN+1。

t2线程进入func_b,此时pOnce = INT_MIN+1,既不是0也不是-1。进入_Init_thread_header之后,pOnce的值不会改变。自然而然,res的值也不会发生改变。t3和t4线程的执行情况同t2线程。

◆CASE TWO

若res的初始化需要耗费一定的时间,比如:res = func_c(),func_c函数中调用一个sleep函数。

那么此时的执行情况如下:

a. t1线程,_Init_thread_header -> res的初始化流程。

b. t2线程,进入到_Init_thread_header,执行else分支中的while循环。直到t1线程的res初始化过程结束,然后调用_Init_thread_footer修改pOnce的值,pOnce的值变为INT_MIN+1。t2线程会退出while循环,pOnce值不为-1,自然也就不会再次初始化。

c. 若t3或t4线程同t2线程,也执行func_b函数,则会被_Init_thread_lock函数阻塞。若t3或t4在t1执行_Init_thread_footer函数后调用func_b,这种情况同CASE ONE下。

因为线程是乱序执行的,所以res的结果不是确定的,如下图:

C++ static关键字引发的思考
C++ static关键字引发的思考


Linux下的G++编译器的实现


源码的编译环境是Ubuntu 22.04,g++ 11.4.0

静态局部变量func_a_value


同Windows下的常量赋值给静态局部变量,IDA的视图如下:

C++ static关键字引发的思考

变量赋值给静态局部变量见"静态局部变量res"章节。

静态局部变量res


IDA反汇编func_b的结果如下:

C++ static关键字引发的思考

从该图中可以看出,__cxa_guard_acquire发挥同_Init_thread_header相同的效果,而__cxa_guard_release发挥同_Init_thread_footer相同的效果。这两个函数的源码如下:

__cxa_guard_acquire函数源码

// guard.cc
int __cxa_guard_acquire (__guard *g) // typedef int __guard, 初始值为0
{
if (_GLIBCXX_GUARD_TEST_AND_ACQUIRE (g)) //
return 0;

if (__gnu_cxx::__is_single_threaded()) // 调用pthread_create时,__gnu_cxx::__is_single_threaded() 为false
{
// No need to use atomics, and no need to wait for other threads.
int *gi = (int *) (void *) g;
if (*gi == 0)
{
*gi = _GLIBCXX_GUARD_PENDING_BIT; // 0x100
return 1;
}
else
{
throw_recursive_init_exception();
}
}
else
{
int *gi = (int *) (void *) g;
const int guard_bit = _GLIBCXX_GUARD_BIT; // 1
const int pending_bit = _GLIBCXX_GUARD_PENDING_BIT; // 0x100
const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT; // 0x10000

while (1)
{
int expected(0);
if (__atomic_compare_exchange_n(gi, &expected, pending_bit, false,
__ATOMIC_ACQ_REL,
__ATOMIC_ACQUIRE))
{
return 1; // This thread should do the initialization.
}

if (expected == guard_bit)
{
// Already initialized.
return 0;
}

if (expected == pending_bit)
{
// Use acquire here.
int newv = expected | waiting_bit; // 0x10100
if (!__atomic_compare_exchange_n(gi, &expected, newv, false,
__ATOMIC_ACQ_REL,
__ATOMIC_ACQUIRE))
{
if (expected == guard_bit) // 1
{
// Make a thread that failed to set the
// waiting bit exit the function earlier,
// if it detects that another thread has
// successfully finished initialising.
return 0;
}
if (expected == 0)
continue;
}

expected = newv;
}

syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAIT, expected, 0);
}
}
return acquire (g);
}

__cxa_guard_release函数源码

// guard.cc
extern "C" void __cxa_guard_release (__guard *g) noexcept
{
// If __atomic_* and futex syscall are supported, don't use any global
// mutex.
if (__gnu_cxx::__is_single_threaded())
{
int *gi = (int *) (void *) g;
*gi = _GLIBCXX_GUARD_BIT; // 1
return;
}
else
{
int *gi = (int *) (void *) g;
const int guard_bit = _GLIBCXX_GUARD_BIT;
const int waiting_bit = _GLIBCXX_GUARD_WAITING_BIT;
int old = __atomic_exchange_n (gi, guard_bit, __ATOMIC_ACQ_REL);

if ((old & waiting_bit) != 0)
syscall (SYS_futex, gi, _GLIBCXX_FUTEX_WAKE, INT_MAX);
return;
}
set_init_in_progress_flag(g, 0);
_GLIBCXX_GUARD_SET_AND_RELEASE (g);
}

像Windows MSVC平台那样分CASE分析,初始状态*g = 0,__gnu_cxx::__is_single_threaded() = true。
__gnu_cxx::__is_single_threaded()在pthread_create函数已经被设置为了false,这个操作在执行func_b函数之前。

◆CASE ONE

a. 若t1线程首先进入func_b,执行顺序如下:__cxa_guard_acquire-> res = b + a -> __cxa_guard_release,此时g = 0x1。

b. t2 线程进入时,因为
g = 0x1,t2在__cxa_guard_acquire函数的41行返回,因返回值为0,因此不会对res再次初始化。t3和t4所遇情况,同t2线程。

◆CASE TWO

res的初始化需要较长时间。此时的情况如下:

a. t1线程,__cxa_guard_acquire-> res的初始化流程,此时g = 0x100。

b. t2线程进入到__cxa_guard_acquire函数中,会执行syscall系统调用进行阻塞,此时g = 0x10100。直到在t1调用__cxa_guard_release解除t2线程的阻塞,*g = 1。

c. 若t3或t4线程同t2线程,也执行func_b函数,则会被syscall(SYS_futex)系统调用阻塞。若t3或t4在t1执行__cxa_guard_release函数后调用func_b,这种情况同CASE ONE下。

总结


◆使用常量初始化静态局部变量,MSVC和G++实现的方法相同。

◆使用变量初始化静态局部变量,包括:多线程和单线程,CASE ONE和CASE TWO各个线程面对情况是一样的,只不过是两种平台的同步机制不同而已。



C++ static关键字引发的思考


看雪ID:baolongshou

https://bbs.kanxue.com/user-home-738427.htm

*本文为看雪论坛优秀文章,由 baolongshou 原创,转载请注明来自看雪社区

C++ static关键字引发的思考

# 

原文始发于微信公众号(看雪学苑):C++ static关键字引发的思考

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月2日22:28:22
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   C++ static关键字引发的思考https://cn-sec.com/archives/2356929.html

发表评论

匿名网友 填写信息