Static Libraries Creating, Maintaining & Using .a Archive Libraries

 

41.2 — Static Libraries
Creating, Maintaining & Using .a Archive Libraries | EmbeddedPathashala

Key Terms

Static Library (.a) Archive ar command ar r (replace) ar t (table) ar d (delete) -l flag -L flag Selective linking libname.a convention

What is a Static Library?

A static library (also called an archive) is simply a container file that holds multiple compiled object files (.o files) packed together into one file. It uses the Unix archive format, which is why the tool to manage them is called ar (archiver).

By convention, static library filenames follow the pattern lib<name>.a. For example:

  • libmath.a — a math utility library
  • libdemo.a — a demo library
  • libc.a — the C standard library (static version)

When the linker uses a static library to build an executable, it copies the machine code of the needed functions directly into the executable binary. The resulting binary is completely self-contained — it does not need the .a file to run. But this also means that if you fix a bug in the library, you must relink all programs that use it to pick up the fix.

How a Static Library is Structured

Inside libdemo.a
The .a file is an archive containing multiple .o files
libdemo.a (archive file)
mod1.o — permissions, uid/gid, size, mtime, code
mod2.o — permissions, uid/gid, size, mtime, code
mod3.o — permissions, uid/gid, size, mtime, code
↓ linker (only copies what’s needed)
myprog (executable)
Only mod1.o and mod2.o were needed → only those are copied in

The ar Command Reference

The ar command is the tool for creating and managing static libraries. Format:

$ ar [options] archive object-file...

## options = operation code + optional modifiers
Operation Code Description Example
Replace / Create r Insert object file into archive. If an object with the same name exists, replaces it. This is the standard way to create or update an archive. ar r libdemo.a mod1.o mod2.o
Table of Contents t List the object files inside the archive. Add v modifier for verbose output (permissions, uid, size, date). ar tv libdemo.a
Delete d Remove a named object file from the archive. ar d libdemo.a mod3.o
Extract x Extract object files from the archive into the current directory. ar x libdemo.a mod1.o

Coding Example 1 — Create and Use a Static Library

We’ll build a simple embedded-style utility library with two modules: one for GPIO simulation and one for UART simulation, package them into a static library, and link a main program against it.

/* gpio.h - GPIO utility header */
#ifndef GPIO_H
#define GPIO_H

#define GPIO_OUTPUT 0
#define GPIO_INPUT  1

void gpio_init(int pin, int direction);
void gpio_set(int pin, int value);
int  gpio_get(int pin);

#endif
/* gpio.c - GPIO utility implementation */
#include "gpio.h"
#include <stdio.h>

static int gpio_state[64] = {0};  /* Simulate 64 GPIO pins */
static int gpio_dir[64] = {0};

void gpio_init(int pin, int direction) {
    if (pin < 0 || pin >= 64) return;
    gpio_dir[pin] = direction;
    gpio_state[pin] = 0;
    printf("[GPIO] Pin %d initialized as %s\n",
           pin, direction == GPIO_OUTPUT ? "OUTPUT" : "INPUT");
}

void gpio_set(int pin, int value) {
    if (pin < 0 || pin >= 64) return;
    if (gpio_dir[pin] != GPIO_OUTPUT) {
        printf("[GPIO] Error: Pin %d is not OUTPUT\n", pin);
        return;
    }
    gpio_state[pin] = value ? 1 : 0;
    printf("[GPIO] Pin %d set to %d\n", pin, gpio_state[pin]);
}

int gpio_get(int pin) {
    if (pin < 0 || pin >= 64) return -1;
    return gpio_state[pin];
}
/* uart.h - UART utility header */
#ifndef UART_H
#define UART_H

void uart_init(int baud_rate);
void uart_send(const char *msg);

#endif
/* uart.c - UART utility implementation */
#include "uart.h"
#include <stdio.h>

static int uart_baud = 0;

void uart_init(int baud_rate) {
    uart_baud = baud_rate;
    printf("[UART] Initialized at %d baud\n", baud_rate);
}

void uart_send(const char *msg) {
    if (uart_baud == 0) {
        printf("[UART] Error: Not initialized!\n");
        return;
    }
    printf("[UART] Sending: \"%s\"\n", msg);
}
/* main.c - application that uses both GPIO and UART */
#include <stdio.h>
#include "gpio.h"
#include "uart.h"

int main(void) {
    /* Initialize UART */
    uart_init(115200);

    /* Configure GPIO pins */
    gpio_init(5, GPIO_OUTPUT);   /* LED pin */
    gpio_init(3, GPIO_INPUT);    /* Button pin */

    /* Use the GPIO */
    gpio_set(5, 1);   /* Turn on LED */
    printf("LED state = %d\n", gpio_get(5));

    gpio_set(5, 0);   /* Turn off LED */
    printf("LED state = %d\n", gpio_get(5));

    /* Send a UART message */
    uart_send("Hello from EmbeddedPathashala!");

    return 0;
}
## ============================================
## BUILD STEPS: Create and use a static library
## ============================================

