Chapter 89: Shared Libraries (.so): Creation Process (PIC, -shared)

Chapter Objectives

By the end of this chapter, you will be able to:

  • Understand the fundamental differences between static and dynamic linking and their respective impacts on memory usage and application maintenance.
  • Explain the concept of Position-Independent Code (PIC) and its critical role in enabling shared libraries.
  • Implement the complete workflow for creating a shared library (.so file) from C source code using the GCC toolchain.
  • Configure an embedded Linux system to correctly locate and use shared libraries at runtime using ldconfig and the LD_LIBRARY_PATH environment variable.
  • Debug common issues related to shared library creation and usage, such as unresolved symbols and linking errors.
  • Analyze the structure of shared libraries using tools like readelf to inspect their internal components, such as the Global Offset Table (GOT).

Introduction

In the world of embedded systems, efficiency is paramount. Every byte of RAM and every CPU cycle is a precious resource. As embedded applications grow in complexity, managing this efficiency becomes a significant challenge. Imagine an embedded device, perhaps a smart home hub or an industrial controller, running a dozen different applications. If each application includes its own copy of common functionalities—like a math library, a string manipulation utility, or a communication protocol stack—the result is a tremendous waste of memory. This is where the power of dynamic linking and shared libraries becomes evident.

shared library, known on Linux systems by its .so (Shared Object) extension, is a collection of compiled code designed to be shared by multiple programs simultaneously. Instead of statically linking the same code into every application that needs it, a single copy of the shared library is loaded into memory by the operating system. Each application can then access this shared code, drastically reducing the overall memory footprint of the system. This approach not only saves space but also simplifies software maintenance. To update a shared function, you only need to replace the shared library file; all applications using it will benefit from the update automatically, without needing to be recompiled themselves.

This chapter delves into the core principles and practical mechanics of creating and using shared libraries in an embedded Linux environment, using the Raspberry Pi 5 as our development platform. We will explore the crucial concept of Position-Independent Code (PIC), the cornerstone that allows a single library to be loaded at different memory addresses for different applications. You will learn the specific GCC compiler and linker flags—-fPIC and -shared—that transform your source code into a functional shared library. By the end of this chapter, you will have the practical skills to build, deploy, and manage shared libraries, a fundamental technique for creating modular, efficient, and maintainable embedded Linux systems.

Technical Background

The Tale of Two Linkers: Static vs. Dynamic Linking

Before we can appreciate the elegance of shared libraries, we must first understand the process that makes them possible: linking. After the compiler translates your human-readable source code into machine-readable object files (.o files), the linker’s job is to assemble these object files, along with any required library code, into a final executable program. This process can happen in one of two ways: statically or dynamically.

Static linking is the more straightforward approach. The linker acts like a meticulous archivist, finding every piece of code a program needs—from your own object files and from any static libraries (.a files)—and copying it directly into the final executable file. The result is a single, self-contained binary. This has the advantage of simplicity and portability; the executable has no external dependencies and will run on any compatible system without needing to find specific library files. However, this simplicity comes at a cost. As mentioned earlier, if multiple programs on a system use the same static library, each will have its own redundant copy embedded within it, consuming significant disk space and RAM. Furthermore, updating a function in a static library requires recompiling and relinking every single application that uses it—a maintenance nightmare for complex systems.

Dynamic linking, in contrast, is a more sophisticated and efficient process. Instead of copying library code into the executable, the linker places a small stub or placeholder in the binary. This stub essentially says, “At runtime, I will need function X from shared library Y.so.” When you run the program, a special part of the operating system called the dynamic linker (or loader, typically /lib/ld-linux.so.3 on modern systems) springs into action. It reads the placeholders in the executable, finds the required shared libraries (.so files) on the system, and loads them into memory. The dynamic linker then resolves the placeholders, patching the program’s memory so that calls to library functions are correctly redirected to the shared code. If another program that needs the same library is started, the dynamic linker is smart enough to see that the library is already in memory and will simply map it into the new program’s address space, rather than loading a second copy. This “load-once, share-many” model is the key to the memory efficiency of dynamic linking.

