C++ static关键字引发的思考
2024-1-2 18:5:11 Author: 看雪学苑(查看原文) 阅读量:2 收藏


基本用法

在面向对象中的用法

在类中,可以使用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对应的位置,这与单(多)线程无关,与单(多)次初始化无关。



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

静态局部变量res

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



对应的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的结果不是确定的,如下图:


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

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

静态局部变量func_a_value

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



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

静态局部变量res

IDA反汇编func_b的结果如下:



从该图中可以看出,__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各个线程面对情况是一样的,只不过是两种平台的同步机制不同而已。

看雪ID:baolongshou

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

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

# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua

球分享

球点赞

球在看


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458532982&idx=1&sn=3f81cf99aef7ab0c00c1cde85376d810&chksm=b036c40fc4de9b2452ce8059035b29419167d1f7f8b1cf4a81f361d5e68d8822e120e776b15f&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh