Exploring Static Variables in C++11 Lazy Singleton Pattern

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 getInstance static member function returns a static Singleton.

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 ms determined?

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 variable ms.
  • %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::thread to start a thread and obtain the singleton object to execute func within the thread.
  • • Added a variable m_a to Singleton and 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. 1. Definition of static variable ms
  2. # ms variable itself (4 bytes, stores Singleton object)
    _ZZN9Singleton11getInstanceEvE2ms:
        .zero 4
    
    # Guard Variable (8 bytes, used for thread safety control)
    _ZGVZN9Singleton11getInstanceEvE2ms:
        .zero 8
  3. 2. Complete thread-safe initialization process
  4. # 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
  5. 3. Calling the constructor
  6. # 3. Call Singleton constructor
    leaq   _ZZN9Singleton11getInstanceEvE2ms(%rip), %rax
    movq   %rax, %rdi
    call   _ZN9SingletonC1Ev    # Constructor call
  7. 4. Release initialization lock
  8. # 4. Release initialization lock
    leaq   _ZGVZN9Singleton11getInstanceEvE2ms(%rip), %rax
    movq   %rax, %rdi
    call   __cxa_guard_release@PLT
  9. 5. Exception safety handling
  10. # 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 ms is stored in the .bss section 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 RIP relative 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/GOT mechanism implements delayed binding of dynamic library functions.

Leave a Comment