Feature Static Linking Dynamic Linking
Executable Size Larger. All library code is copied into each executable. Smaller. Executable only contains stubs to shared code.
Memory Usage Higher. Each running application has its own copy of library code in RAM. Lower. A single copy of the library is loaded into memory and shared among all applications.
Update Process Difficult. Every application must be re-compiled and re-linked to update the library. Easy. Replace the central .so file, and all applications use the new version on their next run.
Runtime Dependencies None. The executable is fully self-contained. High. The required .so files must be present on the target system for the application to run.
Startup Time Generally faster, as no dynamic linking work is needed at launch. Slightly slower, due to the work done by the dynamic linker to find and load libraries.
Use Case Small, single-purpose embedded systems or situations where portability is paramount. Complex systems with multiple applications, desktop OSs, and most modern embedded Linux systems.

The Challenge of Sharing: Position-Independent Code (PIC)

The “share-many” aspect of dynamic linking introduces a complex problem. The operating system provides each process with its own private, virtual address space. For security and flexibility, the exact memory address where a shared library will be loaded can change every time a program runs. This is a feature known as Address Space Layout Randomization (ASLR). If one program loads libc.so at memory address 0xb7400000 and another loads it at 0xb7800000, how can the same library code work correctly in both places?

If the library’s code contained absolute memory addresses—for example, a jump instruction like JMP 0x12345678—it would fail catastrophically. The address 0x12345678 might be valid within the context of the first program, but it would point to garbage or protected memory in the second. The library code must be written in a way that it doesn’t depend on being loaded at any specific, fixed address. This is the principle of Position-Independent Code (PIC).

PIC is generated by the compiler when you use the -fPIC flag. It solves the problem of absolute addressing by using relative addressing instead. Instead of saying “jump to address X,” a PIC instruction says “jump to the address Y bytes forward from my current location.” Since the relative distance between different parts of the library code is always the same, these jumps work regardless of where the library is loaded in memory.

However, this only solves part of the problem. What about accessing global variables or calling functions that are also inside the library? The code still needs a way to find their addresses. This is where the magic of the Global Offset Table (GOT) and the Procedure Linkage Table (PLT) comes in.

The Global Offset Table (GOT) and Procedure Linkage Table (PLT)

To achieve true position independence, the linker separates the code (the .text section), which is immutable and shared, from the data (the .data section), which can be modified and is private to each process. It then creates two special data sections to act as intermediaries for all external memory accesses: the Global Offset Table (GOT) and the Procedure Linkage Table (PLT).

Think of the Global Offset Table (GOT) as an address book for the shared library. It’s a table of memory addresses located in the library’s private data section. When the library code needs to access a global variable, instead of trying to use a hardcoded, absolute address, it does the following:

  1. It calculates its own current position in memory.
  2. It uses this position to find the start of the GOT (which is at a fixed, relative offset).
  3. It looks up the correct entry in the GOT to get the variable’s actual memory address.
  4. It then uses that address to access the variable.

This process ensures that even though the variable’s absolute address changes with each program execution, the library code can always find it through the indirection provided by the GOT. The dynamic linker is responsible for filling in the correct addresses in the GOT when it first loads the library into memory.

The Procedure Linkage Table (PLT) provides a similar mechanism of indirection, but specifically for function calls. Calling functions is slightly more complex because we want to avoid the overhead of looking up the function’s address every single time it’s called. The PLT enables a technique called lazy binding.

Here’s how it works:

  1. When your code calls a function in a shared library for the very first time, it doesn’t actually jump to the function. Instead, it jumps to an entry in the PLT.
  2. This PLT entry contains a special piece of code that, in turn, jumps to a helper routine inside the dynamic linker itself.
  3. The dynamic linker’s routine looks up the real address of the requested function (e.g., printf).
  4. Crucially, it then patches the corresponding entry in the GOT with this real address.
  5. Finally, it jumps to the real function.

Now, the next time your code calls the same function, the process is much faster. The call again goes to the PLT entry, but this time, the PLT entry simply jumps to the address now stored in the GOT. The expensive lookup process by the dynamic linker is skipped. This lazy resolution means that the overhead of finding functions is only paid for the functions that are actually used, improving application startup time.

flowchart TD
    subgraph Application
        A["<b>Start:</b><br>Application calls<br>a library function e.g., <i>printf()</i>"]
    end

    subgraph "Indirection Layers"
        B["Process Jumps to<br>Procedure Linkage Table<br>(PLT) entry for <i>printf</i>"]
        C{"<br>Is address of <i>printf</i><br>in the Global Offset Table<br>(GOT) resolved?"};
    end

    subgraph "Dynamic Linker (ld.so) - First Call Only"
        D[PLT jumps to helper<br>routine in the<br>Dynamic Linker]
        E[Linker searches libraries<br>to find the real address<br>of <i>printf</i>]
        F[<b>Patch GOT:</b><br>Linker writes the real<br>address into the GOT entry<br>for <i>printf</i>]
    end
    
    subgraph "Shared Library Code"
        G[<b>Execute:</b><br>Jump to the real<br><i>printf</i> function in memory]
    end

    A --> B;
    B --> C;
    C -- No --> D;
    D --> E;
    E --> F;
    F --> G;
    C -- Yes (Subsequent Calls) --> G;

    %% Styling
    style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
    style G fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
    style C fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
    style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
    style D fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
    style E fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
    style F fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff

