Creating a Shared Library The Linux Programming Interface

 

๐Ÿ”จ Creating a Shared Library
Chapter 41.4.1 | The Linux Programming Interface
Topic
gcc -fPIC -shared
Level
Intermediate
Part
2 of 3

๐Ÿ”‘ Key Concepts

gcc -fPIC gcc -shared libfoo.so ELF format Object file (.o) LD_LIBRARY_PATH Two-step build nm / readelf

What Format Does Linux Use for Shared Libraries?

Linux shared libraries use the ELF (Executable and Linking Format). ELF is also used for regular executables and object files. ELF replaced older formats like a.out and COFF on Linux and most modern UNIX systems.

An ELF shared library is a compiled binary that contains machine code, symbol tables, relocation information, and metadata that allows the dynamic linker to load it at run time and connect it to programs that need it.

๐Ÿ“‹ The Two-Step Process to Build a Shared Library

Creating a shared library always involves two separate commands: first, compiling source files into position-independent object files; second, linking those object files into the .so file.

1 Compile source files with -fPIC

Each .c source file must be compiled into a .o object file using the -fPIC flag. This tells GCC to generate Position-Independent Code โ€” code that can be loaded at any memory address.

gcc -g -c -fPIC -Wall mod1.c mod2.c mod3.c

This produces: mod1.o, mod2.o, mod3.o

2 Link the object files into a shared library with -shared

The -shared flag tells GCC to produce a shared object file instead of an executable. The -o flag names the output file.

gcc -g -shared -o libfoo.so mod1.o mod2.o mod3.o

This produces: libfoo.so โ€” your shared library, ready to use.

Naming Convention: Shared library filenames must start with lib and end with .so. This is a convention enforced by the linker: when you write -lfoo, the linker searches for libfoo.so (or libfoo.a). If your file is named foo.so, the linker will not find it with -lfoo.

โšก Single-Command Build (Alternative)

It is possible to combine both steps into one GCC command. GCC is smart enough to handle compilation and shared library creation together:

gcc -g -fPIC -Wall mod1.c mod2.c mod3.c -shared -o libfoo.so

However, the book and most professional workflows keep the two steps separate because:

  • It is clearer which step is compilation vs. library building
  • The compiled .o files can be reused without recompiling
  • Build systems like make handle incremental builds better with separate steps

๐Ÿ” Key Difference: No Individual Object Files After Linking

With a static library (.a file), the individual object files (.o) retain their identities inside the archive. You can add or remove individual .o files from a .a archive using the ar command.

With a shared library (.so file), the object files are merged together during linking and lose their individual identities. You cannot extract or replace individual .o files from a .so. If you need to change one source file, you must rebuild all the object files and relink the entire .so.

๐Ÿ“ฆ Static Library (libfoo.a)
[ mod1.o ]
[ mod2.o ]
[ mod3.o ]
Each .o is identifiable
Can add/remove individually
๐Ÿ“ฆ Shared Library (libfoo.so)
merged code
(mod1+mod2+mod3)
โ€” no separation โ€”
Object files merged permanently
Cannot add/remove individually

๐Ÿ“– GCC Flags Reference for Shared Libraries
Flag Purpose When Used
-fPIC Generate Position-Independent Code โ€” required for shared libs Compile step (-c)
-shared Tell linker to create a shared object instead of an executable Link step
-g Include debugging information (useful during development) Both steps
-Wall Enable all compiler warnings Compile step
-L<dir> Add directory to library search path at link time When linking a program against the .so
-l<name> Link against lib<name>.so (e.g., -lfoo โ†’ libfoo.so) When linking a program
-Wl,-rpath,<dir> Embed a run-time library search path into the executable When linking a program

โ–ถ๏ธ Using a Shared Library You Built

After building libfoo.so, you link a program against it and run it:

# Link prog against libfoo.so in the current directory
gcc -Wall -o prog main.c -L. -lfoo

# Tell the dynamic linker to also search the current directory
export LD_LIBRARY_PATH=.

