The singleton pattern in C++11 is one of the best practices we are familiar with. It utilizes the characteristics of static variables to safely implement lazy initialization, thread safety, and automatic memory management with very concise code.
The implementation of the lazy singleton pattern in C++11 is roughly as follows:
#include <iostream>
class Singleton {
public:
static Singleton& getInstance() {
static Singleton ms;
return ms;
}
void func() {
std::cout << __func__ << std::endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
Singleton() = default;
};
int main() {
Singleton::getInstance().func();
return 0;
}
The key points of this implementation are as follows:
- • The default constructor must be private.
- • Copying and assignment should be prohibited.
- • The
getInstancestatic member function returns a staticSingleton.
When using it, we can directly call Singleton::getInstance() to obtain the singleton object. This makes the code concise and easy to understand, providing great convenience for our development.
The lifetime of static variables is throughout the entire runtime of the program, and for C++11 and above, the static member function returns a static variable, ensuring that the initialization of the variable is thread-safe. This means that when we access this variable, all its resources have been initialized, and there is no situation where memory is allocated but not fully constructed before being returned.
At that time, I had a question while using it:
When is the position of the above
msdetermined?
Position of ms in ELF
Before verifying, we need to compile the above program into an executable file.
Then we use the objdump tool to view the disassembly code related to getInstance:
g++ -g test_singleton_static.cpp -o test_singleton_static
objdump -S test_singleton_static | c++filt
In the output, we focus on the assembly part of getInstance:
0000000000001234 <Singleton::getInstance()>:
static Singleton& getInstance() {
1234: f3 0f 1e fa endbr64
1238: 55 push %rbp
1239: 48 89 e5 mov %rsp,%rbp
return ms;
123c: 48 8d 05 0f 2f 00 00 lea 0x2f0f(%rip),%rax # 4152 <Singleton::getInstance()::ms>
}
1243: 5d pop %rbp
1244: c3 ret
1245: 90 nop
Now let’s explain the above assembly code:
Function Header Information
0000000000001234 <Singleton::getInstance()>:
- •
0000000000001234: The address of the function in memory. - •
<Singleton::getInstance()>: The function name.
Function Prologue
static Singleton& getInstance() {
1234: f3 0f 1e fa endbr64
1238: 55 push %rbp
1239: 48 89 e5 mov %rsp,%rbp
- •
endbr64: Intel CET (Control-flow Enforcement Technology) instruction to prevent ROP/JOP attacks. - •
push %rbp: Save the caller’s stack base pointer. - •
mov %rsp,%rbp: Set the current function’s stack base pointer.
Core Statement
return ms;
123c: 48 8d 05 0f 2f 00 00 lea 0x2f0f(%rip),%rax # 4152 <Singleton::getInstance()::ms>
This is the most critical line:
- •
lea: Load Effective Address, loads the effective address. - •
0x2f0f(%rip): Uses RIP-relative addressing to calculate the address of the static variablems. - •
%rax: Stores the address in the return value register. - •
# 4152 <Singleton::getInstance()::ms>: Comment showing the actual address and variable name.
Function Epilogue
}
1243: 5d pop %rbp
1244: c3 ret
1245: 90 nop
- •
pop %rbp: Restores the caller’s stack base pointer. - •
ret: Returns to the caller. - •
nop: No operation, used for alignment.
We look at the core statement above regarding the address calculation of the static variable ms:
Actual Address = RIP + Offset
- • RIP: Points to the address of the next instruction.
- • Offset: Here it is
0x2f0f.
So the calculation is: current instruction address 123c + offset 0x2f0f + instruction length (7 bytes) 7 = 4152.
We have found the address of the static variable ms, but where is the address of ms located in the ELF? We can check the relevant information of the test_singleton_static section:
objdump -h test_singleton_static | c++filt
The output is as follows:
test_singleton_static: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000000318 0000000000000318 00000318 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.gnu.property 00000030 0000000000000338 0000000000000338 00000338 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 0000000000000368 0000000000000368 00000368 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .note.ABI-tag 00000020 000000000000038c 000000000000038c 0000038c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gnu.hash 00000028 00000000000003b0 00000000000003b0 000003b0 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynsym 00000138 00000000000003d8 00000000000003d8 000003d8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .dynstr 0000016e 0000000000000510 0000000000000510 00000510 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version 0000001a 000000000000067e 000000000000067e 0000067e 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .gnu.version_r 00000050 0000000000000698 0000000000000698 00000698 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.dyn 00000120 00000000000006e8 00000000000006e8 000006e8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .rela.plt 00000060 0000000000000808 0000000000000808 00000808 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
11 .init 0000001b 0000000000001000 0000000000001000 00001000 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .plt 00000050 0000000000001020 0000000000001020 00001020 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .plt.got 00000010 0000000000001070 0000000000001070 00001070 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .plt.sec 00000040 0000000000001080 0000000000001080 00001080 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
15 .text 000001c4 00000000000010c0 00000000000010c0 000010c0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
16 .fini 0000000d 0000000000001284 0000000000001284 00001284 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
17 .rodata 00000009 0000000000002000 0000000000002000 00002000 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
18 .eh_frame_hdr 00000054 000000000000200c 000000000000200c 0000200c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
19 .eh_frame 0000012c 0000000000002060 0000000000002060 00002060 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
20 .init_array 00000010 0000000000003d78 0000000000003d78 00002d78 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .fini_array 00000008 0000000000003d88 0000000000003d88 00002d88 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .dynamic 00000200 0000000000003d90 0000000000003d90 00002d90 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .got 00000070 0000000000003f90 0000000000003f90 00002f90 2**3
CONTENTS, ALLOC, LOAD, DATA
24 .data 00000010 0000000000004000 0000000000004000 00003000 2**3
CONTENTS, ALLOC, LOAD, DATA
25 .bss 00000118 0000000000004040 0000000000004040 00003010 2**6
ALLOC
26 .comment 0000002d 0000000000000000 0000000000000000 00003010 2**0
CONTENTS, READONLY
27 .debug_aranges 00000050 0000000000000000 0000000000000000 0000303d 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
28 .debug_info 000024f6 0000000000000000 0000000000000000 0000308d 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
29 .debug_abbrev 00000655 0000000000000000 0000000000000000 00005583 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
30 .debug_line 00000184 0000000000000000 0000000000000000 00005bd8 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
31 .debug_str 000012bf 0000000000000000 0000000000000000 00005d5c 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
32 .debug_line_str 0000028b 0000000000000000 0000000000000000 0000701b 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
33 .debug_rnglists 0000002c 0000000000000000 0000000000000000 000072a6 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
We focus on the information in the third column (VMA):
........
25 .bss 00000118 0000000000004040 0000000000004040 00003010 2**6
ALLOC
26 .comment 0000002d 0000000000000000 0000000000000000 00003010 2**0
CONTENTS, READONLY
........
We can see that the static variable ms resides in the .bss section!
Through the following readelf command:
readelf -x .bss test_singleton_static
We can also know that static variables generally do not occupy actual space in ELF.
Similarly, readelf can also check the relevant information of the section headers:
readelf -S test_singleton_static | c++filt
The output is as follows, confirming the result:
There are 38 section headers, starting at offset 0x7c70:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000030 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000368 00000368
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b0
0000000000000028 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 000003d8
0000000000000138 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000510 00000510
000000000000016e 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 000000000000067e 0000067e
000000000000001a 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000698 00000698
0000000000000050 0000000000000000 A 7 2 8
[10] .rela.dyn RELA 00000000000006e8 000006e8
0000000000000120 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000808 00000808
0000000000000060 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000050 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001070 00001070
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000001080 0000000000001080
0000000000000040 0000000000000010 AX 0 0 16
[16] .text PROGBITS 00000000000010c0 000010c0
00000000000001c4 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 0000000000001284 00001284
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
0000000000000009 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 000000000000200c 0000200c
0000000000000054 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002060 00002060
000000000000012c 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003d78 00002d78
0000000000000010 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003d88 00002d88
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003d90 00002d90
0000000000000200 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003f90 00002f90
0000000000000070 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004040 00003010
0000000000000118 0000000000000000 WA 0 0 64
[27] .comment PROGBITS 0000000000000000 00003010
000000000000002d 0000000000000001 MS 0 0 1
[28] .debug_aranges PROGBITS 0000000000000000 0000303d
0000000000000050 0000000000000000 0 0 1
[29] .debug_info PROGBITS 0000000000000000 0000308d
00000000000024f6 0000000000000000 0 0 1
[30] .debug_abbrev PROGBITS 0000000000000000 00005583
0000000000000655 0000000000000000 0 0 1
[31] .debug_line PROGBITS 0000000000000000 00005bd8
0000000000000184 0000000000000000 0 0 1
[32] .debug_str PROGBITS 0000000000000000 00005d5c
00000000000012bf 0000000000000001 MS 0 0 1
[33] .debug_line_str PROGBITS 0000000000000000 0000701b
000000000000028b 0000000000000001 MS 0 0 1
[34] .debug_rnglists PROGBITS 0000000000000000 000072a6
000000000000002c 0000000000000000 0 0 1
[35] .symtab SYMTAB 0000000000000000 000072d8
0000000000000480 0000000000000018 36 21 8
[36] .strtab STRTAB 0000000000000000 00007758
000000000000039d 0000000000000000 0 0 1
[37] .shstrtab STRTAB 0000000000000000 00007af5
000000000000017a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
R (retain), D (mbind), l (large), p (processor specific)
We can see that the static variable ms is located in the .bss section!
Next, let’s explore how the thread safety of static variable initialization is achieved.
Thread Safety of ms Initialization
Initially, I wanted to use gdb to directly debug the above program, but I found that when I set a breakpoint at the line with static Singleton ms in the getInstance function, it would automatically skip that line and set the breakpoint at return ms, making it impossible to observe the initialization process.
I checked the assembly code at getInstance during gdb runtime:
(gdb) disassemble
Dump of assembler code for function _ZN9Singleton11getInstanceEv:
0x0000555555555234 <+0>: endbr64
0x0000555555555238 <+4>: push %rbp
0x0000555555555239 <+5>: mov %rsp,%rbp
=> 0x000055555555523c <+8>: lea 0x2f0f(%rip),%rax # 0x555555558152 <_ZZN9Singleton11getInstanceEvE2ms>
0x0000555555555243 <+15>: pop %rbp
0x0000555555555244 <+16>: ret
End of assembler dump.
(gdb)
As we can see here, the address of ms has become 0x555555558152. This is because the address observed in objdump is relative virtual address, which is the offset when assuming loaded from address 0, and is statically determined in ELF. 0x555555558152 is the actual address at runtime.
Here, similar to the behavior observed in the objdump assembly code, I did not see the initialization process of ms. I suspect that our program is too simple and does not involve inter-thread initialization operations, leading the compiler to optimize it away.
So I decided to modify the code to involve threads:
#include <iostream>
#include <thread>
class Singleton {
public:
static Singleton& getInstance() {
static Singleton ms;
return ms;
}
void func() {
std::cout << __func__ << " m_a=" << m_a << std::endl;
}
private:
Singleton() : m_a(42) { // Non-zero initialization, forces constructor call
std::cout << "Singleton constructor called" << std::endl;
}
int m_a;
};
int main() {
std::thread t([]() {
Singleton::getInstance().func();
});
t.detach();
Singleton::getInstance().func();
return 0;
}
- • Added
std::threadto start a thread and obtain the singleton object to executefuncwithin the thread. - • Added a variable
m_atoSingletonand performed non-zero initialization, expecting the compiler to force the constructor to be called so that we can observe the initialization process.
I plan to use another method, not using gdb to view the assembly content, but to use compilation parameters and options to generate detailed annotated assembly code for easier browsing:
g++ -S -fverbose-asm test_singleton_static.cpp -o test_singleton_static.s
Let me explain the parameters and compilation options of this command:
- •
-S: Stops at the assembly stage without linking; outputs the assembly code file (.s). - •
-fverbose-asm: Adds detailed comments in the assembly code (including: original C++ code line numbers and content, variable names and function names, register usage descriptions, compiler optimization information, etc.).
You can try this in your own Linux environment; I won’t paste the entire file content here, but we will focus on the initialization process:
- 1. Definition of static variable ms
- 2. Complete thread-safe initialization process
- 3. Calling the constructor
- 4. Release initialization lock
- 5. Exception safety handling
# ms variable itself (4 bytes, stores Singleton object)
_ZZN9Singleton11getInstanceEvE2ms:
.zero 4
# Guard Variable (8 bytes, used for thread safety control)
_ZGVZN9Singleton11getInstanceEvE2ms:
.zero 8
# 1. Check if already initialized
movzbl _ZGVZN9Singleton11getInstanceEvE2ms(%rip), %eax
testb %al, %al
sete %al
testb %al, %al
je .L11 # If initialized, jump to return
# 2. Acquire initialization lock (thread-safe)
leaq _ZGVZN9Singleton11getInstanceEvE2ms(%rip), %rax
movq %rax, %rdi
call __cxa_guard_acquire@PLT
testl %eax, %eax
# 3. Call Singleton constructor
leaq _ZZN9Singleton11getInstanceEvE2ms(%rip), %rax
movq %rax, %rdi
call _ZN9SingletonC1Ev # Constructor call
# 4. Release initialization lock
leaq _ZGVZN9Singleton11getInstanceEvE2ms(%rip), %rax
movq %rax, %rdi
call __cxa_guard_release@PLT
# If constructor throws an exception, call guard_abort
leaq _ZGVZN9Singleton11getInstanceEvE2ms(%rip), %rax
movq %rax, %rdi
call __cxa_guard_abort@PLT
- •
__cxa_guard_acquire: Atomically checks and acquires the initialization lock. - •
__cxa_guard_release: Marks initialization as complete and releases the lock. - •
__cxa_guard_abort: Cleans up the lock state in case of exceptions.
Thus, we understand how GCC ensures thread safety during the initialization process of the static variable ms. We summarize this with a flowchart:
Initialized = 1
Uninitialized = 0
Failure
Success
Success
Exception
Call getInstance()
Check Guard Variable movzbl _ZGVZN...
Directly return ms reference leaq ms_address
Call __cxa_guard_acquire
Lock acquired successfully?
Wait for other threads to complete initialization
Call constructor SingletonC1Ev
Construction successful?
Call __cxa_guard_release mark initialization complete
Call __cxa_guard_abort clean up lock state
Throw exception
Return singleton reference
Conclusion
This article delves into the underlying implementation mechanism of local static variables in the C++11 lazy singleton pattern, with the main findings as follows:
1. Memory Layout of Static Variables
- • Local static variable
msis stored in the.bsssection of the ELF file. - • The ELF file shows relative virtual addresses (e.g.,
0x4152), which are relocated to actual addresses (e.g.,0x555555558152) at runtime. - • Uses
RIPrelative addressing to access static variables.
2. Thread Safety Mechanism
- • The compiler generates complete thread-safe initialization code for complex constructors.
- • Atomic operations are implemented through C++ ABI functions:
- •
__cxa_guard_acquire: Acquires the initialization lock. - •
__cxa_guard_release: Marks initialization as complete. - •
__cxa_guard_abort: Cleans up the lock state in case of exceptions.
3. Compiler Optimization
- • For simple zero-initialized classes, the compiler skips complex initialization mechanisms.
- • Non-zero initialized constructors force the generation of complete thread-safe code.
- • The
PLT/GOTmechanism implements delayed binding of dynamic library functions.