The ELF Format: A Blueprint for Executables and Libraries

The structure that orchestrates this complex dance of linking and loading is the Executable and Linkable Format (ELF). ELF is the standard binary file format used by most modern Unix-like operating systems, including Linux. It defines the structure for executables, object files, and, most importantly for us, shared libraries.

An ELF file is composed of a few key components:

  • ELF Header: This is at the very beginning of the file and acts as a roadmap. It contains essential information like the file type (e.g., executable or shared object), the target machine architecture (e.g., AArch64 for the Raspberry Pi 5), and pointers to the other major components of the file.
  • Program Header Table: This table tells the system how to create a process image in memory. For an executable, it describes which segments of the file (like the code and data sections) need to be loaded into memory and what their permissions should be (e.g., read-only and executable for code, read-write for data).
  • Section Header Table: This table provides a more detailed view of the file’s contents, breaking it down into “sections.” Sections hold the bulk of the object file information: compiled code (.text), initialized data (.data), uninitialized data (.bss), the GOT, the PLT, symbol tables (.symtab), and relocation information. When the linker combines object files, it merges sections of the same type.
  • Sections: These are the actual chunks of code and data. For a shared library, the most relevant sections are .text (the PIC code), .got, and .plt, as well as the dynamic linking information found in sections like .dynamic and .dynsym. The .dynamic section contains entries that the dynamic linker uses, such as a list of required libraries and the location of the GOT.

When you use the gcc -shared command, you are instructing the linker to produce an ELF file of type DYN (shared object). This file will contain the necessary sections and program headers to allow the dynamic linker to load it into memory, perform relocations (i.e., fill in the GOT), and map it into a process’s address space correctly. Understanding the ELF structure is key to troubleshooting linking problems, as tools like readelf and objdump allow you to peer inside these files and see exactly how they are constructed.

Practical Examples

Now that we have a solid theoretical foundation, let’s put it into practice. In this section, we will create a simple shared library for a custom logging utility, compile an application that uses it, and run it on our Raspberry Pi 5.

graph TD
    subgraph "1- Library Creation"
        A(Write Library Code<br><i>log.c, log.h</i>)
        B(Compile to Object File<br><b>gcc -c -fPIC log.c -o log.o</b>)
        C(Link into Shared Library<br><b>gcc -shared log.o -o libmylogger.so</b>)
    end

    subgraph "2- Application Creation"
        D(Write Application Code<br><i>main.c</i>)
        E(Compile and Link Application<br><b>gcc main.c -L. -lmylogger -o my_app</b>)
    end
    
    subgraph "3- Execution"
        F{Run Application<br><b>./my_app</b>}
        G{Dynamic Linker<br>Finds <i>libmylogger.so</i>?}
        H[<b>Success!</b><br>Program runs correctly]
        I["<b>Error!</b><br>"cannot open shared<br>object file""]
    end
    
    subgraph "4- Solution for Runtime"
        J(Use <b>LD_LIBRARY_PATH</b><br>for development)
        K(Install to system dir<br>& run <b>ldconfig</b> for production)
    end

    A --> B --> C
    D --> E
    C --> E
    E --> F
    F --> G
    G -- Yes --> H
    G -- No --> I
    I --> J
    I --> K

    %% Styling
    style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
    style D fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff
    style H fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
    style I fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff
    style G fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff
    style B fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
    style C fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
    style E fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
    style F fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff
    style J fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937
    style K fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937

Step 1: Setting Up the Project Environment

First, let’s create a directory for our project and organize our files. A clean structure makes managing larger projects much easier.

Bash
# On your Raspberry Pi 5 or cross-compilation host
mkdir shared_lib_project
cd shared_lib_project
mkdir mylogger
mkdir app

Our project will have two main parts:

  • mylogger/: This directory will contain the source code for our shared logging library.
  • app/: This directory will contain the source code for a simple test application that uses our library.

