Skip to main content
Compilation Toolchain
Compilation Toolchain
IOT
Rx 2h Practical #1: Makefile

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: Easy

You 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

  1. Compile the library source to an object file:

    gcc -Wall -Wextra -c strutils.c -o strutils.o
  2. Create the static archive:

    ar rcs libstrutils.a strutils.o
  3. Link the main program against the static library:

    gcc -Wall -Wextra main.c -L. -lstrutils -o prog_static
    ./prog_static
  4. Inspect the library:

    ar -t libstrutils.a       # list archive contents
    nm libstrutils.a           # list symbols

Questions

  1. What do the r, c, s flags of ar mean?
  2. Delete libstrutils.a and run ./prog_static again. Does it still work? Why?

Exercise 2 — Multi-file Static Library

Difficulty: Rx

Real 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

  1. Compile both modules to object files:

    gcc -Wall -Wextra -c strutils.c -o strutils.o
    gcc -Wall -Wextra -c mathutils.c -o mathutils.o
  2. Bundle them into a single archive called libutils.a:

    ar rcs libutils.a strutils.o mathutils.o
  3. Verify both modules are present:

    ar -t libutils.a
    nm libutils.a
  4. Write a new main2.c that uses at least one function from each module, compile it against libutils.a, and run it.

  5. Extract a single object file from the archive without unpacking everything:

    ar -x libutils.a mathutils.o

Questions

  1. What is the difference between ar -t and nm in terms of what they display?
  2. If you only use str_len in your program, does the linker include the mathutils.o module 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.

  1. Disassemble the strutils.o object file:

    objdump -d strutils.o
  2. List the symbol table with sizes:

    objdump -t strutils.o
  3. Display section headers:

    objdump -h strutils.o
  4. Now run the same commands on the final prog_static executable. Notice that the functions are now embedded in the binary at concrete addresses.

Questions

  1. In the object file, function addresses start at 0x0000000000000000. Why?
  2. After linking, what addresses are the str_len and str_copy functions at?
  3. 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: Rx

Build the same source as a shared library (.so).

  1. 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
  2. Create the shared library from both modules:

    gcc -shared -o libutils.so strutils_pic.o mathutils_pic.o
  3. Link the main program:

    gcc -Wall -Wextra main2.c -L. -lutils -o prog_dynamic
  4. Try to run it:

    ./prog_dynamic
Warning

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.

  1. Fix it using one of these approaches:

    Option A — LD_LIBRARY_PATH (temporary)

    LD_LIBRARY_PATH=. ./prog_dynamic

    Option B — rpath (embedded in executable)

    gcc -Wall -Wextra main2.c -L. -lutils -Wl,-rpath,'$ORIGIN' -o prog_dynamic
    ./prog_dynamic
  2. Compare the two executables:

    ls -l prog_static prog_dynamic
    ldd prog_dynamic

Questions

  1. Which executable is larger? Why?
  2. What does ldd show for prog_dynamic? What about prog_static?
  3. Delete libutils.so and run ./prog_dynamic. What happens? How does this differ from the static case?

Exercise 5 — Library Versioning and Soname

Difficulty: Hard

In 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

FileRole
libutils.so.1.0.0Real file — full version
libutils.so.1libutils.so.1.0.0Soname symlink — major version only
libutils.solibutils.so.1Linker symlink — used at compile time

Tasks

  1. 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
  2. Create the two symlinks manually:

    ln -sf libutils.so.1.0.0 libutils.so.1
    ln -sf libutils.so.1      libutils.so
  3. 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
  4. Compile your program:

    gcc -Wall -Wextra main2.c -L. -lutils -Wl,-rpath,'$ORIGIN' -o prog_versioned
  5. Check what soname the linker recorded in the binary:

    ldd prog_versioned
    readelf -d prog_versioned | grep NEEDED
  6. Simulate a library update (v1.1.0):

    • Copy libutils.so.1.0.0 to libutils.so.1.1.0
    • Update the libutils.so.1 symlink to point to libutils.so.1.1.0
    • Run prog_versioned — it should still work because the soname (libutils.so.1) did not change

Questions

  1. Why does the linker record libutils.so.1 (the soname) rather than libutils.so.1.0.0 (the real filename)?
  2. When would you need to change the major version number (i.e. bump from .so.1 to .so.2)?
  3. On a real system, ldconfig updates 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: Rx

Go 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:

TargetAction
allBuild prog_static (linked against the static library)
libutils.aBuild the static archive
libutils.soBuild the shared library (with soname)
cleanRemove all generated files
Info

Naming convention

Libraries must be named lib<name>.a or lib<name>.so for the -l<name> linker flag to work.


Summary

TopicKey commands
Static libraryar rcs libname.a file.o [file2.o ...]
Archive inspectionar -t, nm, ar -x
Binary inspectionobjdump -d, objdump -t, objdump -h
Dynamic librarygcc -fPIC -c then gcc -shared -o libname.so
Sonamegcc -shared -Wl,-soname,libname.so.1
Runtime pathgcc -Wl,-rpath,'$ORIGIN'
Library inspectionldd, readelf -d, objdump -p