Question

Static Libraries, Shared Objects, and dlopen in C/C++

ccpplinuxdlllinker

Question

In C/C++ development on Linux and other POSIX systems, what is the difference between static libraries (.a files), shared objects (.so files), and DLLs?

I understand that a static library is linked into an executable at build time, while a shared object is normally linked as an external dependency and loaded when the program starts. Is that accurate?

I have also heard the term “DLL” used for a library loaded explicitly with dlopen() and dlsym(). Is this meaningfully different from a shared object, or is it only a loading method?

Finally, what does “shared archive” mean? Is it a standard type of library, or does it refer to a static archive compiled in a way that allows it to be included in a shared object?

Short Answer

Static archives (.a) supply object-code files to the linker, which copies the needed code into the final executable or library. Shared objects (.so) remain separate files and are loaded either automatically by the dynamic loader or explicitly with dlopen().

On POSIX systems, “DLL” is not a separate standard file type. It is often an informal name for a dynamically loaded shared object. “Shared archive” is also not a standard Unix library format; it usually means a project-specific name for a static archive intended for use when building a shared library.

Concept

A C/C++ library is reusable compiled code. The major distinction is when code is combined with, or found by, your program.

Static libraries: .a

A static library is an archive of object files. It is commonly created with ar:

ar rcs libmathutil.a add.o multiply.o

When the linker builds an executable, it extracts the object files needed to resolve symbols referenced by your program. Those selected object files become part of the executable.

gcc main.o -L. -lmathutil -o calculator

A static library is not necessarily copied in its entirety. Usually, the linker includes only archive members needed to satisfy unresolved references, although linker options and object-file layout can affect this.

Shared objects: .so

A shared object is an ELF file containing executable code and data designed to be loaded dynamically. It is commonly built with position-independent code:

gcc -fPIC -c mathutil.c -o mathutil.o
gcc -shared -o libmathutil.so mathutil.o

When you link an executable against it, the executable records a dependency on the library rather than copying its implementation code into the executable:

gcc main.o -L. -lmathutil -o calculator

At program startup, the system dynamic loader finds required shared objects, maps them into memory, and resolves symbols. On Linux, this loader is commonly named ld-linux.

Headers, compilation, and linking

Headers are important, but they are not the shared library itself:

  • A header (.h) provides declarations, such as function names, parameter types, and structures.
  • The compiler uses declarations while compiling source code.
  • The linker uses object files, archives, or shared objects to resolve the compiled symbol references.

For example, this declaration tells the compiler that add exists:

int add(int left, int right);

The implementation must still be supplied later by add.o, libmathutil.a, or libmathutil.so.

Automatic versus explicit loading

A shared object can be loaded in two main ways:

  1. Automatic dynamic linking: the executable has a recorded dependency, and the loader loads the .so before main() runs.
  2. Explicit dynamic loading: the program calls dlopen() at runtime and obtains symbols with dlsym().

Both approaches use shared-object files on POSIX systems. dlopen() does not define a separate DLL file format.

Is a DLL different from a shared object?

“DLL” means Dynamic-Link Library. It is mainly Windows terminology, where such libraries normally use the .dll extension and the PE file format. On Linux and most POSIX systems, the usual term and format are shared object and ELF .so.

Developers sometimes call a POSIX .so a DLL informally, especially when discussing runtime loading. Technically, on POSIX, it is still a shared object whether it is loaded automatically or via dlopen().

What is a shared archive?

“Shared archive” is not a standard POSIX or ELF library category. A .a file is a static archive; a .so file is a shared object.

A project may use “shared archive” to describe a static archive whose object files were compiled with -fPIC, making them suitable for inclusion in a shared object. However, the name is a local convention, not an industry-standard format or suffix.

If code from a static archive is linked into a .so, it becomes part of that shared object. Whether its symbols are visible to consumers depends on linker options and symbol-visibility settings, not on a special archive type.

Mental Model