## Step 1: Compile the library source files into object files
## -g for debug info, -Wall for warnings, -c for compile-only
$ gcc -g -Wall -c gpio.c uart.c

## Step 2: Pack the object files into a static library
## 'r' = replace/create, 'v' = verbose output
$ ar rv libembedutils.a gpio.o uart.o
## Output:
## ar: creating libembedutils.a
## a - gpio.o
## a - uart.o

## Step 3: Verify the library contents
$ ar tv libembedutils.a
## Output:
## rw-r--r-- 1000/1000   2456 Jun  5 10:00 2025 gpio.o
## rw-r--r-- 1000/1000    816 Jun  5 10:00 2025 uart.o

## (Optional) Clean up the .o files — they're no longer needed
## since they're stored inside the .a file
$ rm gpio.o uart.o

## Step 4: Compile and link the main program against the library
## Method A: Name the library directly
$ gcc -g -Wall -c main.c
$ gcc -g -o myapp main.o libembedutils.a

## Method B: Use -l and -L flags (standard way)
$ gcc -g -o myapp main.o -L. -lembedutils
## -L. tells linker to search current directory (.)
## -lembedutils means link against libembedutils.a

## Step 5: Run the program
$ ./myapp
## [UART] Initialized at 115200 baud
## [GPIO] Pin 5 initialized as OUTPUT
## [GPIO] Pin 3 initialized as INPUT
## [GPIO] Pin 5 set to 1
## LED state = 1
## [GPIO] Pin 5 set to 0
## LED state = 0
## [UART] Sending: "Hello from EmbeddedPathashala!"

## Step 6: Check what symbols are in the library
$ nm libembedutils.a

Coding Example 2 — Updating and Selective Linking

This example shows two important concepts: (a) how to update a single module inside a static library without rebuilding everything, and (b) how the linker only includes modules that are actually needed.

/* timer.c - Timer utilities (will be added to library later) */
#include <stdio.h>
#include <time.h>

void timer_delay_ms(int ms) {
    /* Simulate delay using nanosleep */
    struct timespec ts;
    ts.tv_sec  = ms / 1000;
    ts.tv_nsec = (ms % 1000) * 1000000L;
    nanosleep(&ts, NULL);
    printf("[TIMER] Delayed %d ms\n", ms);
}

void timer_print_elapsed(clock_t start) {
    clock_t end = clock();
    double elapsed = (double)(end - start) / CLOCKS_PER_SEC;
    printf("[TIMER] Elapsed time: %.3f seconds\n", elapsed);
}
/* gpio_v2.c - Updated GPIO with new get_direction() function */
#include "gpio.h"
#include <stdio.h>

static int gpio_state[64] = {0};
static int gpio_dir[64] = {0};

void gpio_init(int pin, int direction) {
    if (pin < 0 || pin >= 64) return;
    gpio_dir[pin] = direction;
    gpio_state[pin] = 0;
    printf("[GPIO v2] Pin %d initialized as %s\n",
           pin, direction == GPIO_OUTPUT ? "OUTPUT" : "INPUT");
}

void gpio_set(int pin, int value) {
    if (pin < 0 || pin >= 64) return;
    gpio_state[pin] = value ? 1 : 0;
}

int gpio_get(int pin) {
    if (pin < 0 || pin >= 64) return -1;
    return gpio_state[pin];
}

/* NEW function added in v2 */
int gpio_get_direction(int pin) {
    if (pin < 0 || pin >= 64) return -1;
    return gpio_dir[pin];
}
## ============================================
## UPDATING A STATIC LIBRARY
## ============================================

## Current library: libembedutils.a has gpio.o and uart.o

## Add the new timer module to the library
$ gcc -g -c timer.c
$ ar rv libembedutils.a timer.o
## Output: a - timer.o   (added)

## Now update gpio.o with the new version
$ gcc -g -c gpio_v2.c -o gpio.o
$ ar rv libembedutils.a gpio.o
## Output: r - gpio.o   (replaced)

## Verify the library now has 3 modules
$ ar tv libembedutils.a
## gpio.o  uart.o  timer.o

## ============================================
## SELECTIVE LINKING DEMO
## ============================================

## prog_gpio_only.c - uses only GPIO functions (NOT uart or timer)
cat > prog_gpio_only.c << 'EOF'
#include <stdio.h>
#include "gpio.h"

int main(void) {
    gpio_init(10, GPIO_OUTPUT);
    gpio_set(10, 1);
    printf("Pin 10 = %d, direction = %d\n",
           gpio_get(10), gpio_get_direction(10));
    return 0;
}
EOF

## Compile and link against the full library
$ gcc -g -o prog_gpio_only prog_gpio_only.c -L. -lembedutils

## The linker only includes gpio.o — uart.o and timer.o are SKIPPED
## because no symbols from them are referenced!

