C Programming: Low-Level Mastery
HomeInsightsCoursesC ProgrammingStatic vs. Dynamic Linking
Advanced Topics

Linking and Libraries

Master linking: static vs dynamic libraries, symbol resolution, name mangling, position-independent code, creating shared libraries, and understanding the complete compilation and linking process in C projects.

Understanding Linking

Linking combines object files into executables or libraries. Linker resolves symbols (function/variable names), relocates code, combines sections. Two types: static (compile-time) and dynamic (runtime). Understanding linking helps debug undefined references, multiple definitions, and library conflicts.

C
/* Compilation and linking process */

/*
   Source (.c) → Preprocessor → Preprocessed (.i)
                ↓
            Compiler → Assembly (.s)
                ↓
            Assembler → Object (.o)
                ↓
            Linker → Executable/Library
*/

/* Example project */

/* math_ops.c */
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

/* math_ops.h */
int add(int a, int b);
int subtract(int a, int b);

/* main.c */
#include "math_ops.h"

int main(void) {
    int result = add(10, 20);
    return 0;
}

/* Compilation steps */

/* 1. Compile to object files */
/* gcc -c math_ops.c -o math_ops.o */
/* gcc -c main.c -o main.o */

/* 2. Link object files */
/* gcc math_ops.o main.o -o program */

/* Or in one command */
/* gcc math_ops.c main.c -o program */

/* What linker does */
/*
   1. Symbol resolution:
      - Finds definitions for all extern symbols
      - Matches function calls to implementations
   
   2. Relocation:
      - Assigns final addresses
      - Adjusts code/data references
   
   3. Combining sections:
      - .text (code) sections merged
      - .data (initialized data) merged
      - .bss (uninitialized) merged
*/

/* Symbol types */

/* Defined (exported) */
int global_func(void) { return 0; }
int global_var = 42;

/* Undefined (imported) */
extern int external_func(void);
extern int external_var;

/* Static (internal linkage) */
static int internal_func(void) { return 0; }
static int internal_var = 0;

/* View symbols in object file */
/* nm math_ops.o */
/*
   Output:
   0000000000000000 T add        (T = defined in text)
   0000000000000010 T subtract   (T = defined in text)
*/

/* nm main.o */
/*
   Output:
                    U add        (U = undefined)
   0000000000000000 T main       (T = defined)
*/

Static Libraries

Static libraries (.a on Unix, .lib on Windows) are archives of object files. Linked at compile time, code included in executable. Larger executables but no runtime dependencies. Create with ar (archiver). Common for reusable code.

C
/* Creating static library */

/* Step 1: Compile source files */
/* gcc -c file1.c -o file1.o */
/* gcc -c file2.c -o file2.o */
/* gcc -c file3.c -o file3.o */

/* Step 2: Create archive */
/* ar rcs libmylib.a file1.o file2.o file3.o */

/* ar options:
   r = insert/replace files
   c = create archive
   s = create index (ranlib)
*/

/* Using static library */

/* Link with library */
/* gcc main.c -L. -lmylib -o program */

/* -L. = look in current directory */
/* -lmylib = link libmylib.a */

/* Or specify full path */
/* gcc main.c libmylib.a -o program */

/* Example: Math library */

/* math_lib.c */
#include "math_lib.h"

int math_add(int a, int b) {
    return a + b;
}

int math_subtract(int a, int b) {
    return a - b;
}

int math_multiply(int a, int b) {
    return a * b;
}

/* math_lib.h */
#ifndef MATH_LIB_H
#define MATH_LIB_H

int math_add(int a, int b);
int math_subtract(int a, int b);
int math_multiply(int a, int b);

#endif

/* Create library */
/* gcc -c math_lib.c */
/* ar rcs libmath.a math_lib.o */

/* Use library */
/* main.c */
#include "math_lib.h"
#include <stdio.h>

int main(void) {
    printf("10 + 20 = %d\n", math_add(10, 20));
    return 0;
}

