Understanding compiler and linker flags

When building a program in C, C++, or other compiled languages, one typically invokes a compiler or compiler driver (like g++ or clang) with a list of command-line options. These options (often referred to as flags) tell the compiler how to compile (and sometimes link) your code.

I stumbled upon many of these compiling/linking commands through my software engineering career, and this blogpost kind of reflects my current understanding of these concepts, from both a theoretical and practical viewpoints.

In this post, we’ll cover:

1. What Are “Flags”?

Compiler flags control different aspects of the compilation process.

Typical flags include:

These flags provide precise control over how your code is parsed, optimized, and linked.

2. How Do Flags Relate to Compilers?

2.1 Compilation vs. Linking

Compilation:

Linking:

Examples of Flag Usage:

Note on Linker Flag Order:

The order of libraries in the linker command can sometimes matter, particularly with static libraries. If unresolved symbols appear, adjusting the order might resolve them.

2.2 Example of a Typical g++ Command

Compilation:

g++ -I/usr/local/include -O3 -std=c++11 -c myfile.cpp -o myfile.o

Linking:

g++ myfile.o -L/usr/local/lib -lm -o myapp

You can also combine these steps:

g++ -I/usr/local/include -O3 -std=c++11 myfile.cpp -L/usr/local/lib -lm -o myapp

Under the hood, the compiler first compiles, then links, all in one invocation.

3. What Are Linkers?

A linker is a program that resolves references between object files and external libraries. For example, if your code calls sqrt() from the math library, the linker locates where sqrt is defined (in libm.so on Linux, .so for shared objects) and links it into the final executable.

When you pass flags like -L/usr/local/lib -lm, you’re telling the linker to:

4. Why Use L and l?

Example:

g++ main.o -L/home/me/mylibs -lfoo -o main

The linker checks /home/me/mylibs for libfoo.so (shared) or libfoo.a (static) and includes it in the final executable.

Static vs. Shared Libraries:

While the discussion above focuses on shared libraries (using flags like -shared and -fPIC), static libraries are linked differently. For static libraries, the linker includes the library code directly into the executable, and the flags used (-l<name>) point to .a files instead of .so or .dylib.

5. How Does a Compiler Work Internally?

Here’s a very simplified overview of the compilation process:

  1. Preprocessing
    • Processes directives like #include and #define (for code in C/C++ for instance), expanding macros and including header files.
  2. Parsing & Semantic Analysis
    • Converts the source code into an internal representation (often an Abstract Syntax Tree) and checks for errors (e.g. type mismatches).
  3. Optimization
    • Applies optimizations based on flags such as O2 or O3.
  4. Code Generation
    • Translates the optimized code into machine instructions (Assembly code).
  5. Assembly & Object File Creation
    • The assembly is transformed into an object file (.o or .obj) containing machine code and unresolved symbols.
  6. Linking
    • The linker merges multiple object files and libraries to produce the final executable or library.

6. Code Snippets of Typical Compiler/Linker Commands

6.1 C++ on Linux with a Shared Library

Compile:

g++ -fPIC -Iinclude -O3 -std=c++17 -c mylibrary.cpp -o mylibrary.o

Link:

g++ -shared -o libmylibrary.so mylibrary.o

Using the Shared Library:

g++ main.cpp -Iinclude -L. -lmylibrary -o main

6.2 Using OpenMP

g++ -fopenmp -O3 main.cpp -o main

6.3 Example with Clang on macOS

clang++ -std=c++14 -stdlib=libc++ -I/usr/local/include -L/usr/local/lib main.cpp -o myapp -lc++

7. CUDA Example

When compiling CUDA code, you typically use nvcc, NVIDIA’s compiler driver. Under the hood, nvcc calls the host compiler (like gcc or clang) for CPU code.

Basic CUDA Compilation:

nvcc -I/path/to/cuda/include -c mykernel.cu -o mykernel.o

More Complex Example:

nvcc -arch=sm_75 \\
     -I/usr/local/cuda/include \\
     -I/usr/local/include \\
     -L/usr/local/cuda/lib64 \\
     -lcudart \\
     -Xcompiler -fPIC \\
     -O3 -o mycudaapp main.cu

8. Putting It All Together

9. Summary

Understanding compiler and linker flags is crucial for building functional and optimized software. Here’s what you need to remember:

By carefully controlling these flags, you can ensure your project is both robust and efficient.