Initialization and Finalization Functions constructor & destructor Attributes

 

42.4 — Initialization and Finalization Functions
constructor & destructor Attributes · _init() / _fini() | Chapter 42 · TLPI
EmbeddedPathashala.com — Free Embedded & Linux Tutorials

Automatic Setup and Teardown in Shared Libraries

Sometimes a shared library needs to perform initialization when it is first loaded and cleanup when it is unloaded. Examples include:

  • Allocating a global resource pool (thread pool, connection pool, buffer cache)
  • Registering the library with a central manager or logging system
  • Opening a log file or database connection
  • Setting up signal handlers or timers

You could require the caller to always call lib_init() and lib_cleanup() explicitly, but this is error-prone — callers forget. Linux provides a better way: constructor and destructor functions that run automatically when the library loads and unloads.

Key Concepts

__attribute__((constructor)) __attribute__((destructor)) Library load event Library unload event Multiple init/fini Priority ordering _init() _fini() -nostartfiles dlopen trigger

The Constructor and Destructor Attributes

GCC provides two special function attributes for initialization and finalization:

/* Syntax: mark a function to run automatically when the library LOADS */
void __attribute__ ((constructor)) my_lib_init(void)
{
    /* Initialization code runs here BEFORE any user code calls the library */
}

/* Syntax: mark a function to run automatically when the library UNLOADS */
void __attribute__ ((destructor)) my_lib_fini(void)
{
    /* Cleanup code runs here AFTER all user code is done with the library */
}

Key rules:

  • Both must return void and take no arguments.
  • The function names can be anything — the attribute is what matters.
  • You can define multiple constructor/destructor functions in the same library.
  • They work for both startup-loaded libraries and dlopen()-loaded libraries.
  • They also work in the main program (not just shared libraries).
Library Loaded
(dlopen or program start)

constructor() runs

Library In Use
Normal program execution,
user calls library functions
Library Unloaded
(dlclose or process exit)

destructor() runs

Multiple Constructor/Destructor Functions and Priority

A library can have multiple constructor functions. They run in an unspecified order unless you specify a priority:

/* Multiple constructors — order unspecified without priority */
void __attribute__((constructor)) init_logging(void) {
    printf("init_logging() called\n");
}

void __attribute__((constructor)) init_networking(void) {
    printf("init_networking() called\n");
}

/* With priority: lower number runs FIRST (before higher numbers) */
/* Priority range: 101 and above (0–100 are reserved for system use) */
void __attribute__((constructor(101))) init_first(void) {
    printf("init_first() — priority 101, runs earliest\n");
}

void __attribute__((constructor(200))) init_second(void) {
    printf("init_second() — priority 200, runs after 101\n");
}

/* Destructors with priority: higher number runs FIRST (reverse of constructors) */
void __attribute__((destructor(200))) cleanup_second(void) {
    printf("cleanup_second() — priority 200, runs first at unload\n");
}

void __attribute__((destructor(101))) cleanup_first(void) {
    printf("cleanup_first() — priority 101, runs last at unload\n");
}
Priority Constructor runs Destructor runs
101 (lower) First (earliest at load) Last (latest at unload)
200 (higher) Second First (earliest at unload)
Think of it as a stack: Constructors push onto a stack in priority order; destructors pop off in reverse. The last thing initialized is the first thing cleaned up — which is the correct dependency order.

The Old Approach: _init() and _fini() (Obsolete)

Before GCC constructor/destructor attributes existed, libraries used two special functions named exactly _init() and _fini():

/* OLD STYLE — DO NOT USE in new code */
#include <stdio.h>

/* Called when library is first loaded */
void _init(void) {
    printf("Library loaded via _init()\n");
}

/* Called when library is unloaded */
void _fini(void) {
    printf("Library unloaded via _fini()\n");
}
# Must use -nostartfiles to prevent the linker from including
# its own default _init/_fini stubs:
gcc -fPIC -shared -nostartfiles -o mylib.so mylib.c
Feature _init()/_fini() (old) constructor/destructor (modern)
Multiple functions allowed No — only one each Yes — unlimited
Works in main program No Yes
Priority ordering No Yes
Special linker flag needed -nostartfiles required None needed
Status Obsolete Current best practice