/* Compile */
/* gcc main.c -L. -lmath -o program */

/* List contents of library */
/* ar t libmath.a */
/*
   Output:
   math_lib.o
*/

/* Extract from library */
/* ar x libmath.a math_lib.o */

/* Static library advantages */
/*
   + Simple deployment (single executable)
   + No runtime dependencies
   + Faster startup (no dynamic loading)
   + Version conflicts impossible
*/

/* Static library disadvantages */
/*
   - Larger executables
   - Updates require recompilation
   - Memory waste (duplicate code)
   - No code sharing between programs
*/

/* Standard library locations */
/*
   /usr/lib
   /usr/local/lib
   /lib
*/

/* Standard libraries */
/* -lm   = libm.a (math) */
/* -lpthread = libpthread.a (threads) */
/* -lc   = libc.a (standard C) */

Dynamic/Shared Libraries

Dynamic libraries (.so on Unix, .dll on Windows, .dylib on macOS) are loaded at runtime. Code shared among processes. Smaller executables, updates don't require recompilation. Require position-independent code (-fPIC). More complex but more flexible.

C
/* Creating shared library (Linux/Unix) */

/* Step 1: Compile with -fPIC */
/* gcc -c -fPIC file1.c -o file1.o */
/* gcc -c -fPIC file2.c -o file2.o */

/* Step 2: Link into shared library */
/* gcc -shared -o libmylib.so file1.o file2.o */

/* Or in one step */
/* gcc -shared -fPIC file1.c file2.c -o libmylib.so */

/* -fPIC = Position Independent Code */
/* -shared = Create shared library */

/* Using shared library */

/* Compile with library */
/* gcc main.c -L. -lmylib -o program */

/* Run with library */
/* LD_LIBRARY_PATH=. ./program */

/* Or install to standard location */
/* sudo cp libmylib.so /usr/local/lib */
/* sudo ldconfig */

/* Example: String utilities library */

/* stringutils.c */
#include "stringutils.h"
#include <string.h>
#include <stdlib.h>

char* string_reverse(const char *str) {
    size_t len = strlen(str);
    char *result = malloc(len + 1);
    
    for (size_t i = 0; i < len; i++) {
        result[i] = str[len - 1 - i];
    }
    result[len] = '\0';
    
    return result;
}

/* stringutils.h */
#ifndef STRINGUTILS_H
#define STRINGUTILS_H

char* string_reverse(const char *str);

#endif

/* Create shared library */
/* gcc -shared -fPIC stringutils.c -o libstringutils.so */

/* Use library */
/* main.c */
#include "stringutils.h"
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    char *reversed = string_reverse("Hello");
    printf("%s\n", reversed);
    free(reversed);
    return 0;
}

/* Compile and run */
/* gcc main.c -L. -lstringutils -o program */
/* LD_LIBRARY_PATH=. ./program */

/* Symbol visibility */

/* Default: All symbols exported */
int public_func(void) { return 0; }

/* Hidden: Not exported */
__attribute__((visibility("hidden")))
int private_func(void) { return 0; }

/* Control default visibility */
/* gcc -fvisibility=hidden ... */

/* Then explicitly export */
__attribute__((visibility("default")))
int exported_func(void) { return 0; }

/* Version script (advanced) */
/* version.map */
/*
{
  global:
    public_api_*;
  local:
    *;
};
*/

/* Use version script */
/* gcc -Wl,--version-script=version.map ... */

/* Shared library naming */
/*
   libname.so         - linker name (symlink)
   libname.so.1       - soname (symlink)
   libname.so.1.2.3   - real name
   
   1 = major version (API changes)
   2 = minor version (additions)
   3 = patch version (bugfixes)
*/

/* Create versioned library */
/* gcc -shared -Wl,-soname,libmylib.so.1 -o libmylib.so.1.0.0 ... */
/* ln -s libmylib.so.1.0.0 libmylib.so.1 */
/* ln -s libmylib.so.1 libmylib.so */