Step 2: Creating the Shared Library Source Code

We will create a simple logger that provides functions to log messages at different levels (INFO, WARNING, ERROR).

Create a header file mylogger/log.h:

C
// File: mylogger/log.h

#ifndef LOG_H
#define LOG_H

/*
 * @brief Logs an informational message.
 * @param msg The message string to log.
 */
void log_info(const char *msg);

/*
 * @brief Logs a warning message.
 * @param msg The message string to log.
 */
void log_warning(const char *msg);

/*
 * @brief Logs an error message.
 * @param msg The message string to log.
 */
void log_error(const char *msg);

#endif // LOG_H

Now, create the implementation file mylogger/log.c:

C
// File: mylogger/log.c

#include <stdio.h>
#include "log.h"

void log_info(const char *msg) {
    printf("[INFO] %s\n", msg);
}

void log_warning(const char *msg) {
    // In a real application, this might write to a different file or use color
    fprintf(stdout, "[WARNING] %s\n", msg);
}

void log_error(const char *msg) {
    // Errors are typically written to standard error
    fprintf(stderr, "[ERROR] %s\n", msg);
}

This is a very basic library, but it’s perfect for demonstrating the compilation and linking process.

Step 3: Compiling the Shared Library

This is the most critical step. We need to compile log.c into a shared object file, libmylogger.so. This involves two key GCC flags:

  • -fPIC: This tells the compiler to generate Position-Independent Code. As we discussed, this is essential for any code that will be part of a shared library.
  • -shared: This tells the linker to create a shared library (.so file) instead of a standard executable.

Let’s execute the command. Navigate to the mylogger directory first.

Bash
cd mylogger

# Compile log.c into an object file with Position-Independent Code
# -c: Compile and assemble, but do not link.
# -fPIC: Generate position-independent code.
# -o log.o: Specify the output object file name.
gcc -c -fPIC -o log.o log.c

# Link the object file into a shared library
# -shared: Produce a shared object which can then be linked with other objects to form an executable.
# -o libmylogger.so: The output file name. By convention, shared libraries are named lib<name>.so
gcc -shared -o libmylogger.so log.o

# Let's see what we've created
ls -l

You should see the following files in your mylogger directory:

  • log.c: The original source code.
  • log.h: The header file.
  • log.o: The position-independent object file.
  • libmylogger.so: Our brand new shared library!

Tip: The lib prefix in libmylogger.so is a standard naming convention on Linux. When you later link against this library using the -l flag (e.g., -lmylogger), the linker will automatically search for a file named libmylogger.so.

Step 4: Creating the Test Application

Now we need a program that actually uses our library. Go back to the project root and into the app directory.

Bash
cd ../app

Create a file named main.c:

C
// File: app/main.c

#include "../mylogger/log.h" // Include the library header

int main() {
    log_info("Application starting up.");
    log_warning("Configuration file not found, using defaults.");
    
    // Simulate some work
    for (int i = 0; i < 3; ++i) {
        log_info("Doing work...");
    }

    log_error("Failed to connect to the sensor device!");
    log_info("Application shutting down.");

    return 0;
}

This simple program includes our log.h header and calls the functions we defined.

Step 5: Compiling and Linking the Application

To compile main.c, we need to tell the compiler where to find the log.h header file and how to link against our libmylogger.so library.

Bash
# Compile the application and link it against our shared library
# -I../mylogger: Tells the compiler to look in the ../mylogger directory for header files.
# -L../mylogger: Tells the linker to look in the ../mylogger directory for library files.
# -lmylogger: Tells the linker to link against the 'mylogger' library. The linker will find libmylogger.so.
# -o test_app: The name of our final executable.
gcc main.c -I../mylogger -L../mylogger -lmylogger -o test_app

# Check the result
ls -l

You should now have an executable file named test_app in the app directory.

Step 6: Running the Application and Solving the Final Puzzle

We have our library and our application. Let’s try to run it.

Bash
./test_app

You will likely be greeted with an error message similar to this:

Bash
./test_app: error while loading shared libraries: libmylogger.so: cannot open shared object file: No such file or directory

What happened? We told the linker where to find the library at compile time with the -L flag, but we haven’t told the dynamic linker where to find it at run time. The dynamic linker, by default, only looks in a few standard locations (like /lib and /usr/lib). Our project directory isn’t one of them.

We have two common ways to solve this on a development system:

Method 1: Using the LD_LIBRARY_PATH Environment Variable

