What is Position-Independent Code (PIC)?
When a shared library is loaded by the dynamic linker, it can be placed at any virtual address in the process’s memory — the exact address is not known at compile time. This creates a problem: how can library code reference its own functions and global variables if it doesn’t know where it will be in memory?
The solution is Position-Independent Code (PIC). When you compile with -fPIC, gcc generates code that uses relative addressing instead of absolute addresses. It accesses global data through a Global Offset Table (GOT) and calls external functions through a Procedure Linkage Table (PLT). These tables are filled in at load time by the dynamic linker with the correct addresses.
The key benefit: because the code never hard-codes any absolute addresses, the same physical code pages can be mapped into different virtual addresses in different processes — enabling true memory sharing between processes.
(relative, not absolute)
[1] → actual address of var2
Step-by-Step: Building a Shared Library
-fPICThis generates position-independent object files. Do NOT forget this flag — without it, the linker will refuse to create a shared library (or produce one that won’t share memory properly).
$ gcc -g -fPIC -Wall -c gpio.c uart.c timer.c
-shared and set the sonameThe
-Wl,-soname,libname.so.1 flag embeds the soname into the .so’s ELF header.
$ gcc -g -shared -Wl,-soname,libembedutils.so.1 \
-o libembedutils.so.1.0.0 \
gpio.o uart.o timer.o
$ ln -sf libembedutils.so.1.0.0 libembedutils.so.1 # soname
$ ln -sf libembedutils.so.1 libembedutils.so # linker name
$ gcc -g -o myapp main.c -L. -lembedutils
## Option 1: Set LD_LIBRARY_PATH (for testing)
$ LD_LIBRARY_PATH=. ./myapp
## Option 2: Install to /usr/local/lib and run ldconfig (for deployment)
$ sudo cp libembedutils.so.1.0.0 /usr/local/lib/
$ sudo ldconfig
$ ./myapp
Coding Example 1 — Full Shared Library Build
A complete embedded I2C simulation library built and distributed as a shared library.
/* i2c_lib.h - I2C simulation library header */
#ifndef I2C_LIB_H
#define I2C_LIB_H
#define I2C_SUCCESS 0
#define I2C_ERR_NACK -1
#define I2C_ERR_BUS -2
/* Initialize I2C bus at given frequency */
int i2c_init(int bus_num, int freq_hz);
/* Write bytes to a device */
int i2c_write(int bus_num, unsigned char addr,
const unsigned char *data, int len);
/* Read bytes from a device */
int i2c_read(int bus_num, unsigned char addr,
unsigned char *buf, int len);
/* Close the bus */
void i2c_close(int bus_num);
/* Get library version string */
const char* i2c_lib_version(void);
#endif /* I2C_LIB_H */
/* i2c_lib.c - I2C simulation implementation */
#include "i2c_lib.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* ========================================
* IMPORTANT: This file must be compiled
* with -fPIC to be used in a shared lib!
* ======================================== */
#define MAX_BUS 4
static int bus_initialized[MAX_BUS] = {0};
static int bus_freq[MAX_BUS] = {0};
/* Simulated I2C device memory (for demo) */
static unsigned char device_memory[256] = {
0x48, 0x00, 0x19, 0x00, /* Temperature sensor regs */
0xFF, 0x80, 0x64, 0x32, /* Config regs */
0, 0, 0, 0, 0, 0, 0, 0 /* Data regs */
};
const char* i2c_lib_version(void) {
return "libi2c version 1.0.0 (shared)";
}
int i2c_init(int bus_num, int freq_hz) {
if (bus_num < 0 || bus_num >= MAX_BUS) {
fprintf(stderr, "[I2C] Invalid bus number: %d\n", bus_num);
return I2C_ERR_BUS;
}
bus_initialized[bus_num] = 1;
bus_freq[bus_num] = freq_hz;
printf("[I2C] Bus %d initialized at %d Hz\n", bus_num, freq_hz);
return I2C_SUCCESS;
}
int i2c_write(int bus_num, unsigned char addr,
const unsigned char *data, int len) {
if (!bus_initialized[bus_num]) {
fprintf(stderr, "[I2C] Bus %d not initialized\n", bus_num);
return I2C_ERR_BUS;
}
printf("[I2C] Write to device 0x%02X: ", addr);
for (int i = 0; i < len; i++) {
printf("0x%02X ", data[i]);
/* Simulate writing to device memory */
if (addr < 256) {
device_memory[addr + i] = data[i];
}
}
printf("... ACK\n");
return I2C_SUCCESS;
}
int i2c_read(int bus_num, unsigned char addr,
unsigned char *buf, int len) {
if (!bus_initialized[bus_num]) {
fprintf(stderr, "[I2C] Bus %d not initialized\n", bus_num);
return I2C_ERR_BUS;
}
/* Simulate reading from device memory */
for (int i = 0; i < len; i++) {
buf[i] = (addr + i < 256) ? device_memory[addr + i] : 0xFF;
}
printf("[I2C] Read from device 0x%02X: ", addr);
for (int i = 0; i < len; i++) {
printf("0x%02X ", buf[i]);
}
printf("\n");
return I2C_SUCCESS;
}
void i2c_close(int bus_num) {
bus_initialized[bus_num] = 0;
bus_freq[bus_num] = 0;
printf("[I2C] Bus %d closed\n", bus_num);
}
/* main_i2c_app.c - Application using the I2C shared library */
#include <stdio.h>
#include "i2c_lib.h"
int main(void) {
printf("=== I2C Application ===\n");
printf("Using: %s\n\n", i2c_lib_version());
/* Initialize I2C bus 0 at 400 kHz (fast mode) */
int ret = i2c_init(0, 400000);
if (ret != I2C_SUCCESS) {
fprintf(stderr, "Failed to init I2C bus!\n");
return 1;
}
/* Simulate writing config to a temperature sensor at address 0x48 */
unsigned char config[] = {0x01, 0x60}; /* Pointer reg, config val */
ret = i2c_write(0, 0x48, config, 2);
/* Read temperature register (16-bit value) */
unsigned char temp_buf[2];
ret = i2c_read(0, 0x00, temp_buf, 2);
/* Convert raw value to Celsius (simplified) */
int raw = (temp_buf[0] << 4) | (temp_buf[1] >> 4);
float temp_c = raw * 0.0625f;
printf("\nTemperature: %.2f degrees C\n", temp_c);
i2c_close(0);
return 0;
}
## ============================================
## BUILD THE I2C SHARED LIBRARY
## ============================================
## Step 1: Compile with -fPIC (REQUIRED for shared lib)
$ gcc -g -fPIC -Wall -c i2c_lib.c -o i2c_lib.o
## Verify PIC was used — check for GOT references:
$ objdump -d i2c_lib.o | grep -i "got\|plt" | head -5
## Step 2: Create the shared library with proper soname
$ gcc -g -shared \
-Wl,-soname,libi2c.so.1 \
-o libi2c.so.1.0.0 \
i2c_lib.o
## Verify the soname is embedded correctly:
$ readelf -d libi2c.so.1.0.0 | grep SONAME
## Output: (SONAME) Library soname: [libi2c.so.1]
## Step 3: Create symlinks
$ ln -sf libi2c.so.1.0.0 libi2c.so.1
$ ln -sf libi2c.so.1 libi2c.so
$ ls -la libi2c*
## Step 4: Compile the app
$ gcc -g -Wall -o i2c_app main_i2c_app.c -L. -li2c
## Step 5: Check what libs the app needs:
$ ldd i2c_app
## libi2c.so.1 => ./libi2c.so.1 (0x...)
## libc.so.6 => /lib/...
## Step 6: Run the app
$ LD_LIBRARY_PATH=. ./i2c_app
Coding Example 2 — PIC vs Non-PIC: What Goes Wrong?
This example shows what happens when you forget -fPIC and try to build a shared library. It also demonstrates using objdump to spot the difference.
/* test_pic.c - A simple module to test PIC compilation */
#include <stdio.h>
static int counter = 0; /* global variable */
void increment(void) {
counter++;
printf("Counter = %d\n", counter);
}
int get_counter(void) {
return counter;
}
## ============================================
## COMPARE: PIC vs non-PIC compilation
## ============================================
## Compile WITHOUT -fPIC (for static or direct use)
$ gcc -g -c test_pic.c -o test_pic_nopic.o
## Compile WITH -fPIC (required for shared library)
$ gcc -g -fPIC -c test_pic.c -o test_pic_pic.o
## Compare the disassembly — look for @PLT and @GOT references
## Non-PIC: uses absolute addresses (BAD for shared libs on x86-64)
$ objdump -d test_pic_nopic.o | grep -A3 "counter\|printf"
## PIC: uses RIP-relative addressing and @PLT stubs (GOOD)
$ objdump -d test_pic_pic.o | grep -A3 "counter\|printf"
## Try to build a shared lib WITHOUT -fPIC (will warn or fail):
$ gcc -shared -o test_nopic.so test_pic_nopic.o
## On modern x86-64, this may succeed but produce a warning or
## behave incorrectly. On x86-32, this WILL fail with relocation errors.
## Build correctly WITH -fPIC:
$ gcc -shared -Wl,-soname,libtest.so.1 \
-o libtest.so.1.0.0 test_pic_pic.o
## No warnings — this is correct!
## ============================================
## INSPECT THE FINAL .so FILE
## ============================================
## See all exported symbols (functions programs can call):
$ nm -D libtest.so.1.0.0
## Output shows: increment, get_counter (T = exported text symbols)
## See the ELF header info:
$ readelf -h libtest.so.1.0.0 | grep "Type\|Entry"
## Type: DYN (Shared object file) — NOT EXEC!
## Check for GOT and PLT sections (present when -fPIC was used):
$ readelf -S libtest.so.1.0.0 | grep -E "GOT|PLT|DYNAMIC"
## See the full dynamic section (soname, needed libs, etc.):
$ readelf -d libtest.so.1.0.0
## Compare file sizes:
$ ls -lh test_pic_nopic.o test_pic_pic.o
## PIC object is slightly larger due to GOT/PLT indirection code
-fPIC for shared libraries:On 32-bit x86, forgetting
-fPIC will cause a hard linker error because the relocations cannot be resolved for shared use. On 64-bit x86 (x86-64), the linker may accept it (because x86-64 uses RIP-relative addressing by default) but the resulting .so file may use text relocations — these prevent memory sharing between processes and produce a security warning from the kernel (text segments with writable relocations are marked non-executable on SELinux/PaX systems).-fPIC is for shared libraries. -fPIE (Position-Independent Executable) is for executables compiled with ASLR support. Modern Linux systems compile programs with -fPIE by default for security. They generate similar code but serve different purposes: -fPIC allows multiple loads of the same library; -fPIE allows the kernel to randomize the executable’s base address (ASLR).Interview Questions — Section 41.4
-Wl,-soname option?-Wl,-soname,libfoo.so.1 is a linker option (passed through gcc’s -Wl to ld) that embeds the soname string into the ELF dynamic section of the shared library. When a program is linked against the library, the linker records this soname in the program’s ELF “NEEDED” entry (not the actual filename). At run time, the dynamic linker resolves this soname to a symlink, which points to the actual .so file. This is how version management works: updating the symlink allows transparently updating the library.printf), it calls the PLT entry for that function. The first time it’s called, the PLT stub invokes the dynamic linker to resolve the actual address and stores it in the GOT. Subsequent calls go through the GOT directly without involving the dynamic linker. This “lazy” approach speeds up program startup because not all external functions need to be resolved immediately.readelf -d libname.so | grep SONAME to display the soname stored in the library’s ELF dynamic section. Alternatively, objdump -p libname.so | grep SONAME also works. For example:
$ readelf -d libembedutils.so.1.0.0 | grep SONAME 0x0000000e (SONAME) Library soname: [libembedutils.so.1]