Code Example 1: Library with Automatic Init and Cleanup

/* resourcelib.c — library that manages a resource pool automatically */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define POOL_SIZE 8

/* Internal resource pool */
static char *pool[POOL_SIZE];
static int pool_initialized = 0;

/* Constructor — called automatically when library is loaded */
void __attribute__((constructor)) lib_startup(void) {
    printf("[resourcelib] Constructor: initializing resource pool\n");
    for (int i = 0; i < POOL_SIZE; i++) {
        pool[i] = malloc(256);
        if (pool[i]) {
            snprintf(pool[i], 256, "slot_%d", i);
        }
    }
    pool_initialized = 1;
    printf("[resourcelib] Pool ready: %d slots allocated\n", POOL_SIZE);
}

/* Destructor — called automatically when library is unloaded */
void __attribute__((destructor)) lib_shutdown(void) {
    printf("[resourcelib] Destructor: releasing resource pool\n");
    for (int i = 0; i < POOL_SIZE; i++) {
        free(pool[i]);
        pool[i] = NULL;
    }
    pool_initialized = 0;
    printf("[resourcelib] Pool freed\n");
}

/* Public API */
const char *pool_get(int slot) {
    if (!pool_initialized || slot < 0 || slot >= POOL_SIZE) return NULL;
    return pool[slot];
}

void pool_set(int slot, const char *value) {
    if (!pool_initialized || slot < 0 || slot >= POOL_SIZE) return;
    strncpy(pool[slot], value, 255);
    pool[slot][255] = '\0';
}
/* test_resourcelib.c */
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>

int main(void) {
    printf("[main] Loading library...\n");
    void *lib = dlopen("./resourcelib.so", RTLD_LAZY);
    if (!lib) { fprintf(stderr, "%s\n", dlerror()); exit(1); }

    const char *(*get_fn)(int);
    void (*set_fn)(int, const char *);
    dlerror();
    *(void **)(&get_fn) = dlsym(lib, "pool_get");
    *(void **)(&set_fn) = dlsym(lib, "pool_set");

    if (!dlerror()) {
        printf("[main] pool slot 0: %s\n", get_fn(0));
        set_fn(2, "modified_by_main");
        printf("[main] pool slot 2: %s\n", get_fn(2));
    }

    printf("[main] Unloading library...\n");
    dlclose(lib);
    printf("[main] Library unloaded\n");
    return 0;
}
gcc -fPIC -shared -o resourcelib.so resourcelib.c
gcc test_resourcelib.c -ldl -o test_resourcelib
./test_resourcelib

# Output:
# [main] Loading library...
# [resourcelib] Constructor: initializing resource pool
# [resourcelib] Pool ready: 8 slots allocated
# [main] pool slot 0: slot_0
# [main] pool slot 2: modified_by_main
# [main] Unloading library...
# [resourcelib] Destructor: releasing resource pool
# [resourcelib] Pool freed
# [main] Library unloaded

Code Example 2: Multiple Constructors with Priority Ordering

/* multi_init.c — demonstrates multiple init functions and their order */
#include <stdio.h>

/* These run in order of ASCENDING priority number at load time */
void __attribute__((constructor(101))) setup_config(void) {
    printf("[INIT-101] setup_config: loading configuration\n");
}

void __attribute__((constructor(102))) setup_logging(void) {
    printf("[INIT-102] setup_logging: opening log file\n");
}

void __attribute__((constructor(103))) setup_network(void) {
    printf("[INIT-103] setup_network: connecting to server\n");
}

/* These run in order of DESCENDING priority number at unload time */
void __attribute__((destructor(103))) teardown_network(void) {
    printf("[FINI-103] teardown_network: disconnecting\n");
}

void __attribute__((destructor(102))) teardown_logging(void) {
    printf("[FINI-102] teardown_logging: closing log file\n");
}

void __attribute__((destructor(101))) teardown_config(void) {
    printf("[FINI-101] teardown_config: releasing config\n");
}

/* Public function */
void library_action(void) {
    printf("[LIB] performing action\n");
}
gcc -fPIC -shared -o multi_init.so multi_init.c

