The C Path — learn C, visually

⚙️ Compiler & Toolchain Mastery

Libraries: static, shared & shipping your code

⏱ 16 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

To draw graphics, compress files, or store data the way real apps do, your program needs code other people wrote — a library — and the first time you try, you'll meet two of the most-Googled error messages in all of C: "undefined reference" and "cannot open shared object file". By the end of this final lesson you'll fix both on sight, and you'll know how to package your own code so other programmers can build on it.

Every C program you've ever run uses libraries — you've been linking against libc, the library that holds printf, since hello.c. In this final lesson you cross to the other side: building libraries, understanding how the two flavors load, and mastering the linker quirks that generate the internet's most-asked C questions.

Two flavors of library

This spot has an interactive flow widget — open the interactive lesson to play with it.

static .ashared .so
what it isan ar archive of .o filesa real ELF binary, position-independent
when resolvedat link time — code copied into your executableat load time — by the dynamic loader ld.so
resultbigger, self-contained binarysmall binary, one lib shared by all processes
library updatesneed relink to pick up fixesfix the .so, every program benefits at next launch

Building libmymath, both ways

Our library is two files — mymath.h (the contract) and mymath.c (the goods):

mymath.h + mymath.c + main.c
/* ---- mymath.h ---- */
#ifndef MYMATH_H
#define MYMATH_H
int square(int x);
int cube(int x);
#endif

/* ---- mymath.c ---- */
#include "mymath.h"
int square(int x) { return x * x; }
int cube(int x)   { return x * x * x; }

/* ---- main.c ---- */
#include <stdio.h>
#include "mymath.h"
int main(void) {
    printf("6 squared is %d, cubed is %d\n", square(6), cube(6));
    return 0;
}

Static is charmingly low-tech — an archive of object files, made with ar:

static: ar rcs
$ gcc -c mymath.c -o mymath.o
$ ar rcs libmymath.a mymath.o        # r=insert, c=create, s=index
$ ar t libmymath.a                   # list members
mymath.o
$ gcc main.c -L. -lmymath -o app_static
$ ./app_static
6 squared is 36, cubed is 216
$ ls -l app_static                   # square()'s code is INSIDE this file
-rwxr-xr-x 1 ali ali 16144 Aug  1 14:02 app_static

Shared needs position-independent code (-fPIC) because the .so may land at any address in any process — remember ASLR from the memory lessons:

shared: -fPIC -shared, and the rite of passage
$ gcc -fPIC -c mymath.c -o mymath.o
$ gcc -shared mymath.o -o libmymath.so
$ gcc main.c -L. -lmymath -o app
$ ./app
./app: error while loading shared libraries: libmymath.so:
cannot open shared object file: No such file or directory
# linker found it at build time; the LOADER can't at run time. Fixes:
$ LD_LIBRARY_PATH=. ./app                      # 1: env var (testing)
6 squared is 36, cubed is 216
$ gcc main.c -L. -lmymath -Wl,-rpath,'$ORIGIN' -o app
$ ./app                                        # 2: rpath baked in
6 squared is 36, cubed is 216
# 3 (system-wide): sudo cp libmymath.so /usr/local/lib && sudo ldconfig

That runtime error is the rite of passage: the linker found libmymath.so at build time (thanks to -L.), but the dynamic loader doesn't search your project directory at run time. Three fixes, in increasing order of permanence: LD_LIBRARY_PATH (quick test), an rpath baked into the binary (-Wl,-rpath,'$ORIGIN' = "look next to the executable"), or install to /usr/local/lib and run ldconfig to refresh the loader's cache.

🧠 Checkpoint: Your program builds fine but at launch says cannot open shared object file. Which stage is failing?

  • The compiler — missing header
  • The static linker ld at build time
  • The dynamic loader ld.so at run time
  • The preprocessor
Show answer

The dynamic loader ld.so at run time — Build-time -L told ld where the .so was, but that path is not recorded for run time. The dynamic loader searches its cache (ldconfig), standard dirs, rpath, and LD_LIBRARY_PATH — your project directory is in none of them until you act.

ldd: who do you depend on?

