Question

GCC -fPIC: Position-Independent Code in C

cppgccfpic

Question

I have read GCC's documentation about code-generation options but do not understand what “Generate position-independent code (PIC)” means. What does the -fPIC option do, and can you provide an example that demonstrates why it is needed?

Short Answer

By the end of this page, you will understand why machine code sometimes depends on a memory address, how position-independent code avoids that dependency, and why gcc -fPIC is commonly used when building shared libraries on Linux and other ELF-based systems.

Concept

Position-independent code (PIC) is machine code that can run correctly even if the operating system loads it at a different memory address each time.

A compiled program contains instructions that access functions, global variables, and constants. Non-PIC machine code may embed an address directly into an instruction. If the code is moved elsewhere in memory, that embedded address may no longer be correct.

PIC avoids depending on a fixed load address. It uses mechanisms such as:

  • PC-relative addressing: refer to nearby data relative to the current instruction address.
  • Global Offset Table (GOT): look up the runtime address of external or global objects through a table.
  • Procedure Linkage Table (PLT): make calls to functions whose final addresses may be resolved by the dynamic linker.

The operating system can therefore load the same shared library at a convenient address in each process. It can also share the library's read-only code pages between processes more easily.

In GCC, -fPIC requests PIC suitable for shared libraries. A typical build command is:

gcc -fPIC -shared -o libmathtools.so mathtools.c

-fPIC affects how the source file is compiled. -shared affects how object files are linked into a shared library. They are commonly used together, but they do different jobs.

On many 64-bit systems, some code is naturally easy to make position-independent because the CPU supports PC-relative instructions. However, -fPIC is still important for the compiler's handling of symbols, especially global data and symbols defined outside the current source file.

Mental Model

Imagine a coffee shop's recipe card says:

“The sugar is at building address 500 Main Street.”

That recipe works only while the shop stays at that exact address. If the shop moves, the instruction is wrong.

PIC uses directions like:

“The sugar is in the labeled cupboard beside this counter.”

or:

“Check the shop's location directory for the current sugar cupboard.”

The recipe can now move to any building and still work. In the same way, PIC lets compiled code move to a different memory address without rewriting instructions that contain fixed addresses.

Take Quiz

Syntax and Examples

Compile a C source file into a PIC object file with -fPIC:

gcc -fPIC -c greeting.c -o greeting.o

Build a shared library from that object file:

gcc -shared -o libgreeting.so greeting.o

Or do both stages in one command:

gcc -fPIC -shared -o libgreeting.so greeting.c

Example source file:

/* greeting.c */
#include <stdio.h>

int greeting_count = 0;

void greet(const char *name) {
    greeting_count++;
    printf("Hello, %s! (%d)\n", name, greeting_count);
}

This source accesses:

  • greeting_count, a global variable.
  • printf, a function provided by another library.

When compiled as PIC, GCC generates code that can find these symbols after libgreeting.so has been loaded at an arbitrary address. The exact assembly differs by CPU architecture, compiler version, optimization level, and symbol visibility, but the central idea is the same: do not assume a single fixed load address.

A small program can use the library:

/* main.c */
void greet(const char *name);

int main(void) {
    greet("Ada");
    return 0;
}

Build and run it on a typical Linux system:

gcc -fPIC -shared -o libgreeting.so greeting.c
gcc -o app main.c -L. -lgreeting
LD_LIBRARY_PATH=. ./app

Expected output:

Hello, Ada! (1)

Step by Step Execution

Consider this library code:

int multiplier = 3;

int scale(int value) {
    return value * multiplier;
}

When scale(5) runs, the machine code must obtain the current value of multiplier.

  1. A program calls scale(5).
  2. The operating system may have loaded the shared library containing scale at a memory address chosen at runtime.
  3. The generated code locates multiplier without relying on one hard-coded library base address.
  4. It reads multiplier, whose value is 3.
  5. It calculates 5 * 3.
  6. It returns 15.

For an external function call, the idea is similar:

#include <stdio.h>

void report(void) {
    puts("Finished");
}
  1. report executes.
  2. It needs the runtime address of puts from the C library.
  3. PIC-compatible linkage uses dynamic-linking structures such as the PLT and GOT when appropriate.
  4. The program calls the resolved puts implementation.

The key point is not that every address lookup happens in exactly the same way. The exact instructions are platform-specific. The key point is that the code remains valid when its containing shared object is loaded somewhere else.

Real World Use Cases

PIC is most often used for these situations:

  • Shared libraries: Build .so libraries on Linux, such as libsqlite3.so or an application's plugin library.
  • Plugins: A host application can load plugin shared objects at runtime with APIs such as dlopen.
  • Language extension modules: Native modules for Python, Ruby, PHP, or similar runtimes are often shared libraries.
  • System libraries used by many processes: PIC allows the operating system to map a library's code into many processes without needing private modifications to its code pages.
  • Address Space Layout Randomization (ASLR): Modern operating systems often vary load addresses as a security measure. PIC supports code that can operate at those varying addresses.

For example, a reusable C library might be built like this:

gcc -O2 -fPIC -c parser.c -o parser.o
gcc -shared -o libparser.so parser.o

Applications can then link against libparser.so instead of copying the library implementation into every executable.

Take Quiz

Real Codebase Usage

A common project layout compiles library sources and application sources differently:

CFLAGS = -Wall -Wextra -O2

libwidget.so: widget.o
	gcc -shared -o $@ $^

widget.o: widget.c widget.h
	gcc $(CFLAGS) -fPIC -c widget.c -o $@

app: main.o libwidget.so
	gcc -o $@ main.o -L. -lwidget

