gcc attribute: constructor & destructor

Explore the internal process and usage of gcc attributes: constructor (pre-main execution) and destructor (post-main execution).
March 9, 2026

gcc attribute: constructor & destructor

Reqruies
  • Compiler: gcc 2.7.2 or later
In C++, the concepts of constructor, called when a class instance is created, and destructor, called when its lifecycle ends, already exist.
Unfortunately, the C language does not natively have the concepts of constructor and destructor for creation and destruction like object-oriented languages.
💡 Note: Even in C, you can achieve similar effects by indirectly hooking functions using weak symbols and the LD_PRELOAD technique.
However, the GCC compiler provides constructor attribute, called before the main function executes, and destructor attribute, called after the main function finishes, as extension features to supplement this.
gcc-4.7.0/Function-Attributes
Doc

constructor destructor constructor (priority) destructor (priority)


The constructor attribute causes the function to be called automatically before execution enters main(). Similarly, the destructor attribute causes the function to be called automatically after main() has completed or exit() has been called.

Functions with these attributes are useful for initializing data that will be used implicitly during program execution.

You can optionally provide an integer priority to control the execution order of constructor and destructor functions.

Constructors with smaller priority numbers are executed earlier, while destructors have the opposite relationship.

Therefore, if you have a constructor that allocates a resource and a destructor that releases the same resource, they typically share the same priority.

The priorities of constructor and destructor functions are the same as those specified for C++ objects.

The usage is very simple.
usage.c
c
Compile & Run
bash
As you can see, ctor_func defined using attribute((constructor)) is called first, and dtors_func defined using attribute((destructor)) is called after the main function.
Now, let's look at how constructors and destructors are called internally.

Internal Process of Constructor and Destructor

First, let's check how GCC manages and calls them.
Before GCC 4.7, constructors were managed using the .ctors section of ELF, and destructors were managed using the .dtors section.
On the other hand, from GCC 4.7 onwards, TARGET_ASM_CONSTRUCTOR is defined as default_elf_init_array_asm_out_constructor, which calls get_elf_initfini_array_priority_section and stores the functions in the .init_array and .fini_array sections of ELF.
Let's take a closer look at this calling process.
We will use .init_array as the reference.
First, check the start address of the sample code created earlier.
Check start address
bash
You can see the start address above.
This value is the e_entry value in the ELF header.
Using objdump to find the function at the start address confirms that it is the _start function.
The starting position of ELF
bash
Examining this _start function, it has a structure that internally stores the addresses of __libc_csu_init, __libc_csu_fini, and the main function at the register level, and then calls _libc_start_main.
__libc_start_main is an important function that processes csu (C start up) or crt (C runtime) routines defined within glibc before calling main(), the program's entry point.
As a side note, you can see the linking process by using the -v option when building with GCC.
Check link process
bash
The crt*.o object files located in GCC's sysroot directory are included to perform these startup routines.
Inside glibc's LIBC_START_MAIN, call_init() and call_fini() are called.
call_init() subsequently calls __init_array_start, and call_fini() calls __fini_array_start.
check libc-start.c in glibc
c
zenuml title Call init and fini sequence @Actor Kernel // 1. Kernel triggers the entry point Kernel -> _start: execve() // 2. _start prepares arguments and calls libc _start -> "__libc_start_main".init() { // 3. call_init Phase (Constructors) "__libc_start_main" -> "call_init".run() { while("for each func in .init_array") { "call_init" -> Contructor: execute() } } // 4. Actual Program Execution "__libc_start_main" -> main.run() // 5. Cleanup Phase (Destructors) "__libc_start_main" -> exit.process() { exit -> "call_fini".run() { while("for each func in .fini_array (Reverse)") { "call_fini" -> Destructor: execute() } } // 6. Final Control Return to Kernel @return Kernel: _exit() } }
Now, let's check which functions are actually registered and executed in init_array and fini_array.
Looking at the ELF dynamic section of the sample program, there are INIT_ARRAY / FINI_ARRAY and INIT_ARRAYSZ / FINI_ARRAYSZ.
Check ELF dynamic section
bash
Let's use the debugger (gdb) to dump the INIT_ARRAY memory area for the size of INIT_ARRAYSZ.
Inside INIT_ARRAY
bash
Disassembling the memory results from the x/2g command confirms that the start address of the explicitly declared ctor_func is registered in the INIT_ARRAY.
This principle applies equally to FINI_ARRAY.
Inside FINI_ARRAY
bash
The fact that function addresses are managed in an array means that multiple functions can be registered and called in sequence.
Therefore, by specifying priorities in individual attributes, a programmer can explicitly define the desired execution order.

Priorities

Supporting priorities: ctor_pri.c
c
Testing Priority values
bash
For constructors, smaller priority values result in earlier execution.
Conversely, for destructors, larger priority values result in earlier execution (at the beginning of the destruction phase) to take priority in the cleanup process.
You can freely specify priority values of 101 or higher.
What happens if multiple functions with the same priority are registered?
How are the same priorities handled?
c
Testing Identical Priorities
bash
From the output above, it is evident that when priority values are identical, functions are registered in the array in the order the source code was parsed and analyzed by the compiler, and they execute according to that sequential structure.
The final destructor calls also strictly follow the LIFO (Last-In, First-Out) principle of a stack, ensuring that the destructor for the first-registered constructor is called last, in a safe reverse order.

Behavior on Abnormal Termination

What if an exceptional situation occurs where the program terminates abnormally via a Signal Handler or an exit system call, rather than a normal return from the main function?
Let's further verify if the assigned destructor is correctly called even in these special cases.
Does it work in signal handler too?
c
Testing Signal Handler
bash
Even if a program is blocked in an infinite loop by while (1) and interrupted by an external SIGINT signal, explicitly calling exit(0) inside the signal handler ensures that dtors_func() is called normally with the intervention of the OS.

Leveraging in Shared Libraries

This constructor/destructor attribute control feature shows its true power when used with shared/dynamic libraries (Shared/Dynamic Library), not just in single executable files.
Even for target applications already compiled and distributed as binaries, there's no need to rebuild them or modify their source code.
By simply loading an external library equipped with constructors and destructors into memory (e.g., using LD_PRELOAD), you can easily inject desired code logic before and after the main function entry at any time.
Testing Shared Library (hooklib.c)
c
Testing LD_PRELOAD
bash
Of course, it can also be used directly in regular dynamic libraries linked to the application under development, not just with LD_PRELOAD.
Testing Shared Library Link (hooklib.c)
c
sample.c
c
Testing Shared Library Execution
bash

Conclusion

As demonstrated, leveraging these built-in GCC compiler attributes allows for a variety of technical approaches.
For example, it can be used as a safe backdoor hooking interface for indirect debugging of black-box binaries whose source code is lost or build is completed, or for optimization purposes such as preventing fatal memory leaks by controlling the dynamic allocation and deallocation of heavy global variables.
Jooojub
System S/W engineer
Explore Tags
Series
    Recent Post
    © 2026. jooojub. All right reserved.