# Run the program
./prog

The LD_LIBRARY_PATH environment variable tells the dynamic linker (ld.so) where to find shared libraries at run time. This is fine for development, but not recommended for production. The proper solution is to install the library in a standard location like /usr/local/lib and run ldconfig.

โš ๏ธ Do not set LD_LIBRARY_PATH in production scripts or shell profiles. It overrides system library paths for all programs run by that user and can cause programs to pick up the wrong library version โ€” a security and stability risk.

๐Ÿ’ป Coding Examples

Example 1 โ€” Full Shared Library Build: math utility library

A complete walkthrough: three source files, build as shared library, write a program that uses it, compile the program, and run it.

mod1.c โ€” string utilities

/* mod1.c */
#include <stdio.h>
#include <string.h>

void print_upper(const char *str) {
    char buf[256];
    int i;
    for (i = 0; str[i] && i < 255; i++)
        buf[i] = (str[i] >= 'a' && str[i] <= 'z') ? str[i] - 32 : str[i];
    buf[i] = '\0';
    printf("%s\n", buf);
}

int string_len(const char *s) {
    return (int)strlen(s);
}

mod2.c โ€” math utilities

/* mod2.c */
#include <stdio.h>

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

double power(double base, int exp) {
    double result = 1.0;
    int i;
    for (i = 0; i < exp; i++)
        result *= base;
    return result;
}

mod3.c โ€” info function

/* mod3.c */
#include <stdio.h>

void library_info(void) {
    printf("libutils.so version 1.0 โ€” loaded successfully\n");
}

main.c โ€” program using all three modules

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

/* Declare functions from the shared library */
void print_upper(const char *str);
int string_len(const char *s);
int factorial(int n);
double power(double base, int exp);
void library_info(void);

int main(void) {
    library_info();

    print_upper("hello from shared library");
    printf("Length of 'hello': %d\n", string_len("hello"));

    printf("5! = %d\n", factorial(5));
    printf("2^10 = %.0f\n", power(2.0, 10));

    return 0;
}

Makefile to build everything

# Makefile for shared library demo
CC = gcc
CFLAGS = -g -Wall -fPIC

# Step 1: Compile each source to a PIC object file
mod1.o: mod1.c
	$(CC) $(CFLAGS) -c mod1.c

mod2.o: mod2.c
	$(CC) $(CFLAGS) -c mod2.c

mod3.o: mod3.c
	$(CC) $(CFLAGS) -c mod3.c

# Step 2: Link object files into shared library
libutils.so: mod1.o mod2.o mod3.o
	$(CC) -g -shared -o libutils.so mod1.o mod2.o mod3.o

# Step 3: Compile and link the main program
prog: main.c libutils.so
	$(CC) -g -Wall -o prog main.c -L. -lutils

# Run target: sets LD_LIBRARY_PATH and executes
run: prog
	LD_LIBRARY_PATH=. ./prog

clean:
	rm -f *.o *.so prog
# Build and run manually
gcc -g -Wall -fPIC -c mod1.c mod2.c mod3.c
gcc -g -shared -o libutils.so mod1.o mod2.o mod3.o
gcc -g -Wall -o prog main.c -L. -lutils
LD_LIBRARY_PATH=. ./prog

# Expected output:
# libutils.so version 1.0 โ€” loaded successfully
# HELLO FROM SHARED LIBRARY
# Length of 'hello': 5
# 5! = 120
# 2^10 = 1024
Example 2 โ€” Inspecting a Shared Library with nm, readelf, objdump

These tools reveal what is inside your shared library: the symbol table, section headers, and relocation entries. Understanding this output is essential for debugging linking problems.

# After building libutils.so from the previous example:

# 1. nm โ€” list all symbols in the shared library
nm -D libutils.so
# -D shows dynamic symbol table (what's exported)
# T prefix = function in text segment (defined, exported)
# U prefix = undefined (must come from another library)
# Output includes: factorial, power, print_upper, etc.

