编辑
2025-05-27
记录知识
0
请注意,本文编写于 31 天前,最后修改于 31 天前,其中某些信息可能已经过时。

目录

测试代码
复现问题
ASAN检测
分析问题
使用gdb佐证
总结
参考链接

asan提供了定位全局对象的构造顺序相关的方法,本文详细了解一下关于c/c++全局变量的构造顺序带来的问题

测试代码

为了实施这个bug,我们需要两个cpp文件,其代码如下。

# cat initialization_order_fiasco_1.cpp #include <stdio.h> extern int extern_global; int __attribute__((noinline)) read_extern_global() { return extern_global; } int x = read_extern_global() + 1; int main() { printf("%d\n", x); return 0; } # cat initialization_order_fiasco_2.cpp int foo() { return 2; } int extern_global = foo();

根据上面的代码,我们知道有两个全局变量extern_global和x

  • initialization_order_fiasco_1.cpp使用了extern_global变量,声明了x变量
  • initialization_order_fiasco_2.cpp声明了extern_global变量

复现问题

此时我们通过修改编译顺序来复现问题

g++ -g initialization_order_fiasco_1.cpp initialization_order_fiasco_2.cpp -o initialization_order_fiasco_1_2 g++ -g initialization_order_fiasco_2.cpp initialization_order_fiasco_1.cpp -o initialization_order_fiasco_2_1

此时运行initialization_order_fiasco_1_2,如下

# ./initialization_order_fiasco_1_2 1

如果运行initialization_order_fiasco_2_1,如下

# ./initialization_order_fiasco_2_1 3

可以发现,因为我们g++传入文件的顺序不一致,而两个cpp中关于全局变量extern_global存在依赖,所以就导致了问题产生,按照我们理想的想法,这里的值应该是3,但是有可能是1

ASAN检测

我们将得出正常结论的编译方式加入asan来检测,如下

g++ -fsanitize=address -g initialization_order_fiasco_2.cpp initialization_order_fiasco_1.cpp -o asan_2_1

注意,这里先加入initialization_order_fiasco_2.cpp后加入initialization_order_fiasco_1.cpp,也就是说先声明extern_global,再使用extern_global和声明x。
此时运行asan检测,没有上报问题。

现在我们将编译顺序调换,先声明x和使用extern_global,再声明extern_global

g++ -fsanitize=address -g initialization_order_fiasco_1.cpp initialization_order_fiasco_2.cpp -o asan_1_2

此时运行程序

# LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0 ASAN_OPTIONS=check_initialization_order=true ./asan_1_2 ================================================================= ==133524==ERROR: AddressSanitizer: initialization-order-fiasco on address 0x0000004121e0 at pc 0x000000400948 bp 0x007fc3aff370 sp 0x007fc3aff390 READ of size 4 at 0x0000004121e0 thread T0 #0 0x400944 in read_extern_global() /root/asan/initialization_order/initialization_order_fiasco_1.cpp:4 #1 0x400a10 in __static_initialization_and_destruction_0 /root/asan/initialization_order/initialization_order_fiasco_1.cpp:6 #2 0x400a90 in _GLOBAL__sub_I__Z18read_extern_globalv /root/asan/initialization_order/initialization_order_fiasco_1.cpp:10 #3 0x400c54 in __libc_csu_init (/root/asan/initialization_order/asan_1_2+0x400c54) #4 0x7fa15efd34 in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d34) #5 0x400828 (/root/asan/initialization_order/asan_1_2+0x400828) 0x0000004121e0 is located 0 bytes inside of global variable 'extern_global' defined in 'initialization_order_fiasco_2.cpp:2:5' (0x4121e0) of size 4 registered at: #0 0x7fa17b7a10 (/usr/lib/aarch64-linux-gnu/libasan.so.5.0.0+0x3aa10) #1 0x400bec in _sub_I_00099_1 (/root/asan/initialization_order/asan_1_2+0x400bec) #2 0x400c54 in __libc_csu_init (/root/asan/initialization_order/asan_1_2+0x400c54) #3 0x7fa15efd34 in __libc_start_main (/lib/aarch64-linux-gnu/libc.so.6+0x20d34) #4 0x400828 (/root/asan/initialization_order/asan_1_2+0x400828) SUMMARY: AddressSanitizer: initialization-order-fiasco /root/asan/initialization_order/initialization_order_fiasco_1.cpp:4 in read_extern_global() Shadow bytes around the buggy address: 0x0010000823e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x0010000823f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082410: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 0x001000082420: 00 00 f9 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 =>0x001000082430: 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00[f6]f6 f6 f6 0x001000082440: f6 f6 f6 f6 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082450: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0x001000082480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 Shadow byte legend (one shadow byte represents 8 application bytes):

根据上面的信息,得出如下结论

  1. 0x0000004121e0地址出现了全局变量初始化顺序问题
  2. 产生的操作是在read_extern_global函数中读取0x0000004121e0地址上的4个字节
  3. 0x0000004121e0是全局变量extern_global
  4. extern_global的定义实际在initialization_order_fiasco_2.cpp的第2行
  5. 0xf6代表Global init order

根据上面的信息,我们返回代码补充一个信息

  1. initialization_order_fiasco_2.cpp的第2行代码是int extern_global = foo();默认情况下其值是2

所以我们根据上面已有信息,我们就清晰的定位出来extern_global的初始化顺序存在问题。

补充一下,关于0x0000004121e0,因为是全局变量,所以我们在运行前就可以获取验证一下, 确定asan报错是正常的。

# objdump -d -j .bss ./asan_1_2 00000000004121e0 <extern_global>:

分析问题

通过上面的内容,我们定位了extern_global的初始化顺序存在问题。其问题出现的原因在于g++的编译顺序。
我们先拆解g++的编译步骤,我们先编译.o文件