ldd: the dependency X-ray
$ ldd ./app
        linux-vdso.so.1 (0x00007ffd8e5f2000)
        libmymath.so => /home/ali/mathdemo/libmymath.so (0x00007f0e2a614000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f0e2a400000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2
$ ldd ./app_static
        not a dynamic executable    # statically linked: needs nobody
# security note: never run ldd on untrusted binaries — it may execute them.

The linking-order trap

The single most-Googled linker error is self-inflicted:

order matters!
$ gcc -lmymath main.c -L. -o app          # library BEFORE the code
/usr/bin/ld: /tmp/ccJx4Fzq.o: in function `main':
main.c:(.text+0x1a): undefined reference to `square'
main.c:(.text+0x2c): undefined reference to `cube'
collect2: error: ld returned 1 exit status
$ gcc main.c -L. -lmymath -o app          # code first, THEN library
$ echo $?
0

Why? The linker scans left to right, keeping a list of unresolved symbols. When it meets a library, it takes only the members that satisfy the list so far — then moves on and never looks back. -lmymath before main.c means: "any needs? no? moving on" — and by the time main.o asks for square, the library is behind us. Rule: objects first, then -l flags, with libraries after the code that needs them. (This is also why -lm traditionally goes last.)

🧠 Checkpoint: Why does gcc -lmymath main.c fail while gcc main.c -lmymath works?

  • -l flags are only valid at the end of the line
  • The linker scans left to right and pulls from a library only symbols already known to be needed
  • main.c shadows the library’s symbols
  • Libraries must be compiled last for ABI reasons
Show answer

The linker scans left to right and pulls from a library only symbols already known to be needed — A single left-to-right pass: when libmymath is visited, nothing needs square() yet, so nothing is taken. main.o’s needs arise later, unmet. Objects and sources first, libraries after — with each library after the code that uses it.

Using any third-party library: the universal recipe

Every C library on Earth is consumed the same two-part way — header for the compiler, lib for the linker:

using an installed library: header + lib
/* zdemo.c — compress a string with zlib, the classic C library */
#include <stdio.h>
#include <string.h>
#include <zlib.h>                 /* 1) header: declarations   */

int main(void) {
    const char *text = "hello hello hello hello hello!";
    unsigned char out[128];
    uLongf outlen = sizeof out;

    compress(out, &outlen, (const Bytef *)text, strlen(text) + 1);
    printf("%zu bytes -> %lu bytes\n", strlen(text) + 1, outlen);
    return 0;
}                                 /* 2) lib: gcc ... -lz       */
pkg-config: ask, don’t guess
$ gcc zdemo.c -o zdemo
/usr/bin/ld: /tmp/ccj2mQ8v.o: undefined reference to `compress'
# header satisfied the COMPILER; the LINKER still needs the code:
$ pkg-config --cflags --libs zlib
-lz
$ gcc zdemo.c $(pkg-config --cflags --libs zlib) -o zdemo
$ ./zdemo
31 bytes -> 19 bytes
# same recipe for every library: sdl2, gtk4, openssl, sqlite3...
$ pkg-config --cflags --libs sqlite3
-lsqlite3

pkg-config is the standard directory service: installed libraries register their flags, and you splice them in with shell substitution. No more guessing include paths.

Symbol visibility: a library's public face

By default, every non-static function in your .so is exported — your internal helpers become someone's load-bearing dependency. Professionals flip the default with -fvisibility=hidden, then explicitly mark the API:

visibility.c — a deliberate public API
/* build with: gcc -fPIC -shared -fvisibility=hidden ... */
#define API __attribute__((visibility("default")))

API int mylib_open(const char *path);   /* exported            */
API int mylib_close(int h);             /* exported            */

int helper_parse(const char *s);        /* hidden: internal    */
static int table_size;                  /* static: file-local  */

Smaller symbol tables, faster load times, freedom to refactor internals. (This is the extern/static distinction from the scope lesson, scaled up to whole libraries.)

🧠 Checkpoint: A library is built with -fvisibility=hidden. What happens to functions not marked visibility("default")?

  • They are deleted from the binary
  • They still exist and work inside the .so but are invisible to programs linking against it
  • They become static and can’t cross files
  • They cause a link error
Show answer

They still exist and work inside the .so but are invisible to programs linking against it — Hidden symbols work normally within the library — internal callers are unaffected — but they don’t appear in the dynamic export table, so outside code can’t link to them. Your internals stay yours.

⚠️

Naming ritual: the file must be named libname.a / libname.so for -lname to find it — -lmymath literally means "search the -L paths for libmymath.so, then libmymath.a". Forget the lib prefix on the file and the linker will never find it.

Practice

This spot has an interactive editor widget — open the interactive lesson to play with it.

🎓 You made it

Look at the road behind you: bits and two's complement, pointers and the heap, every qualifier and storage class, the preprocessor, C23, the standard library, algorithms — and now the entire toolchain, from preprocessor tokens to relocated ELF symbols. You are not a beginner anymore. You are a C programmer.

🎉

Where to go next:

  • Build things. Nothing cements C like projects: a shell, an HTTP server, a text editor, an interpreter, a ray tracer. Pick one that scares you slightly.
  • Read the classics: Kernighan & Ritchie's The C Programming Language (see the language through its creators' eyes) and Jens Gustedt's Modern C (free online — the C17/C23 view).
  • Read real code: curl, SQLite, Redis, and the Linux kernel are masterclasses in C style — and every __attribute__, Makefile, and linker trick you now recognize.
  • Contribute to open source: find a C project you use, run its test suite under ASan, fix a warning, submit the patch. Welcome to the community.

The toolchain is yours, the language is yours — now go compile something that didn't exist yesterday. 🚀

▶ Practice this lesson interactively (with live gcc)