Accessing Symbols in the Main Program Advanced Shared Library Features

 

Accessing Symbols in the Main Program
TLPI Chapter 42.1.6 — Advanced Shared Library Features
42.1.6
Section
3
Code Examples
5
Interview Q&A

Key Terms in This Section
dlopen() dlsym() –export-dynamic -rdynamic Dynamic Linker Global Symbols Callback Mechanism RTLD_GLOBAL

What Is This Section About?

When you use dlopen() to load a shared library at runtime, the functions inside that library can normally only call functions from other shared libraries already loaded. But sometimes, you want the library to call back into a function that lives in your main program — this is the classic callback pattern.

By default, the dynamic linker does NOT make the main program’s symbols available to dynamically loaded libraries. To enable this, you must compile your main program with a special linker flag: --export-dynamic.

The Problem: Who Can See What?
Symbol Visibility Without –export-dynamic
main program
main()
callback_fn()
❌ callback_fn NOT visible
to dynamic libs
dlopen()
plugin.so
x()
x() calls callback_fn()?
❌ FAILS — symbol not found
Symbol Visibility WITH –export-dynamic
main program
main()
callback_fn() ✅ EXPORTED
All global symbols visible
to dynamic libs
symbols shared
plugin.so
x()
x() calls callback_fn()
✅ SUCCESS

Deep Explanation
How the Dynamic Linker Resolves Symbols

When a shared library is loaded with dlopen(), and a function inside that library tries to call another function, the dynamic linker looks for that function in:

  1. The library itself
  2. Other shared libraries already loaded (like libc.so, libpthread.so, etc.)

The main program’s own functions are NOT in this search list by default. This is why calling a main-program function from inside a plugin will fail with an “undefined symbol” error.

Real-world analogy: Think of a plugin (shared library) as a contractor. By default, the contractor can only use tools from their own toolkit or public toolshops (other .so files). If you want the contractor to use a special tool you own (function in main program), you must first put it on the table and say “this is available” — that’s exactly what --export-dynamic does.
The –export-dynamic Linker Option

This option tells the linker to add all global symbols from the main program into the dynamic symbol table of the resulting ELF executable. Normally, executables only have a small symbol table (just enough for linking). With this option, the full global symbol table is exported.

Option Meaning
-Wl,--export-dynamic Standard form — passes –export-dynamic to the linker via gcc
-export-dynamic Equivalent shorthand
-rdynamic Another synonym (commonly used in practice)
-Wl,-E Yet another synonym (less common)
All four options do the exact same thing. Most codebases use -rdynamic as it’s the shortest form.

Code Example 1 — Basic Callback from Plugin into Main
Step 1: The Plugin (shared library)

This plugin defines a function run_plugin(). Inside it, it tries to call host_callback() — a function that lives in the main program.

/* plugin.c — compiled into plugin.so */
#include <stdio.h>

/* Declaration: host_callback lives in main program, not here */
extern void host_callback(const char *msg);

void run_plugin(void) {
    printf("[plugin] run_plugin() called\n");
    /* Call back into the main program */
    host_callback("Hello from plugin!");
}

Compile the plugin as a shared library:

gcc -g -fPIC -shared -o plugin.so plugin.c
Step 2: The Main Program

The main program defines host_callback() and dynamically loads the plugin.

/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

/* This function is in main — we want the plugin to be able to call it */
void host_callback(const char *msg) {
    printf("[main] host_callback received: %s\n", msg);
}

