Practical #2: Libraries
This session covers how to build, inspect, and use C libraries — both static archives and shared libraries — and how to integrate them into a Makefile-based project.
Part A — Static Libraries (50 min)
Exercise 1 — Static Library
Difficulty: EasyYou will create a small string utilities library as a static archive (.a).
Starter files
Create a directory tp_libraries/ with the following files:
strutils.h
#ifndef STRUTILS_H
#define STRUTILS_H
#include <stddef.h>
// Return the length of s (like strlen).
size_t str_len(const char *s);
// Copy src into dst and return dst (like strcpy).
char *str_copy(char *dst, const char *src);
// Return 1 if s contains character c, 0 otherwise.
int str_contains(const char *s, char c);
// Convert s to uppercase in-place. Returns s.
char *str_upper(char *s);
#endif
strutils.c
#include "strutils.h"
size_t str_len(const char *s) {
size_t len = 0;
while (s[len] != '\0') len++;
return len;
}
char *str_copy(char *dst, const char *src) {
char *p = dst;
while ((*p++ = *src++) != '\0');
return dst;
}
int str_contains(const char *s, char c) {
while (*s != '\0') {
if (*s == c) return 1;
s++;
}
return 0;
}
char *str_upper(char *s) {
for (char *p = s; *p != '\0'; p++) {
if (*p >= 'a' && *p <= 'z') {
*p -= 32;
}
}
return s;
}
main.c
#include <stdio.h>
#include "strutils.h"
int main(void) {
char buf[64];
printf("str_len(\"hello\") = %zu\n", str_len("hello"));
str_copy(buf, "world");
printf("str_copy -> \"%s\"\n", buf);
printf("str_contains(\"hello\", 'l') = %d\n", str_contains("hello", 'l'));
printf("str_contains(\"hello\", 'z') = %d\n", str_contains("hello", 'z'));
str_copy(buf, "make it loud");
str_upper(buf);
printf("str_upper -> \"%s\"\n", buf);
return 0;
}
Tasks
-
Compile the library source to an object file:
gcc -Wall -Wextra -c strutils.c -o strutils.o -
Create the static archive:
ar rcs libstrutils.a strutils.o -
Link the main program against the static library:
gcc -Wall -Wextra main.c -L. -lstrutils -o prog_static ./prog_static -
Inspect the library:
ar -t libstrutils.a # list archive contents nm libstrutils.a # list symbols
Questions
- What do the
r,c,sflags ofarmean? - Delete
libstrutils.aand run./prog_staticagain. Does it still work? Why?
Exercise 2 — Multi-file Static Library
Difficulty: RxReal libraries span multiple source files. You will now split strutils into two modules and build a library from both.
Create the following additional files inside tp_libraries/:
mathutils.h
#ifndef MATHUTILS_H
#define MATHUTILS_H
// Return the absolute value of n.
int math_abs(int n);
// Return the minimum of a and b.
int math_min(int a, int b);
// Return the maximum of a and b.
int math_max(int a, int b);
// Return 1 if n is prime, 0 otherwise.
int math_is_prime(int n);
#endif
mathutils.c
#include "mathutils.h"
int math_abs(int n) {
return n < 0 ? -n : n;
}
int math_min(int a, int b) {
return a < b ? a : b;
}
int math_max(int a, int b) {
return a > b ? a : b;
}
int math_is_prime(int n) {
if (n < 2) return 0;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return 0;
}
return 1;
}
Tasks
-
Compile both modules to object files:
gcc -Wall -Wextra -c strutils.c -o strutils.o gcc -Wall -Wextra -c mathutils.c -o mathutils.o -
Bundle them into a single archive called
libutils.a:ar rcs libutils.a strutils.o mathutils.o -
Verify both modules are present:
ar -t libutils.a nm libutils.a -
Write a new
main2.cthat uses at least one function from each module, compile it againstlibutils.a, and run it. -
Extract a single object file from the archive without unpacking everything:
ar -x libutils.a mathutils.o
Questions
- What is the difference between
ar -tandnmin terms of what they display? - If you only use
str_lenin your program, does the linker include themathutils.omodule from the archive? Why or why not?
Exercise 3 — Library Inspection with objdump
Difficulty: Rx
Use objdump to examine the binary content of your library at a low level.
-
Disassemble the
strutils.oobject file:objdump -d strutils.o -
List the symbol table with sizes:
objdump -t strutils.o -
Display section headers:
objdump -h strutils.o -
Now run the same commands on the final
prog_staticexecutable. Notice that the functions are now embedded in the binary at concrete addresses.
Questions
- In the object file, function addresses start at
0x0000000000000000. Why? - After linking, what addresses are the
str_lenandstr_copyfunctions at? - What sections (
.text,.data,.bss, etc.) are present in the object file versus the final executable?
Part B — Dynamic Libraries (50 min)
Exercise 4 — Shared Library
Difficulty: RxBuild the same source as a shared library (.so).
-
Compile with position-independent code:
gcc -Wall -Wextra -fPIC -c strutils.c -o strutils_pic.o gcc -Wall -Wextra -fPIC -c mathutils.c -o mathutils_pic.o -
Create the shared library from both modules:
gcc -shared -o libutils.so strutils_pic.o mathutils_pic.o -
Link the main program:
gcc -Wall -Wextra main2.c -L. -lutils -o prog_dynamic -
Try to run it:
./prog_dynamic
Expected error
You will likely see: error while loading shared libraries: libutils.so: cannot open shared object file. The runtime linker does not know where to find the library.
-
Fix it using one of these approaches:
Option A —
LD_LIBRARY_PATH(temporary)LD_LIBRARY_PATH=. ./prog_dynamicOption B —
rpath(embedded in executable)gcc -Wall -Wextra main2.c -L. -lutils -Wl,-rpath,'$ORIGIN' -o prog_dynamic ./prog_dynamic -
Compare the two executables:
ls -l prog_static prog_dynamic ldd prog_dynamic
Questions
- Which executable is larger? Why?
- What does
lddshow forprog_dynamic? What aboutprog_static? - Delete
libutils.soand run./prog_dynamic. What happens? How does this differ from the static case?
Exercise 5 — Library Versioning and Soname
Difficulty: HardIn production systems, shared libraries carry a version number so that multiple versions can coexist on the same machine. This is managed through a naming convention and the concept of soname.
Naming convention
| File | Role |
|---|---|
libutils.so.1.0.0 | Real file — full version |
libutils.so.1 → libutils.so.1.0.0 | Soname symlink — major version only |
libutils.so → libutils.so.1 | Linker symlink — used at compile time |
Tasks
-
Build the shared library with an embedded soname (
libutils.so.1):gcc -shared -Wl,-soname,libutils.so.1 \ -o libutils.so.1.0.0 \ strutils_pic.o mathutils_pic.o -
Create the two symlinks manually:
ln -sf libutils.so.1.0.0 libutils.so.1 ln -sf libutils.so.1 libutils.so -
Check the soname embedded in the library:
objdump -p libutils.so.1.0.0 | grep SONAME # or readelf -d libutils.so.1.0.0 | grep SONAME -
Compile your program:
gcc -Wall -Wextra main2.c -L. -lutils -Wl,-rpath,'$ORIGIN' -o prog_versioned -
Check what soname the linker recorded in the binary:
ldd prog_versioned readelf -d prog_versioned | grep NEEDED -
Simulate a library update (v1.1.0):
- Copy
libutils.so.1.0.0tolibutils.so.1.1.0 - Update the
libutils.so.1symlink to point tolibutils.so.1.1.0 - Run
prog_versioned— it should still work because the soname (libutils.so.1) did not change
- Copy
Questions
- Why does the linker record
libutils.so.1(the soname) rather thanlibutils.so.1.0.0(the real filename)? - When would you need to change the major version number (i.e. bump from
.so.1to.so.2)? - On a real system,
ldconfigupdates a cache at/etc/ld.so.cache. What command would you run instead of creating symlinks manually?
Part C — Makefile Integration (20 min)
Exercise 6 — Full Library Makefile
Difficulty: RxGo back to your TP1 Makefile project (tp_makefile/). Add the strutils and mathutils modules and write a Makefile that builds both library types along with the main executable.
Your Makefile should provide the following targets:
| Target | Action |
|---|---|
all | Build prog_static (linked against the static library) |
libutils.a | Build the static archive |
libutils.so | Build the shared library (with soname) |
clean | Remove all generated files |
Naming convention
Libraries must be named lib<name>.a or lib<name>.so for the -l<name> linker flag to work.
Summary
| Topic | Key commands |
|---|---|
| Static library | ar rcs libname.a file.o [file2.o ...] |
| Archive inspection | ar -t, nm, ar -x |
| Binary inspection | objdump -d, objdump -t, objdump -h |
| Dynamic library | gcc -fPIC -c then gcc -shared -o libname.so |
| Soname | gcc -shared -Wl,-soname,libname.so.1 |
| Runtime path | gcc -Wl,-rpath,'$ORIGIN' |
| Library inspection | ldd, readelf -d, objdump -p |