本文分析了局部静态对象的构造和析构过程。
1. 测试环境
- Linux ubuntu18arm64 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:25:58 UTC 2020 aarch64 aarch64 aarch64 GNU/Linux
- gcc version 7.4.0 (Ubuntu/Linaro 7.4.0-1ubuntu1~18.04.1)
- glibc 2.27
- c++11
2. 调试分析
局部静态对象, 就是在函数内定义的静态对象。
只有当函数第一次调用时,该对象才执行初始化,否则保持当前状态,后续不再初始化。
2.1 测试源码
1 |
|
local_static()反汇编如下:
1 | (gdb) disas |
2.2 构造
局部静态对象的构造大概分为以下几步
- 判断guard variable
- 调用__cxa_guard_acquire()
- 调用构造函数
- 调用__cxa_guard_release()
- 注册析构函数
2.2.1 判断guard variable
每个局部静态对象,编译器为其生成guard variable,用于标识对应的局部静态对象是否已初始化并确保线程安全
1 | nm out/bin/objects | grep local_static |
假设guard variable为gv, int *pgv = &gv,
gv的三个flag含义如下:
- pgv[0]为guard bit, 标识是否完成初始化
- pgv[1]为pending bit,标识是否正在初始化
- pgv[2]为waiting bit,标识是否等待初始化
在本例中,guard variable位于0xaaaaaaabc000 + 0x30 = 0xaaaaaaabc030
1 | => 0x0000aaaaaaaaaf3c <+8>: adrp x0, 0xaaaaaaabc000 |
若局部静态对象未初始化或正在初始化,则第1次load的w0为0x00, 判断为0后再设置为1, 表示要继续往下执行__cxa_guard_acquire()
若局部静态对象已完成初始化,则load的w0为1,判断为非0后再设置为0, 则不再执行__cxa_guard_acquire()和调用构造函数,直接执行局部静态对象后面的代码(0xaaaaaaaaafbc)。
2.2.2 __cxa_guard_acquire()
源文件: gcc/libstdc++-v3/libsupc++/guard.cc
关键代码如下
1 | if (__gthread_active_p ()) |
若局部静态对象未初始化过, 则这三个标志均为0x00
1
2(gdb) x/1xg 0xaaaaaaabc030
0xaaaaaaabc030 <_ZGVZ12local_staticvE16local_static_obj>: 0x0000000000000000若第1个线程刚进来,则设置pending bit, 标识局部静态对象正在初始化
1
2(gdb) x/1xg 0xaaaaaaabc030
0xaaaaaaabc030 <_ZGVZ12local_staticvE16local_static_obj>: 0x0000000000000100若第1个线程未完成,第2个线程又进来尝试初始化,则设置waiting bit, 并调用futex系统调用进入休眠状态(当第1个线程完成初始化后,会再唤醒正在休眠的线程)
2.2.3 调用构造函数
第1个线程从__cxa_guard_acquire()返回1后,下一步调用构造函数进行初始化
1 | 0x0000aaaaaaaaaf84 <+80>: adrp x0, 0xaaaaaaabc000 |
传给构造函数的参数如下
- 第1个参数是局部静态对象的地址x0 = 0xaaaaaaabc028
- 第2个参数是w1 = 3
2.2.4 __cxa_guard_release()
源文件: gcc/libstdc++-v3/libsupc++/guard.cc
__cxa_guard_release()的实现如下:
1 | extern "C" |
该函数完成的主要功能如下
设置guard variable
1
2(gdb) x/1xg 0xaaaaaaabc030
0xaaaaaaabc030 <_ZGVZ12local_staticvE16local_static_obj>: 0x0000000000000001这里只设置了guard bit, pending bit和waiting bit已清除, 标识局部静态对象已完成初始化。
若waiting_bit已设置,则调用futex()唤醒正在休眠的线程。休眠线程醒来,会再次检查: 若guard bit已设置,则返回0,不必再执行初始化。
2.2.5 注册析构函数
在local_static()函数的最后,
1 | 0x0000aaaaaaaaafa0 <+108>: adrp x0, 0xaaaaaaabc000 |
这里调用__cxa_atexit()注册局部静态对象的析构函数
- 第1个参数x0 = 0x0000aaaaaaaab0bc是Base::~Base()
- 第2个参数x1 = 0xaaaaaaabc028是局部静态对象的地址
- 第3个参数x2 = 0xaaaaaaabc008是dso handler
__cxa_atexit()的实现如下
源文件: glibc/stdlib/cxa_atexit.c
1 | int |
__exit_funcs是个结构体指针, 指向initial, 以链表形式存放
1 | static struct exit_function_list initial; |
2.3 析构
局部静态对象析构时的堆栈如下:
1 | (gdb) bt |
exit()的实现如下
源文件: glibc/stdlib/exit.c
1 | /* Call all functions registered with `atexit' and `on_exit', |
可以看到, 程序在退出前,调用exit()->run_exit_handlers(), 传入中的是exit_funcs, 正是之前注册析构函数的地方。
3. 总结
- 局部静态对象在函数第一次调用时完成初始化,这在C++11后是线程安全的。同时注册了析构函数。
- 局部静态对象拥有和程序同样的生命周期,在程序exit时会调用之前注册的析构函数。