int main(void) {
    void *handle;
    void (*run_plugin)(void);
    char *error;

    /* Load the plugin */
    handle = dlopen("./plugin.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    /* Get address of run_plugin() */
    run_plugin = dlsym(handle, "run_plugin");
    if ((error = dlerror()) != NULL) {
        fprintf(stderr, "dlsym failed: %s\n", error);
        dlclose(handle);
        exit(EXIT_FAILURE);
    }

    /* Call the plugin function — it will call back host_callback() */
    run_plugin();

    dlclose(handle);
    return 0;
}

Compile the main program — notice -rdynamic:

# WITHOUT -rdynamic: plugin cannot find host_callback → crash
gcc -g -o main_no_export main.c -ldl

# WITH -rdynamic: plugin can find host_callback → works
gcc -g -rdynamic -o main_with_export main.c -ldl

Code Example 2 — Event Handler Pattern
Realistic use: registering a handler from main that the library calls

This pattern is common in event-driven systems, GUI frameworks, and plugin architectures.

/* event_plugin.c — compiled as event_plugin.so */
#include <stdio.h>

/* These are defined in the main program */
extern void on_start(void);
extern void on_stop(void);

void trigger_events(void) {
    printf("[plugin] triggering start event\n");
    on_start();      /* calls back into main */

    printf("[plugin] triggering stop event\n");
    on_stop();       /* calls back into main */
}
/* event_main.c */
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>

/* Event handler functions — defined in main */
void on_start(void) { printf("[main] START event handled!\n"); }
void on_stop(void)  { printf("[main] STOP event handled!\n"); }

int main(void) {
    void *lib = dlopen("./event_plugin.so", RTLD_LAZY);
    if (!lib) { fprintf(stderr, "%s\n", dlerror()); return 1; }

    void (*trigger)(void) = dlsym(lib, "trigger_events");
    if (dlerror()) { fprintf(stderr, "symbol not found\n"); return 1; }

    trigger();   /* Plugin calls on_start() and on_stop() in main */

    dlclose(lib);
    return 0;
}
gcc -fPIC -shared -o event_plugin.so event_plugin.c
gcc -rdynamic -o event_main event_main.c -ldl
./event_main

Expected output:

[plugin] triggering start event
[main] START event handled!
[plugin] triggering stop event
[main] STOP event handled!

Code Example 3 — Verify Symbol Export with readelf
How to confirm –export-dynamic worked
/* symbols_demo.c */
#include <stdio.h>
#include <stdlib.h>

/* These are global functions — should appear in dynamic symbol table */
void public_func_a(void) { printf("A\n"); }
void public_func_b(void) { printf("B\n"); }

/* static = private, should NOT appear */
static void private_func(void) { printf("private\n"); }

int main(void) {
    public_func_a();
    public_func_b();
    return 0;
}
# Compile WITHOUT -rdynamic
gcc -o demo_no_export symbols_demo.c
readelf --dyn-syms demo_no_export | grep func
# Result: public_func_a and public_func_b may NOT appear

# Compile WITH -rdynamic
gcc -rdynamic -o demo_with_export symbols_demo.c
readelf --dyn-syms demo_with_export | grep func
# Result: public_func_a and public_func_b WILL appear
# private_func NEVER appears (it's static)
readelf –dyn-syms shows the dynamic symbol table. Only symbols here are visible to the dynamic linker at runtime.

How It Works Internally
ELF Symbol Tables — Static vs Dynamic

An ELF executable has two symbol tables:

Table Used For Present Without -rdynamic? Present With -rdynamic?
.symtab Debugging, static analysis Yes (if not stripped) Yes
.dynsym Runtime dynamic linking Minimal (only needed imports) Full global symbol set

The --export-dynamic flag makes the linker populate .dynsym with all global-scope symbols from the main program. The dynamic linker (ld.so) uses .dynsym when resolving symbols for loaded libraries.

Interview Questions & Answers
Q1. You load a shared library with dlopen() and its function calls a function that exists in your main program, but you get “undefined symbol” at runtime. What went wrong and how do you fix it?
By default, the dynamic linker does not expose the main program’s global symbols to dynamically loaded libraries. The fix is to compile the main program with the -rdynamic (or -Wl,--export-dynamic) flag. This adds all global symbols from the main program’s ELF into the dynamic symbol table (.dynsym), making them visible to dlopen()-loaded libraries.
Q2. What is the difference between -rdynamic and -Wl,–export-dynamic?
They are identical in effect. -rdynamic is a gcc driver shorthand. -Wl,--export-dynamic explicitly passes the --export-dynamic flag to the underlying linker (ld). Both cause the same ELF output. The other synonyms are -export-dynamic and -Wl,-E.
Q3. Does -rdynamic expose static functions in the main program to plugin libraries?
No. The static keyword limits a symbol’s scope to its compilation unit. -rdynamic only exports symbols that already have global scope (i.e., are not declared static). Static symbols never appear in the dynamic symbol table regardless of this flag.
Q4. What is the difference between RTLD_GLOBAL and –export-dynamic?
RTLD_GLOBAL is a flag passed to dlopen() — it makes the symbols of the loaded shared library available to subsequently loaded libraries. --export-dynamic is a linker flag used at compile time — it makes the symbols of the main program available to all dynamically loaded libraries. They work in opposite directions: RTLD_GLOBAL is library→future libraries; –export-dynamic is main→all loaded libraries.
Q5. In what real-world scenarios would you use the –export-dynamic / -rdynamic pattern?
Common use cases include: (1) Plugin/extension architectures where plugins call framework functions in the host application. (2) Scripting engines embedded in a C program — the script host calls back C functions in main. (3) Test harnesses that inject mock functions via plugins. (4) Signal/event systems where a dynamically loaded module triggers registered handlers defined in the main application.

Chapter 42 — Advanced Shared Library Features
Continue to the next section: Symbol Visibility

Next: Symbol Visibility → EmbeddedPathashala Home

Leave a Reply

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