gcc -fPIC -shared
Intermediate
2 of 3
๐ Key Concepts
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.
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.
-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
-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.
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.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
.ofiles can be reused without recompiling - Build systems like
makehandle incremental builds better with separate steps
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.
Can add/remove individually
(mod1+mod2+mod3)
โ no separation โ
Cannot add/remove individually
| 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 |
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.
๐ป Coding Examples
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
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.
Continue Learning
โ Prev: Overview of Shared Libraries โถ Next: Position-Independent Code