/* Dynamic library advantages */
/*
   + Smaller executables
   + Code sharing (memory efficient)
   + Easy updates (no recompilation)
   + Plugin architectures possible
*/

/* Dynamic library disadvantages */
/*
   - Runtime dependencies
   - Version conflicts possible
   - Slower startup (loading time)
   - Complex deployment
*/

Runtime Loading (dlopen)

Dynamically load libraries at runtime with dlopen/dlsym. Essential for plugins, optional features, and modular architectures. More flexible than link-time loading. Platform-specific: dlopen on Unix, LoadLibrary on Windows.

C
#include <dlfcn.h>
#include <stdio.h>

/* Load library at runtime */
void runtime_loading_example(void) {
    /* Open library */
    void *handle = dlopen("./libmylib.so", RTLD_LAZY);
    
    if (handle == NULL) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return;
    }
    
    /* Clear any existing error */
    dlerror();
    
    /* Look up symbol */
    typedef int (*AddFunc)(int, int);
    AddFunc add = (AddFunc)dlsym(handle, "add");
    
    const char *error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "dlsym failed: %s\n", error);
        dlclose(handle);
        return;
    }
    
    /* Call function */
    int result = add(10, 20);
    printf("Result: %d\n", result);
    
    /* Close library */
    dlclose(handle);
}

/* dlopen flags */
/*
   RTLD_LAZY   - Lazy symbol resolution
   RTLD_NOW    - Immediate symbol resolution
   RTLD_GLOBAL - Make symbols available
   RTLD_LOCAL  - Keep symbols private
*/

/* Plugin system example */

/* plugin.h */
typedef struct {
    const char *name;
    void (*init)(void);
    void (*execute)(void);
    void (*cleanup)(void);
} Plugin;

/* plugin_loader.c */
Plugin* load_plugin(const char *path) {
    void *handle = dlopen(path, RTLD_NOW);
    if (handle == NULL) {
        return NULL;
    }
    
    /* Get plugin descriptor */
    Plugin *plugin = (Plugin*)dlsym(handle, "plugin_descriptor");
    if (plugin == NULL) {
        dlclose(handle);
        return NULL;
    }
    
    return plugin;
}

/* example_plugin.c */
#include "plugin.h"
#include <stdio.h>

static void my_init(void) {
    printf("Plugin initialized\n");
}

static void my_execute(void) {
    printf("Plugin executing\n");
}

static void my_cleanup(void) {
    printf("Plugin cleanup\n");
}

/* Export plugin descriptor */
Plugin plugin_descriptor = {
    .name = "Example Plugin",
    .init = my_init,
    .execute = my_execute,
    .cleanup = my_cleanup
};

/* Build plugin */
/* gcc -shared -fPIC example_plugin.c -o example_plugin.so */

/* Windows equivalent */
#ifdef _WIN32
#include <windows.h>

void* load_library_windows(const char *path) {
    HMODULE handle = LoadLibrary(path);
    if (handle == NULL) {
        return NULL;
    }
    
    typedef int (*FuncType)(int, int);
    FuncType func = (FuncType)GetProcAddress(handle, "add");
    
    if (func == NULL) {
        FreeLibrary(handle);
        return NULL;
    }
    
    int result = func(10, 20);
    
    FreeLibrary(handle);
    return NULL;
}
#endif

/* Cross-platform wrapper */
#ifdef _WIN32
    #define LIB_HANDLE HMODULE
    #define lib_open(path) LoadLibrary(path)
    #define lib_sym(handle, name) GetProcAddress(handle, name)
    #define lib_close(handle) FreeLibrary(handle)
    #define lib_error() "Windows error"
#else
    #define LIB_HANDLE void*
    #define lib_open(path) dlopen(path, RTLD_NOW)
    #define lib_sym(handle, name) dlsym(handle, name)
    #define lib_close(handle) dlclose(handle)
    #define lib_error() dlerror()
#endif

LIB_HANDLE load_cross_platform(const char *path) {
    return lib_open(path);
}