Think of your program as a cookbook.

  • A header file is an index card that says a recipe named add exists and lists its ingredients. It does not contain the cooked food.
  • A static library is like copying needed recipes directly into your cookbook before printing it. The final book works on its own, but every printed copy includes those recipes.
  • A shared object is like keeping recipes in a separate reference book. Your cookbook records where to find that book. Multiple cookbooks can use the same reference copy.
  • dlopen() is like choosing a reference book from a shelf while cooking. The reference book is still a shared object; you simply decide when to open it.

The key idea is that .a and .so describe different packaging and linking models. dlopen() describes a way to load a .so.

Take Quiz

Syntax and Examples

A small static or shared library

mathutil.h declares the public interface:

#ifndef MATHUTIL_H
#define MATHUTIL_H

int add(int left, int right);

#endif

mathutil.c implements it:

#include "mathutil.h"

int add(int left, int right) {
    return left + right;
}

main.c uses it:

#include <stdio.h>
#include "mathutil.h"

int main(void) {
    printf("2 + 3 = %d\n", add(2, 3));
    return 0;
}

Build and link a static library

gcc -c mathutil.c -o mathutil.o
ar rcs libmathutil.a mathutil.o
gcc main.c -L. -lmathutil -o app-static

The -lmathutil option tells the linker to look for libmathutil.a or libmathutil.so. If both are available, the platform's default linker behavior often prefers the shared library. Use -static only when you specifically intend static linking and understand its platform limitations.

Build and link a shared object

gcc -fPIC -c mathutil.c -o mathutil.pic.o
gcc -shared -o libmathutil.so mathutil.pic.o
gcc main.c -L. -lmathutil -Wl,-rpath,'$ORIGIN' -o app-shared

-fPIC creates position-independent code, which is normally required when object code will be placed in a shared object. -Wl,-rpath,'$ORIGIN' embeds a runtime search path that means “the executable's own directory,” making this simple example runnable from its build directory:

./app-shared

Explicitly load a shared object with dlopen()

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

int main(void) {
    void *handle = dlopen("./libmathutil.so", RTLD_NOW);
    if (handle == NULL) {
        fprintf(stderr, "dlopen failed: %s\n", dlerror());
        return 1;
    }

    int (*add)(int, int) = (int (*)(int, int))dlsym(handle, "add");
    const char *error = dlerror();
    if (error != NULL) {
        fprintf(stderr, "dlsym failed: %s\n", error);
        dlclose(handle);
        return 1;
    }

    printf("2 + 3 = %d\n", add(2, 3));
    dlclose(handle);
    return 0;
}

Build it on Linux with:

gcc dynamic_main.c -ldl -o app-dlopen

dlsym() returns an untyped address, so the program must convert it to the correct function-pointer type. The requested symbol name must also be exported by the shared object.

Step by Step Execution

Consider an executable linked normally against libmathutil.so:

#include <stdio.h>
#include "mathutil.h"

int main(void) {
    int result = add(4, 5);
    printf("%d\n", result);
    return 0;
}

Built with:

gcc main.c -L. -lmathutil -Wl,-rpath,'$ORIGIN' -o app

The sequence is:

  1. The compiler reads mathutil.h and verifies that calling add(4, 5) is valid.
  2. The compiler creates main.o, containing a reference to the external symbol add.
  3. The linker finds libmathutil.so because of -L. and -lmathutil.
  4. The linker verifies that the shared object provides add and records libmathutil.so as a runtime dependency of app.
  5. When ./app starts, the dynamic loader locates and maps libmathutil.so into the process.
  6. The loader resolves the call to add using the function in the mapped shared object.
  7. add(4, 5) returns 9, and printf prints 9.

With libmathutil.a, steps 3 and 4 differ: the linker extracts the required object code from the archive and writes it into app. No separate libmathutil.so is needed at startup for that code.