## Verify: use nm to check what's in the executable
$ nm prog_gpio_only | grep -E "gpio|uart|timer"
## You'll see gpio_init, gpio_set, gpio_get — but NO uart or timer symbols

## Check binary sizes to confirm selective linking
$ size prog_gpio_only     # shows actual code size is small
$ objdump -d prog_gpio_only | grep -c "gpio"   # gpio functions present
$ objdump -d prog_gpio_only | grep -c "uart"   # uart NOT present

## IMPORTANT: Link order matters for static libraries!
## The library should come AFTER the object files that use it:
## CORRECT:
$ gcc -g -o prog main.o -L. -lembedutils
## WRONG (may cause "undefined reference" errors):
$ gcc -g -o prog -L. -lembedutils main.o
🔑 Key Insight — Selective Linking:
When the linker processes a static library, it scans through the object modules one by one. For each module, it checks if any symbol in that module is referenced by the current executable being built. If yes, the entire module is pulled in. If no, the module is skipped entirely. This means a large static library does NOT bloat your binary if you only use a few functions from it.
⚠️ Link Order Problem:
When using static libraries with gcc, the library (-lfoo) must come after the object files that use it on the command line. This is because the linker processes files left to right — it only knows what symbols to look for after it has scanned your .o files. If you put -lfoo first, the linker has not yet seen any undefined symbols, so it skips all modules in the library.

How the Linker Finds Static Libraries

The linker searches for libraries in a specific order:

  1. Directories specified with -L<dir> on the command line (left to right)
  2. Directories in the LIBRARY_PATH environment variable
  3. Standard system directories: /usr/lib, /lib, /usr/local/lib

When you specify -ldemo, the linker looks for a file named libdemo.a (or libdemo.so for shared) in each directory. Note: the lib prefix and .a suffix are added automatically — you only provide the base name.

## Different ways to link against libdemo.a:

## Method 1: Specify the full path (explicit)
$ gcc -g -o prog main.o /home/ravi/mylibs/libdemo.a

## Method 2: Name the library directly (full filename)
$ gcc -g -o prog main.o libdemo.a

## Method 3: Use -L and -l (preferred/portable)
$ gcc -g -o prog main.o -L/home/ravi/mylibs -ldemo
## -L tells linker: also search this directory
## -l tells linker: find libdemo.a or libdemo.so here

## Method 4: Install to standard location
$ sudo cp libdemo.a /usr/local/lib/
$ gcc -g -o prog main.o -ldemo   # No -L needed!

## Method 5: Multiple -L paths (searched in order)
$ gcc -g -o prog main.o -L./mylibs -L/opt/libs -ldemo -lutils

Interview Questions — Section 41.2

Q1. What is a static library and what file extension does it use on Linux?
A static library (also called an archive) is a collection of compiled object files (.o files) bundled into a single file. On Linux, static libraries have the .a extension and follow the naming convention lib<name>.a. For example, the static version of the math library is libm.a. The archive format stores each .o file along with metadata like permissions, user/group IDs, and modification time.
Q2. What does the ar rv libdemo.a mod1.o mod2.o command do?
The ar command creates/manages static library archives.
r = replace operation: insert the object files into the archive, replacing any existing files with the same name. If the archive doesn’t exist, it is created.
v = verbose modifier: print what’s being done.
This command creates (or updates) libdemo.a by inserting mod1.o and mod2.o into it.
Q3. What is the difference between -L and -l in a gcc command?
-L<dir> adds a directory to the list of places the linker searches for library files. For example, -L/home/ravi/libs means “also look in this directory.”

-l<name> specifies which library to link against. The linker automatically prepends “lib” and appends “.a” (or “.so”). So -lembedutils makes the linker look for libembedutils.a or libembedutils.so in all search paths.

Q4. Does the linker include ALL modules from a static library in the executable?
No. The linker uses selective linking — it only includes object modules from the static library that contain symbols actually referenced by the program. If your program uses only gpio_init() and gpio_set(), only the gpio.o module is included. Modules with unreferenced symbols are skipped entirely. This keeps executables small.
Q5. What is the main disadvantage of static libraries compared to shared libraries?
The key disadvantages are:

  • Code duplication: Every program that uses the library gets its own copy of the library code embedded in it, wasting disk space.
  • RAM waste: Each running program has its own copy of the library code in RAM.
  • No runtime updates: If you fix a bug in the library, you must recompile and relink every program that uses it. With shared libraries, you just replace the .so file.
  • Larger binaries: Executables are bigger because they contain all library code.
Q6. Why must -l come AFTER object files in a gcc link command?
The GNU linker processes files from left to right. When it encounters a library with -l, it checks which symbols the library provides against the list of currently unresolved symbols. If no object files have been processed yet, the unresolved list is empty, so the linker skips all library modules. By placing -l after .o files, the linker already knows which symbols your code needs, and can correctly extract the right modules from the library.

Leave a Reply

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