Common Linking Issues

Linking errors are common: undefined references, multiple definitions, library not found. Understanding these errors and their solutions is essential for productive C development. Use proper flags, check library paths, verify symbols.

C
/* Issue 1: Undefined reference */

/* Error: undefined reference to 'add' */
/* gcc main.c -o program */

/* Cause: Missing object file or library */

/* Solution: Link with object file */
/* gcc main.c math.o -o program */

/* Or: Link with library */
/* gcc main.c -lmath -o program */

/* Issue 2: Multiple definition */

/* global.c */
int global_var = 0;  /* Definition */

/* file1.c */
int global_var = 0;  /* Another definition */

/* Error: multiple definition of 'global_var' */

/* Solution: Use extern in headers */
/* global.h */
extern int global_var;  /* Declaration only */

/* global.c */
int global_var = 0;  /* Definition */

/* Issue 3: Library not found */

/* Error: cannot find -lmylib */

/* Solution: Add library path */
/* gcc main.c -L/path/to/libs -lmylib */

/* Or: Set environment variable */
/* export LIBRARY_PATH=/path/to/libs */

/* Issue 4: Wrong library order */

/* Error: undefined reference */
/* gcc main.c -lA -lB */

/* If A depends on B, B must come after A */

/* Solution: Correct order */
/* gcc main.c -lB -lA */

/* Issue 5: Missing -fPIC */

/* Error: relocation R_X86_64_32 against '.data' */

/* Solution: Compile with -fPIC for shared libraries */
/* gcc -fPIC -c file.c */

/* Issue 6: Symbol visibility */

/* Symbol not exported from shared library */

/* Solution: Check visibility */
/* nm -D libmylib.so | grep my_func */

/* If missing, ensure not static or hidden */

/* Issue 7: Version mismatch */

/* Error: undefined symbol: my_func@@VERS_2.0 */

/* Solution: Recompile with correct library version */

/* Debugging tools */

/* nm - list symbols */
/* nm -D libmylib.so */  /* Dynamic symbols */
/* nm -g file.o */       /* Global symbols */

/* ldd - list dependencies */
/* ldd program */

/* readelf - detailed info */
/* readelf -d program */    /* Dynamic section */
/* readelf -s program */    /* Symbol table */

/* objdump - disassemble */
/* objdump -t file.o */     /* Symbol table */

/* Check undefined symbols */
/* nm -u program */

/* Check library path */
/* ldconfig -p | grep mylib */

/* Best practices */

/* 1. Use proper header guards */
/* 2. Declare extern in headers */
/* 3. Define in one .c file */
/* 4. Use -fPIC for shared libraries */
/* 5. Version shared libraries */
/* 6. Check dependencies with ldd */
/* 7. Use pkg-config for system libraries */

/* pkg-config example */
/* pkg-config --cflags gtk+-3.0 */
/* pkg-config --libs gtk+-3.0 */

/* gcc main.c `pkg-config --cflags --libs gtk+-3.0` */

/* Makefile example */
/*
CC = gcc
CFLAGS = -Wall -fPIC
LDFLAGS = -shared

SOURCES = file1.c file2.c
OBJECTS = $(SOURCES:.c=.o)
LIBRARY = libmylib.so

all: $(LIBRARY)

$(LIBRARY): $(OBJECTS)
	$(CC) $(LDFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $<

clean:
	rm -f $(OBJECTS) $(LIBRARY)
*/

Summary & What's Next

Key Takeaways:

  • ✅ Linking combines object files into executables
  • ✅ Static libraries (.a) linked at compile time
  • ✅ Shared libraries (.so) loaded at runtime
  • ✅ Use -fPIC for position-independent code
  • ✅ dlopen() enables runtime plugin loading
  • ✅ Check symbol visibility and library paths
  • ✅ Use nm, ldd, readelf for debugging
  • ✅ Version shared libraries properly

What's Next?

Let's learn about C best practices and coding standards!