Real World Use Cases

  • System libraries: Applications commonly use shared objects such as the C library and graphics libraries. Many programs can use one installed library version.
  • Security and bug-fix updates: Updating a compatible shared library can update behavior for dependent applications without rebuilding every application. Compatibility must be preserved; an incompatible ABI change can break existing binaries.
  • Command-line tools: A small, portable utility may use static linking when deployment requires a single executable, subject to licensing and operating-system constraints.
  • Plugin systems: Editors, media applications, test runners, and database connectors can load optional features using dlopen().
  • Optional hardware or vendor integrations: An application can start without a vendor library installed, then load it only when a user enables that feature.
  • Embedded systems: Build choices depend on storage, memory sharing, update strategy, and the availability of a dynamic loader.
Take Quiz

Real Codebase Usage

Link shared libraries normally for required dependencies

Most applications use a shared object through normal linking because the dependency is required for the program to work:

gcc app.o -lcurl -o downloader

The build system typically records the dependency, while package management installs the matching runtime library.

Use dlopen() for optional plugins

Explicit loading is useful when a feature is optional. Always validate each operation and provide a useful fallback:

void *plugin = dlopen("libfeature_plugin.so", RTLD_NOW);
if (plugin == NULL) {
    /* Continue without the optional feature. */
    return;
}

After dlsym(), check for errors before calling a function pointer. Define a stable plugin API, often including an API version function, so the application can reject incompatible plugins safely.

Build reusable code for a .so with PIC

When a static archive will be included in a shared library, compile its source files with -fPIC:

gcc -fPIC -c helper.c -o helper.o
ar rcs libhelper.a helper.o
gcc -shared -o libplugin.so plugin.o -L. -lhelper

This does not make libhelper.a a new kind of library. It remains a static archive containing PIC object files.

Control exported symbols deliberately

Real shared libraries often expose only their intended public API. For example, C code may use default hidden visibility and explicitly mark public functions:

__attribute__((visibility("default")))
int library_public_function(void) {
    return 42;
}

This reduces accidental API commitments and avoids symbol-name conflicts. C++ libraries additionally need careful ABI management because compiler versions, standard-library choices, and class-layout changes can affect binary compatibility.

Common Mistakes

Assuming headers provide implementations

This compiles but fails to link if no implementation is supplied:

#include "mathutil.h"

int main(void) {
    return add(1, 2);
}

A header only declares add. Link mathutil.o, libmathutil.a, or libmathutil.so as well.

Thinking a static library is always copied completely

A linker generally extracts only object files from a .a that resolve needed symbols. Do not rely on “the whole archive is included” behavior. If every archive member must be included, use an appropriate linker option such as --whole-archive only when truly necessary.

Confusing compile time, link time, and runtime

These are separate phases:

  • Compilation needs declarations from headers.
  • Linking needs implementations or symbol definitions.
  • Runtime needs shared libraries only if the executable depends on them dynamically.

Assuming dlopen() makes a file a different library type

This is inaccurate:

DLL = a special kind of .so loaded with dlopen

On POSIX, a .so loaded through dlopen() is still a shared object. dlopen() changes when and how it is loaded, not its fundamental format.

Forgetting runtime library search paths

A program may link successfully but fail at startup because the loader cannot locate a required .so.

error while loading shared libraries: libmathutil.so: cannot open shared object file

For development, use an rpath such as $ORIGIN, install the library in a standard system location, or configure the platform's loader paths. Avoid relying on LD_LIBRARY_PATH as a permanent deployment design.

Building non-PIC code into a shared object

Some platforms reject or restrict non-PIC object code in a .so. Compile the relevant sources with -fPIC before creating the archive or shared object.

Treating ABI compatibility as guaranteed

Replacing a .so works safely only when the new version remains binary compatible with programs built against the old one. Changing function signatures, removing symbols, or altering C++ class layouts can break existing applications.

Comparisons

TopicStatic library (.a)Shared object (.so)Explicit runtime loading (dlopen)
What it isArchive of object filesDynamically loadable ELF libraryA POSIX API for loading a shared object
Main build effectNeeded object code is copied into outputOutput records a dependencyNo link-time dependency is required for loaded symbols
Needed at application startupNo, for code incorporated into executableYes, if linked as a required dependencyOnly when the code path calls dlopen()
Can multiple processes share code pages?Usually no; each executable has its own incorporated copyOften yes, subject to OS memory managementOften yes after the object is loaded
Typical useSelf-contained tools, internal reusable codeRequired reusable system or application librariesPlugins and optional features