This is the quickest method for testing. The LD_LIBRARY_PATH variable gives the dynamic linker an extra list of directories to search for shared libraries.

Bash
# Prepend the path to our library to LD_LIBRARY_PATH and run the app
export LD_LIBRARY_PATH=../mylogger:$LD_LIBRARY_PATH
./test_app

Now, you should see the expected output:

Plaintext
[INFO] Application starting up.
[WARNING] Configuration file not found, using defaults.
[INFO] Doing work...
[INFO] Doing work...
[INFO] Doing work...
[ERROR] Failed to connect to the sensor device!
[INFO] Application shutting down.

Success! The dynamic linker found our library, loaded it, and the application ran correctly.

Warning: While LD_LIBRARY_PATH is convenient for development, it’s generally considered bad practice for production systems. It can be insecure and can lead to unpredictable behavior if multiple versions of the same library exist on the system.

Method 2: Installing the Library and Updating the Cache (The “Proper” Way)

For a production embedded system, you would install the library into a standard system directory and update the dynamic linker’s cache.

Bash
# First, let's copy our library to a standard location
# We'll use /usr/local/lib, which is a common place for custom libraries
sudo cp ../mylogger/libmylogger.so /usr/local/lib/

# Now, we need to tell the dynamic linker to update its cache of available libraries.
# The ldconfig command scans standard directories and creates the cache file /etc/ld.so.cache.
sudo ldconfig

# Unset our temporary LD_LIBRARY_PATH to prove this works
unset LD_LIBRARY_PATH

# Now run the application again
./test_app

The application should run perfectly again. This is the robust, production-ready method for deploying shared libraries on an embedded system. The system now knows about libmylogger.so permanently (or until it’s removed).

Method Description Best Use Case Pros & Cons
Using LD_LIBRARY_PATH An environment variable that tells the dynamic linker which extra directories to search for shared libraries. The setting is temporary and specific to the current shell session. Development & Testing. Perfect for quickly testing a library without installing it system-wide. + Quick and easy to set up.
+ Doesn’t require root privileges.
+ Avoids cluttering system directories.
Considered bad practice for production.
Can cause conflicts if multiple library versions exist.
Must be set in every new terminal session.
Installing & Using ldconfig The library file is copied to a standard system directory (e.g., /usr/local/lib), and the ldconfig command is run to update the system’s shared library cache. Production & Deployment. The standard, robust way to make a library available to all applications on a system. + System-wide, permanent solution.
+ More secure and predictable.
+ The standard deployment method.
Requires root privileges (sudo) to install and run ldconfig.
Can be more complex to manage in build scripts.

Common Mistakes & Troubleshooting

Even with a clear process, developers new to shared libraries often encounter a few common pitfalls. Understanding these can save hours of frustrating debugging.

Mistake / Issue Symptom(s) Troubleshooting / Solution
Forgetting -fPIC Linker error during library creation:
“…relocation … cannot be used when making a shared object; recompile with -fPIC”
The object code contains absolute addresses. You must recompile all source files for the library with the -fPIC flag.
Example: gcc -c -fPIC my_code.c -o my_code.o
Runtime Linker Error Application fails to start with error:
“error while loading shared libraries: libmylib.so: cannot open shared object file…”
The dynamic linker (ld.so) cannot find your library.
1. For Dev: Use export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH
2. For Prod: Copy to /usr/local/lib and run sudo ldconfig.
3. Debug with: ldd ./your_app
“Undefined reference” Application fails to link with error:
“undefined reference to `my_function`”
The linker can’t find the function’s implementation. Check for:
1. Missing flags: Did you forget -L/path/to/lib or -lmylib?
2. Mismatched names: Does the function name in .c match the .h file exactly?
3. Verify Symbol: Use nm -D libmylib.so | grep my_function to see if the function is exported.
Mixing PIC/non-PIC code Can cause the -fPIC error or other strange linker errors, especially in larger projects. All object files being linked into a single shared library must be compiled with -fPIC. Ensure your build system (e.g., Makefile) applies the flag consistently. A full clean and rebuild is often the best fix.