g++ -g -c initialization_order_fiasco_1.cpp -o 1.o g++ -g -c initialization_order_fiasco_2.cpp -o 2.o

此时针对1.o和2.o,我们读取其bss端的值如下

# objdump -d -j .bss 1.o 0000000000000000 <x>: 0: 00 00 00 00 # objdump -d -j .bss 2.o 0000000000000000 <extern_global>: 0: 00 00 00 00

可以看到,在编译阶段,全局变量加载地址还是0,它需要在链接阶段由ld填充实际地址。问题不出在编译阶段,那么接下来我们链接两个o文件

g++ 1.o 2.o -g -o 1_2 g++ 2.o 1.o -g -o 2_1

此时我们得到两个文件1_2/2_1。 对于libc函数调用的流程可以查看文章《程序的启动过程浅析》,静态变量会运行__static_initialization_and_destruction_0函数来初始化静态全局变量的值,对于glibc的流程如下

_start __libc_start_main __libc_csu_init _GLOBAL__sub_I_XXX (gcc) __static_initialization_and_destruction_0

这里需要注意的是_GLOBAL__sub_I_XXX是gcc为每个cpp文件生成的用于构造静态全局变量的构造函数,它的运行顺序就是每个cpp的添加顺序。
对于1_2程序,根据上面的推论,我们可以猜测其运行顺序如下

__libc_csu_init _GLOBAL__sub_I__Z18read_extern_globalv __static_initialization_and_destruction_0 _GLOBAL__sub_I__Z3foov __static_initialization_and_destruction_0

那么就是

  • 先通过int x = read_extern_global() + 1;完成了x的赋值
  • 然后通过int extern_global = foo();完成extern_global的赋值。

那么这个BUG就出现了。

而对于2_1程序,其正常的原因是如下,我们照常推理其运行顺序

__libc_csu_init _GLOBAL__sub_I__Z3foov __static_initialization_and_destruction_0 _GLOBAL__sub_I__Z18read_extern_globalv __static_initialization_and_destruction_0
  • 先通过int extern_global = foo();完成extern_global的赋值
  • 然后通过int x = read_extern_global() + 1;完成了x的赋值。

此时代码正常运行。

使用gdb佐证

根据上面的信息,我们重点在于__libc_csu_init调用CRT的顺序问题,我们可以对1_2程序打印断点。如下

# gdb ./1_2 (gdb) b _GLOBAL__sub_I__Z3foov (gdb) b _GLOBAL__sub_I__Z18read_extern_globalv (gdb) b __static_initialization_and_destruction_0

为了更好的判断extern_global的变量,这里挂上awatch,如下

(gdb) awatch extern_global Hardware access (read/write) watchpoint 4: extern_global

此时运行结果如下

(gdb) r Starting program: /root/asan/initialization_order/1_2 [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1". Breakpoint 2, _GLOBAL__sub_I__Z18read_extern_globalv () at initialization_order_fiasco_1.cpp:10 10 } (gdb) c Continuing. Breakpoint 3, __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at initialization_order_fiasco_1.cpp:10 10 } (gdb) c Continuing. Hardware access (read/write) watchpoint 4: extern_global Value = 0 read_extern_global () at initialization_order_fiasco_1.cpp:5 5 } (gdb) c Continuing. Breakpoint 1, 0x00000000004006a8 in _GLOBAL__sub_I__Z3foov () at initialization_order_fiasco_2.cpp:2 2 int extern_global = foo(); (gdb) c Continuing. Breakpoint 3, __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at initialization_order_fiasco_2.cpp:2 2 int extern_global = foo(); (gdb) c Continuing. Hardware access (read/write) watchpoint 4: extern_global Old value = 0 New value = 2 0x000000000040068c in __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535) at initialization_order_fiasco_2.cpp:2 2 int extern_global = foo(); (gdb) c Continuing. 1 [Inferior 1 (process 137076) exited normally]

可以看到,出问题的程序1_2,在_GLOBAL__sub_I__Z18read_extern_globalv中extern_global的值默认是0,由运行时默认赋值,然后在_GLOBAL__sub_I__Z3foov中修改值为2,由代码初始化赋值,但是此时x的值已经在GLOBAL__sub_I__Z18read_extern_globalv中取到为1了,所以问题出现了。

总结

本文讨论了全局变量的顺序问题导致的BUG,此问题的原因主要在于gcc的链接过程中cpp的顺序会导致__libc_csu_init运行_GLOBAL__sub_I_XXX的运行顺序问题,从而导致问题的出现。

参考链接

关于此问题的官方解释,有兴趣的可以翻阅,本文做简单的介绍:

"static initialization order problem":这里描述了多个静态类初始化如果存在依赖问题,则程序50%概率崩溃,解决办法是:"Construct On First Use"

https://isocpp.org/wiki/faq/ctors#static-init-order

"Why doesn’t the Construct On First Use Idiom use a static object instead of a static pointer":这里描述首次构造时应该使用对象而不是指针,因为指针会内存泄漏,但是如果使用对象,需要注意静态对象的销毁顺序问题。

https://isocpp.org/wiki/faq/ctors#static-init-order-on-first-use

"What is a technique to guarantee both static initialization and static deinitialization? ":静态对象构造和析构的技术要点

https://isocpp.org/wiki/faq/ctors#construct-on-first-use-v2

"How do I prevent the “static initialization order problem” for my static data members?":通过static Fred& x = X::x();防止初始化问题

https://isocpp.org/wiki/faq/ctors#nifty-counter-idiom

简单来说就说,如果非要使用静态全局对象,那么需要注意上述的每个要点,且都有利弊,否则不推荐频繁使用静态全局对象。