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.
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.
- A program calls
scale(5). - The operating system may have loaded the shared library containing
scaleat a memory address chosen at runtime. - The generated code locates
multiplierwithout relying on one hard-coded library base address. - It reads
multiplier, whose value is3. - It calculates
5 * 3. - It returns
15.
For an external function call, the idea is similar:
#include <stdio.h>
void report(void) {
puts("Finished");
}
reportexecutes.- It needs the runtime address of
putsfrom the C library. - PIC-compatible linkage uses dynamic-linking structures such as the PLT and GOT when appropriate.
- The program calls the resolved
putsimplementation.
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
.solibraries on Linux, such aslibsqlite3.soor 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.
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
-fPICto the compile flags for source files that will become part of a shared library. - Use
-sharedonly at the final link step that produces a.sofile. - Compile normal application-only object files without
-fPICunless 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
| Item | Purpose | Common use |
|---|---|---|
-fPIC | Generate fully position-independent code for shared objects | Compile source files that will go into a .so library |
-fpic | Generate PIC, sometimes with architecture-specific limitations | Smaller or older architectures where GCC distinguishes it from -fPIC |
-shared | Produce a shared library at link time | Create libname.so from object files |
-fPIE | Generate code intended for a position-independent executable | Compile an executable that will be linked as PIE |
-pie | Link a position-independent executable | Final executable link step |
| No PIC option | Generate ordinary code according to compiler defaults | Simple 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.
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
-fPICwhen compiling code for a shared library (.so). - Use
-sharedwhen linking the final shared library. - Compile again with
-fPICif the linker says an object contains an invalid relocation for a shared object. - Use
-fPIEplus-piefor 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.
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.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.