/* Test with a simple loader */
gcc -e main -x c - -ldl <<'EOF'
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
int main(void) {
    void *lib = dlopen("./multi_init.so", RTLD_LAZY);
    void (*action)(void);
    *(void **)(&action) = dlsym(lib, "library_action");
    if (!dlerror()) (*action)();
    dlclose(lib);
    return 0;
}
EOF

# Expected output:
# [INIT-101] setup_config: loading configuration
# [INIT-102] setup_logging: opening log file
# [INIT-103] setup_network: connecting to server
# [LIB] performing action
# [FINI-103] teardown_network: disconnecting
# [FINI-102] teardown_logging: closing log file
# [FINI-101] teardown_config: releasing config

Code Example 3: Constructor/Destructor in the Main Program

/* main_with_ctors.c — constructor and destructor work in main programs too! */
#include <stdio.h>
#include <stdlib.h>

static int *global_array = NULL;
static int array_size = 0;

/* Runs before main() */
void __attribute__((constructor)) program_setup(void) {
    printf("[CTOR] program_setup: allocating global array\n");
    array_size = 10;
    global_array = malloc(array_size * sizeof(int));
    for (int i = 0; i < array_size; i++) {
        global_array[i] = i * i;
    }
}

/* Runs after main() returns (or exit() is called) */
void __attribute__((destructor)) program_teardown(void) {
    printf("[DTOR] program_teardown: freeing global array\n");
    free(global_array);
    global_array = NULL;
}

int main(void) {
    printf("[MAIN] Global array contents:\n");
    for (int i = 0; i < array_size; i++) {
        printf("  [%d] = %d\n", i, global_array[i]);
    }
    printf("[MAIN] Returning from main()\n");
    return 0;
}
gcc main_with_ctors.c -o main_with_ctors
./main_with_ctors

# Output:
# [CTOR] program_setup: allocating global array
# [MAIN] Global array contents:
#   [0] = 0
#   [1] = 1
#   [2] = 4
#   ... (through [9] = 81)
# [MAIN] Returning from main()
# [DTOR] program_teardown: freeing global array
Note: The constructor ran before main() and the destructor ran after main() returned. This is a clean way to ensure global resources are always initialized before use and always freed after use, without depending on callers to remember to call init/cleanup functions.

Interview Questions & Answers

Q1. What GCC attributes are used for library initialization and finalization?
__attribute__((constructor)) marks a function to be called automatically when a shared library is loaded — either when the program starts (for startup-loaded libraries) or when dlopen() is called. __attribute__((destructor)) marks a function to be called automatically when the library is unloaded — either at program termination or when dlclose() brings the reference count to 0. Both functions must have the signature void function_name(void).
Q2. When exactly do constructor and destructor functions run when using dlopen()?
The constructor runs when dlopen() actually loads the library into memory — that is, on the first dlopen() call when the reference count goes from 0 to 1. Subsequent dlopen() calls on the same library (which only increment the reference count) do NOT trigger the constructor again. The destructor runs when dlclose() brings the reference count down to 0 and the library is actually unloaded from memory.
Q3. Can you define multiple constructor functions? In what order do they run?
Yes, a shared library can define multiple constructor functions. Without priority specification, their execution order is unspecified. When you specify priorities using __attribute__((constructor(N))), functions with lower priority numbers run first. Destructor functions run in the reverse order: higher priority numbers first. Priorities must be 101 or above (0–100 are reserved for system initialization code).
Q4. What are the disadvantages of the old _init() and _fini() approach?
The _init() and _fini() functions have several limitations compared to the modern approach: only one of each can exist in a library (no multiple initialization functions with different priorities), they cannot be used in the main program, they require the -nostartfiles linker option to avoid conflicts with the linker’s own default stubs, and they do not support priority ordering. Modern GCC constructor/destructor attributes solve all these problems and _init()/_fini() is considered obsolete.
Q5. What happens to constructor/destructor functions when a process terminates without calling dlclose()?
On process termination, the dynamic linker performs an implicit dlclose() on all dynamically loaded libraries. This triggers all registered destructor functions (and atexit() handlers registered from within libraries). So even if your code forgets to call dlclose(), the destructors still run when the process exits normally. However, if the process terminates abnormally (e.g., killed with SIGKILL), destructors do not run.

Leave a Reply

Your email address will not be published. Required fields are marked *