Exercises

  1. Create a Simple Math Library:
    • Objective: Reinforce the basic library creation workflow.
    • Task: Create a shared library named libmymath.so that provides three functions: int add(int a, int b);int subtract(int a, int b);, and long long multiply(int a, int b);. Write a test application that uses these functions and prints the results.
    • Verification: The application should compile, link, and run correctly (using LD_LIBRARY_PATH), printing the correct mathematical results.
  2. Inspect Your Library with readelf:
    • Objective: Understand the internal structure of an ELF shared object.
    • Task: Run the command readelf -d libmymath.so (from Exercise 1). Examine the output.
    • Verification: Identify the entry tagged as (NEEDED) or (SONAME). Find the (PLTGOT) entry. This shows you the dynamic information the loader uses. Try to find other familiar tags.
  3. The Power of Updates:
    • Objective: Demonstrate the key advantage of shared libraries—updating without recompiling the main application.
    • Task:
      1. Modify the multiply function in your libmymath.so library to print a debug message to the console, like printf("Inside multiply function!\\n");.
      2. Recompile only the shared library. Do not touch or recompile the test application from Exercise 1.
      3. Run the test application again.
    • Verification: The application should now print the debug message when it calls the multiply function, proving that it loaded the new version of the library dynamically.
  4. Dependency Management:
    • Objective: Understand how libraries can depend on other libraries.
    • Task: Create a new “advanced math” library, libadvmath.so, with a function long long power(int base, int exp);. Implement this function using the standard math library’s pow() function (which requires linking with -lm). Now, write a test application that calls power() and link it against both libadvmath and m.
    • Verification: Use the ldd command on your final test application (ldd ./your_app). The output should show that your application depends on libadvmath.so, and libadvmath.so in turn depends on libm.so (the system math library).
  5. Troubleshooting Practice:
    • Objective: Learn to recognize and fix common errors.
    • Task: Intentionally make the mistakes described in the “Common Mistakes & Troubleshooting” section.
      1. Recompile libmymath.so without -fPIC and observe the linker error.
      2. Compile the test application but “forget” the -lmylogger flag and observe the “undefined reference” error.
      3. Move libmymath.so to a new directory and run the test app without updating LD_LIBRARY_PATH to see the “cannot open shared object” error.
    • Verification: Successfully identify, understand, and then fix each error to get the program working again.

Summary

  • Static vs. Dynamic Linking: Static linking copies all code into the executable, creating large, self-contained files. Dynamic linking uses placeholders, loading shared code from .so files at runtime, which saves memory and simplifies updates.
  • Position-Independent Code (PIC): This is the core technology that allows shared library code to run correctly regardless of where it’s loaded in memory. It is enabled with the -fPIC compiler flag and is mandatory for all code destined for a shared library.
  • GOT and PLT: The Global Offset Table and Procedure Linkage Table are key mechanisms that enable PIC. They provide a layer of indirection for accessing global variables and functions, allowing the dynamic linker to resolve their true addresses at runtime.
  • Creating a Shared Library: The process involves compiling source files to object files with gcc -c -fPIC and then linking those object files into a shared library with gcc -shared -o libname.so ....
  • Using a Shared Library: An application is compiled by telling the compiler where to find the library’s headers (-I/path/to/headers) and telling the linker where to find the library file and which one to use (-L/path/to/lib -lname).
  • Runtime Linking: The dynamic linker must be able to find the .so file at runtime. This is achieved either by setting the LD_LIBRARY_PATH environment variable or by installing the library in a standard system directory and running ldconfig.

Further Reading

  1. ELF-64 Object File Format v1.5: The official specification for the ELF format. While dense, it is the ultimate source of truth. (Search for “System V Application Binary Interface AMD64 Architecture Processor Supplement”).
  2. GCC Command-Line Options: The official documentation for the GNU Compiler Collection, detailing the -fPIC and -shared flags among many others. (https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html)
  3. How To Write Shared Libraries by Ulrich Drepper: An in-depth paper by a former lead glibc developer that covers the topic in exhaustive detail. It is a classic and highly authoritative resource.
  4. ld.so(8) Linux Manual Page: The man page for the dynamic linker/loader. It explains the search path rules, LD_LIBRARY_PATH, and the cache file in detail. (man 8 ld.so)
  5. readelf(1) Linux Manual Page: The man page for the readelf utility, an indispensable tool for inspecting the contents of ELF files. (man 1 readelf)
  6. Computer Systems: A Programmer’s Perspective by Randal E. Bryant and David R. O’Hallaron: Chapter 7, “Linking,” provides an excellent university-level explanation of the entire linking process, including static, dynamic, PIC, and the role of the GOT and PLT.
  7. Raspberry Pi Documentation: Official hardware and software documentation for the Raspberry Pi, useful for platform-specific configurations. (https://www.raspberrypi.com/documentation/)

Leave a Comment

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

Scroll to Top