Automatic versus explicit shared-object loading

FeatureNormal -lfoo shared linkingdlopen("libfoo.so", ...)
When library is selectedBuild timeRuntime
When it is normally loadedBefore main()When dlopen() succeeds
Function callsNormal calls using declarationsObtain addresses with dlsym()
Missing library behaviorProgram usually cannot startProgram can handle the failure and continue

POSIX shared objects versus Windows DLLs

Platform conventionPOSIX/LinuxWindows
Common termShared object/shared libraryDLL
Common extension.so.dll
Typical file formatELFPE
Explicit loading APIdlopen, dlsym, dlcloseLoadLibrary, GetProcAddress, FreeLibrary

The concepts are similar, but APIs, binary formats, naming conventions, and ABI rules are platform-specific.

Take Quiz

Cheat Sheet

  • .h: declarations for the compiler; not compiled implementation code.
  • .o: one compiled object file.
  • .a: static archive of .o files, created with ar.
  • .so: ELF shared object, loaded by the dynamic loader.
  • Static link: selected object code from .a is written into the output binary.
  • Dynamic link: the output records a dependency on a .so.
  • -lfoo: search for libfoo.so or libfoo.a in library paths.
  • -Ldir: add dir to the linker's library search paths.
  • -fPIC: compile position-independent code; normally needed for code that will go into a .so.
  • -shared: create a shared object.
  • dlopen(): load a shared object explicitly at runtime.
  • dlsym(): obtain a symbol address from a dlopen() handle.
  • dlclose(): release a dynamic-loading handle.
  • -ldl: links the dynamic-loading API on many Linux toolchains.
  • “DLL” on POSIX: usually informal terminology, not a separate standard .so category.
  • “Shared archive”: nonstandard term; usually a project convention for a static archive, often built from PIC objects.
  • Replacing a shared library safely requires ABI compatibility, not merely the same filename.

FAQ

Is a .so loaded only when dlopen() is called?

No. A .so linked as a normal dependency is usually loaded by the dynamic loader before main() starts. dlopen() is for explicit, runtime-controlled loading.

Does a static library copy every function into the executable?

Usually no. The linker normally extracts the object files needed to resolve referenced symbols. One object file may contain multiple functions, so functions in that selected object can come along with it.

Do I need a header file to link a library?

The linker does not require headers; it resolves binary symbols. However, C/C++ source normally needs headers so the compiler knows correct declarations and types.

Why does my program link successfully but fail to start because a .so is missing?

The linker found the library during the build, but the runtime dynamic loader could not find a compatible library in its search paths when the program started.

Can I use a static library inside a shared object?

Yes. Its object files normally need to be compiled with -fPIC. The linker then incorporates the needed archive members into the resulting .so.

Is a Linux .so the same binary format as a Windows .dll?

No. They serve related purposes but usually use different file formats and platform APIs: ELF on Linux and PE on Windows.

When should I use dlopen() instead of normal linking?

Use it when a dependency is optional, selected at runtime, or part of a plugin system. Use normal linking for dependencies the application always requires.

Can I replace any .so file to update an application?

Only if the replacement is ABI-compatible with the program and its other dependencies. A changed or removed exported symbol can cause startup or runtime failures.

Take Quiz

Mini Project

Description

Build a small calculator library and consume it in two ways: as a normal shared-library dependency and as an optional runtime-loaded feature. This demonstrates that both approaches use the same .so file, while differing in when the program requires it.

Goal

Create libmathutil.so, run one program that links to it normally, and run another that loads it with dlopen().

Requirements

Create a header declaring an add function and a source file implementing it. Build the source as a shared object named libmathutil.so. Create one executable that calls add through normal shared-library linking. Create a second executable that loads the same library with dlopen() and finds add with dlsym(). Handle dlopen() and dlsym() failures in the runtime-loaded program.

Take Quiz

Keep learning

Related questions

View more