Typical practices include:

  • Add -fPIC to the compile flags for source files that will become part of a shared library.
  • Use -shared only at the final link step that produces a .so file.
  • Compile normal application-only object files without -fPIC unless the project has a reason to use it everywhere.
  • Use symbol visibility to reduce a library's public interface and potentially improve optimization:
__attribute__((visibility("default")))
int public_api(int value) {
    return value + 1;
}
  • Keep global mutable state limited. PIC can access global variables correctly, but global state can still create design, testing, and thread-safety problems.

Build systems usually expose this as a property rather than requiring manual flags. For example, CMake can enable PIC for a target:

set_target_properties(widget PROPERTIES POSITION_INDEPENDENT_CODE ON)

The final flags remain platform and toolchain dependent, so inspect generated build commands if a shared-library link fails.

Common Mistakes

Using -shared without compiling PIC objects

A common attempt is:

gcc -c library.c -o library.o
gcc -shared -o liblibrary.so library.o

This may work on some platforms or for some source code, but it can fail when the linker finds relocations that are not valid for a shared object. A common Linux x86-64 error resembles:

relocation ... can not be used when making a shared object; recompile with -fPIC

Compile the library object with -fPIC instead:

gcc -fPIC -c library.c -o library.o
gcc -shared -o liblibrary.so library.o

Thinking -fPIC and -shared are the same option

They have separate responsibilities:

  • -fPIC: generate position-independent machine code while compiling.
  • -shared: link a shared library.

Usually a shared library needs both.

Adding -fPIC only while linking

This does not repair object files already compiled without PIC:

# Usually too late for library.o
gcc -fPIC -shared -o liblibrary.so library.o

Recompile the source files with -fPIC.

Assuming all executable code needs -fPIC

Traditional executables and shared libraries have different requirements. On many modern Linux distributions, executables are often built as PIE (position-independent executables) using flags such as -fPIE and -pie. That is related to PIC, but it is not the same build target.

Hard-coding shared-library search paths during development

This works for a quick local test:

LD_LIBRARY_PATH=. ./app

But deployment usually uses installation paths, an rpath policy, or system linker configuration instead. Do not rely on LD_LIBRARY_PATH as the only production strategy.

Comparisons

ItemPurposeCommon use
-fPICGenerate fully position-independent code for shared objectsCompile source files that will go into a .so library
-fpicGenerate PIC, sometimes with architecture-specific limitationsSmaller or older architectures where GCC distinguishes it from -fPIC
-sharedProduce a shared library at link timeCreate libname.so from object files
-fPIEGenerate code intended for a position-independent executableCompile an executable that will be linked as PIE
-pieLink a position-independent executableFinal executable link step
No PIC optionGenerate ordinary code according to compiler defaultsSimple object files or traditional non-PIE executables

-fpic versus -fPIC is architecture-dependent. On some systems they produce equivalent results. On others, -fpic may use smaller addressing structures with size limits, while -fPIC uses a more general form. For portable shared-library build settings, -fPIC is the usual choice.

PIC versus PIE:

  • PIC is commonly associated with shared libraries.
  • PIE is position-independent code and linking intended for an executable.
  • Both support loading at varying addresses, but GCC provides distinct options because their requirements and optimizations can differ.
Take Quiz

Cheat Sheet

# Compile a source file for use in a shared library
gcc -fPIC -c library.c -o library.o

# Link PIC object files into a shared library
gcc -shared -o liblibrary.so library.o

# One-command version
gcc -fPIC -shared -o liblibrary.so library.c

# Inspect dynamic dependencies of an ELF binary on Linux
ldd ./app
  • PIC means position-independent code.
  • PIC can execute correctly after being loaded at different memory addresses.
  • Use -fPIC when compiling code for a shared library (.so).
  • Use -shared when linking the final shared library.
  • Compile again with -fPIC if the linker says an object contains an invalid relocation for a shared object.
  • Use -fPIE plus -pie for position-independent executables when your toolchain or build policy requires it.
  • The exact generated assembly and relocation types depend on the CPU architecture and toolchain.

FAQ

What does -fPIC mean in GCC?

It means “generate position-independent code.” GCC emits machine code that does not require the code to be loaded at one fixed memory address.

Do I need -fPIC to build a shared library?

Usually yes, especially on 64-bit Linux and many other ELF platforms. Compile each source file for the library with -fPIC, then link with -shared.

What is the difference between -fPIC and -shared?

-fPIC changes code generation during compilation. -shared tells the linker to produce a shared library. They are normally used together for .so files.

Can I use -fPIC for an executable?

You can compile code with it, but executables intended to be position-independent are more commonly built with -fPIE and linked with -pie. Follow your platform's toolchain conventions.

Is PIC slower?

PIC can add an indirection for some global or external symbol accesses. On modern systems the cost is often small, but the exact performance effect depends on the architecture, compiler, optimization settings, and code.

Why does the linker say “recompile with -fPIC”?

One or more object files were compiled with relocations that cannot be placed in the shared library being built. Recompile the relevant source files with -fPIC.

Does -fPIC make my code secure?

It is not a security feature by itself. It enables relocatable code and supports mechanisms such as ASLR, which is one part of a broader security strategy.

Take Quiz

Mini Project

Description

Build a small shared C library that stores a global counter and exports functions for updating and reading it. This demonstrates a realistic reason for -fPIC: the library accesses global data while being loaded dynamically by an application.

Goal

Create libcounter.so with PIC and link an application that calls its exported functions.

Requirements

Create a counter.c file that defines a global counter and two functions. Compile the library code with -fPIC. Link the object file into a shared library named libcounter.so. Create a main.c program that calls the library functions. Build and run the application using the local shared library.

Take Quiz

Keep learning

Related questions

View more