Fundamentals of Shared Libraries | EmbeddedPathashala

 

41.1 — Object Libraries
Chapter 41: Fundamentals of Shared Libraries | EmbeddedPathashala

Key Terms

Source File (.c) Object File (.o) Executable Compiler (cc/gcc) Linker (ld) Static Library Shared Library -g debug flag

What is an Object Library?

When you write a large C program, you split the code across multiple .c source files to keep things manageable. The compiler turns each .c file into an object file (.o) — a chunk of machine code that is not yet a complete program. The linker then joins all the object files together into a final executable.

The problem: if you have utility functions used by many programs (e.g., a math helper, a string formatter), you’d have to compile those same source files again and again for each program, and you’d have to list every .o file on the link command line. This is messy and slow.

The solution: group your object files into an object library. There are two types:

  • Static library (.a) — code is physically copied into each executable at link time.
  • Shared library (.so) — code lives in a separate file and is loaded at run time, shared by all programs that need it.

The Compilation Pipeline

Here is how source code becomes a running program:

Compilation Pipeline
prog.c
mod1.c
mod2.c
mod3.c
gcc -g -c (compile, no link)
prog.o
mod1.o
mod2.o
mod3.o
gcc (invokes ld linker)
prog (executable)

gcc vs ld — What is the Difference?

Many beginners think gcc is only a compiler. In reality, gcc is a driver program — it orchestrates the entire compilation process: preprocessing, compiling, assembling, and linking.

The actual linker is called ld. When you run gcc -o myprog prog.o mod1.o, gcc internally calls ld with the right flags and adds the standard C library automatically. If you called ld directly, you would have to manually specify the C runtime startup files (crt1.o, crti.o), the C library (-lc), and other details.

Rule: Always use gcc (not ld directly) to link your programs on Linux.

The -g Debug Flag

💡 Always compile with -g
The -g flag tells gcc to include debugging information (symbol names, line numbers, variable names) in the object file. This information is used by debuggers like gdb and by tools like valgrind. Modern disks and RAM are cheap — there is almost no reason NOT to use -g during development. For final production builds, you may strip it, but keep it during development.
⚠️ Avoid -fomit-frame-pointer on x86-32
On 32-bit x86, this option removes the frame pointer from stack frames, which makes debugging impossible. On x86-64, it is safe (enabled by default) since x86-64 has enough registers. Similarly, don’t use strip(1) to strip debugging symbols from executables you’re still testing.

Coding Example 1 — Basic Compilation Without a Library

This is the starting point. We have three source files and compile + link them manually.

/* math_utils.h - header for our utility functions */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);
int multiply(int a, int b);

#endif
/* math_utils.c - utility functions */
#include "math_utils.h"
#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}
/* string_utils.c - more utility functions */
#include <string.h>
#include <stdio.h>

void print_greeting(const char *name) {
    printf("Hello, %s!\n", name);
}

int count_chars(const char *str) {
    return (int)strlen(str);
}
/* main.c - main program that uses both utilities */
#include <stdio.h>
#include "math_utils.h"

/* external declaration for string_utils */
void print_greeting(const char *name);
int count_chars(const char *str);

int main(void) {
    int result = add(10, 20);
    printf("10 + 20 = %d\n", result);

    result = multiply(4, 5);
    printf("4 * 5  = %d\n", result);

    print_greeting("EmbeddedPathashala");
    printf("Name length = %d\n", count_chars("EmbeddedPathashala"));

    return 0;
}
## Build steps WITHOUT any library
## Step 1: Compile each .c file into a .o object file (no linking yet)
$ gcc -g -c main.c math_utils.c string_utils.c

## This produces: main.o  math_utils.o  string_utils.o
## Check the files:
$ ls -lh *.o

## Step 2: Link all object files into the final executable
$ gcc -g -o myprog main.o math_utils.o string_utils.o

## Run the program
$ ./myprog
# Output:
# 10 + 20 = 30
# 4 * 5  = 20
# Hello, EmbeddedPathashala!
# Name length = 18

## The -g flag embeds debug info. Use gdb to debug:
$ gdb ./myprog
(gdb) break main
(gdb) run
(gdb) next
(gdb) print result

Coding Example 2 — Understanding Object File Contents

You can inspect what’s inside an object file using tools like nm, objdump, and readelf. This helps you understand what symbols (functions, variables) an object file defines or references.

/* sensor.c - simulates reading a hardware sensor */
#include <stdio.h>

/* This function is DEFINED in this file (exported symbol) */
float read_temperature(void) {
    /* In real embedded code, you'd read a register here */
    return 36.5f;
}

/* This function calls printf — an EXTERNAL symbol from libc */
void display_sensor(void) {
    printf("Temperature: %.1f°C\n", read_temperature());
}
## Compile to object file only
$ gcc -g -c sensor.c -o sensor.o

## Use nm to list all symbols in the object file
$ nm sensor.o

## Typical output:
##   0000000000000000 T read_temperature   (T = defined in Text/code section)
##   0000000000000020 T display_sensor     (T = defined here)
##                    U printf              (U = Undefined, needs to be linked from libc)

## Use objdump to see the disassembly
$ objdump -d sensor.o

## Use objdump to see all section headers
$ objdump -h sensor.o

## Use readelf to see all symbols with full details
$ readelf -s sensor.o

## Use file command to confirm it's an object file (not executable)
$ file sensor.o
## Output: sensor.o: ELF 64-bit LSB relocatable, x86-64, ...
## Note: "relocatable" means it's an object file, not an executable

## Compare with a fully linked executable
$ gcc -g -o sensor_prog sensor.o -lc
$ file sensor_prog
## Output: sensor_prog: ELF 64-bit LSB pie executable, x86-64, ...

Interview Questions — Section 41.1

Q1. What is an object file (.o)? How is it different from an executable?
An object file (.o) is the output of compiling a single source file. It contains machine code but is not a complete program — it has unresolved references to functions and variables defined in other files (marked as “undefined” symbols). An executable is produced by the linker which resolves all these references and combines multiple object files into one self-contained program. Object files have type “relocatable” in their ELF header; executables have type “executable”.
Q2. Why do we use -c flag with gcc? What does it do?
The -c flag tells gcc to compile only, do not link. Without it, gcc would try to compile and immediately link, which would fail if you’re building a multi-file project where each file is compiled separately. With -c, you get individual .o files which can later be linked together in a separate step.
Q3. Who actually performs the linking — gcc or ld?
The actual linker is ld. However, gcc acts as a driver and invokes ld internally with all the correct options, including adding the C runtime startup code and the standard C library. You should always use gcc for linking, not call ld directly, because gcc ensures all the required files are included automatically.
Q4. What are the two types of object libraries in Linux?
Static libraries (.a) — also called archives. The linker copies the needed code from the library directly into the executable at link time. Each executable gets its own copy.

Shared libraries (.so) — the code is NOT copied into the executable. Instead, the program records the library name, and the dynamic linker loads the library into memory at run time. Multiple programs can share the same copy in memory.

Q5. Why should you always compile with the -g flag during development?
The -g flag embeds debugging symbols — function names, variable names, source file names, and line numbers — into the object file. Without it, tools like gdb and valgrind cannot show you meaningful information (they’ll show raw addresses instead of function names). Since disk and RAM are cheap today, there’s no good reason to omit -g during development. You can always strip the binary later for production.

Leave a Reply

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