# 2. nm with better output (demangle, sort by address)
nm -D --defined-only libutils.so
# 3. readelf โ€” show ELF headers and sections
readelf -h libutils.so        # ELF header (type, arch, entry point)
readelf -S libutils.so        # Section headers (.text, .data, .bss, etc.)
readelf -d libutils.so        # Dynamic section (soname, needed libs)
readelf --syms libutils.so    # Full symbol table

# 4. objdump โ€” disassemble and show headers
objdump -d libutils.so        # Disassemble code sections
objdump --all-headers libutils.so   # All headers including dynamic
objdump -p libutils.so        # Program/segment headers

# 5. Check for TEXTREL โ€” means some .o was compiled WITHOUT -fPIC
objdump --all-headers libutils.so | grep TEXTREL
readelf -d libutils.so | grep TEXTREL
# No output = good (all modules compiled with -fPIC)
# If TEXTREL appears = some module was missing -fPIC
# 6. Check if an object file was compiled with -fPIC
# Look for _GLOBAL_OFFSET_TABLE_ in its symbol table:
nm mod1.o | grep _GLOBAL_OFFSET_TABLE_
readelf -s mod1.o | grep _GLOBAL_OFFSET_TABLE_

# If this symbol is PRESENT = compiled WITH -fPIC (good)
# If ABSENT = compiled WITHOUT -fPIC (bad for shared libs)

# 7. ldd โ€” show run-time library dependencies of a program
ldd prog
# Example output:
#   libutils.so => ./libutils.so (0x00007f8a12345000)
#   libc.so.6   => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a12100000)
#   /lib64/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2

# 8. file command โ€” confirms the library is an ELF shared object
file libutils.so
# Output: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
#         dynamically linked, with debug_info, not stripped

๐ŸŽฏ Interview Questions

Q1. What are the two GCC commands needed to build a shared library from source files?

Answer: First, compile each source file to an object file with -fPIC: gcc -c -fPIC -Wall mod1.c mod2.c mod3.c. Second, link the object files into a shared library with -shared: gcc -shared -o libfoo.so mod1.o mod2.o mod3.o.

Q2. What is the naming convention for shared libraries on Linux, and why does it matter?

Answer: Shared libraries must be named with prefix lib and suffix .so, e.g., libfoo.so. This matters because when you link with -lfoo, the linker automatically searches for libfoo.so. If the file is named foo.so, the linker will not find it via the -l option.

Q3. Can you add or remove individual object modules from an existing shared library (.so)?

Answer: No. Unlike static libraries (.a archives) where the ar command can add or remove individual .o files, shared libraries merge all object files into a single entity where they lose their individual identities. To change the library, you must recompile the affected source files and relink the entire .so from scratch.

Q4. What does the LD_LIBRARY_PATH environment variable do?

Answer: It adds directories to the dynamic linker’s run-time library search path. When a program starts, the dynamic linker searches these directories before the system defaults (/lib, /usr/lib, etc.) to find required .so files. It is useful for development but should not be set globally in production due to security risks and potential for loading incorrect library versions.

Q5. What ELF format does Linux use for shared libraries, and what older formats did it replace?

Answer: Linux uses the ELF (Executable and Linking Format). ELF replaced the older a.out and COFF formats. ELF is also used for executables, object files (.o), and core dump files on Linux and most modern UNIX systems.

Q6. How do you check if a .so file contains any module not compiled with -fPIC?

Answer: Use either objdump --all-headers libfoo.so | grep TEXTREL or readelf -d libfoo.so | grep TEXTREL. If either command produces output containing TEXTREL, the library contains at least one object module compiled without -fPIC, meaning its text segment has references requiring run-time relocation.

Q7. What command lists the shared library dependencies of an executable?

Answer: The ldd command, e.g., ldd ./myprog. It shows each required shared library, the file path where it will be found, and the virtual address at which it will be mapped. If a library is shown as not found, the program will fail to start.

Leave a Reply

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