Toolchain and Compilation Pipeline

The C preprocessor handles #include, macros, and conditional compilation.

What this module covers

Preprocessing, compilation, assembly, and linking Compiler, assembler, linker, loader, and archiver

Object files, static libraries, shared libraries, and executables Include paths, macros, symbols, and ABI Debug and release configurations

Warning levels, optimization, and debug symbols Link order and unresolved symbols Cross-compilation and target triples Host tools versus target tools

Inspecting artifacts with platform tools

3.1 Preprocessor

The C preprocessor handles #include, macros, and conditional compilation.

Inspect preprocessed output:

Terminal session (GCC 11.2) on a main.c containing the macros above and printf("BUFFER_SIZE=%d LOG_ENABLED=%d\n", BUFFER_SIZE, LOG_ENABLED); — with and without the definition:

Include search paths and macro definitions affect output, so they are build inputs.

#define BUFFER_SIZE 128

#if defined(ENABLE_DIAGNOSTICS)
#define LOG_ENABLED 1
#else
#define LOG_ENABLED 0
#endif
cc -E -DENABLE_DIAGNOSTICS=1 main.c -o main.i
$ cc -E -DENABLE_DIAGNOSTICS=1 main.c -o main.i
$ grep -F 'printf("BUFFER' main.i
    printf("BUFFER_SIZE=%d LOG_ENABLED=%d\n", 128, 1);
$ cc -E main.c | grep -F 'printf("BUFFER'
    printf("BUFFER_SIZE=%d LOG_ENABLED=%d\n", 128, 0);

3.2 Compiler and assembler

The compiler translates source-language constructs into assembly or another intermediate representation, applies optimizations, and diagnoses errors. The assembler converts assembly into relocatable machine-code object files.

**Terminal session (GCC 11.2) on a sensor.c whose uint8_t sensor_scale(int raw) returns raw * 2 — a real -Wconversion warning:**

-c means compile/assemble without linking for common GCC/Clang-style drivers. Flags differ across compilers.

cc -Wall -Wextra -Wconversion -c sensor.c -o sensor.o
$ cc -Wall -Wextra -Wconversion -c sensor.c -o sensor.o
sensor.c: In function 'sensor_scale':
sensor.c:5:16: warning: conversion from 'int' to 'uint8_t' {aka 'unsigned char'} may change value [-Wconversion]
    5 |     return raw * 2;   /* int result narrowed to uint8_t */
      |            ~~~~^~~

3.3 Linker

The linker combines object files and libraries, resolves symbols, and creates an executable or library.

Terminal session (GCC 11.2), with a main.c that prints sensor_scale(100) and sensor_scale(200) — note what the warning predicted:

A missing-symbol error usually means a required definition was not linked, the symbol name or ABI differs, or a library/order/configuration is wrong.

cc main.o sensor.o -o firmware_host_test
$ cc -c main.c -o main.o
$ cc main.o sensor.o -o firmware_host_test
$ ./firmware_host_test
scaled: 200
scaled: 144

3.4 Static and shared libraries

Static library example on Unix-like systems:

Terminal session (GCC 11.2, binutils 2.38), with nm and a run added:

A static library is typically copied into the final linked image as needed. A shared library is loaded or linked according to the platform's dynamic-linking model.

Library naming, file extensions, and search rules vary by platform. Avoid assuming that a Unix -L/-l command works unchanged on Windows/MSVC.

Link order matters GNU ld scans archives once, in command-line order, pulling in only members that resolve symbols already known to be missing. Put the library after the objects that use it:

Terminal session (GCC 11.2 / GNU ld 2.38, Ubuntu 22.04 on WSL2):

ar rcs libsensor.a sensor.o
cc main.o -L. -lsensor -o app
$ ar rcs libsensor.a sensor.o
$ cc main.o -L. -lsensor -o app
$ nm libsensor.a

sensor.o:
0000000000000000 T sensor_scale
$ ./app
scaled: 200
scaled: 144
$ cc -L. -lsensor main.o -o app2
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0xe): undefined reference to `sensor_scale'
/usr/bin/ld: main.c:(.text+0x31): undefined reference to `sensor_scale'
collect2: error: ld returned 1 exit status
[exit status 1]
$ cc main.o -L. -lsensor -o app2 && ./app2
scaled: 200
scaled: 144

3.5 Debug and release configurations

Debug builds often prioritize debuggability and may include symbols and lower optimization. Release builds commonly enable stronger optimization and omit some debugging conveniences.

Terminal session (GCC 11.2, Ubuntu 22.04 on WSL2) on a small program with an assert:

Optimization can reveal undefined behavior or timing assumptions that do not appear in unoptimized builds. Do not treat a successful debug build as proof that the release build is correct.

Try it: Debug versus Release

cc -O0 -g -Wall -Wextra main.c -o app-debug
cc -O2 -DNDEBUG main.c -o app-release
$ cc -O0 -g -Wall -Wextra main.c -o app-debug
$ cc -O2 -DNDEBUG main.c -o app-release
$ ./app-debug && ./app-release
42
42
$ size app-debug app-release
   text	   data	    bss	    dec	    hex	filename
   1638	    608	      8	   2254	    8ce	app-debug
   1412	    600	      8	   2020	    7e4	app-release
$ file app-debug app-release | sed 's/, BuildID[^,]*//'
app-debug:   ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
app-release: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
$ for f in app-debug app-release; do echo "$f: $(readelf -S $f | grep -c " .debug_") .debug_* sections"; done
app-debug: 6 .debug_* sections
app-release: 0 .debug_* sections
buildconfig

3.6 Cross-compilation

Cross-compilation builds on one host for a different target architecture or operating environment. Distinguish:

Build machine: where the build executes.

Host machine: where a built tool is expected to run (terminology varies by ecosystem).

Target machine: where the produced program runs, especially for compiler toolchains.

A cross compiler might be named arm-none-eabi-gcc, but naming conventions are toolchain-specific. A cross build needs target headers, libraries, linker configuration, and correct architecture flags—not merely a different compiler executable.

3.7 Inspecting artifacts

Common tools include:

file app — identifies file type on supported systems.

nm app — lists symbols on supported object formats.

objdump -d app — disassembles supported binaries.

readelf -h app — inspects ELF headers.

size app — reports section-size information on supported platforms.

These are platform/toolchain-specific examples. Use the equivalents supplied by your toolchain.

Terminal session (binutils 2.38, Ubuntu 22.04 on WSL2) on the app built above from main.c, sensor.c and libsensor.a:

nm shows sensor_scale defined in the text section (T); objdump shows the doubling as a single add %eax,%eax; readelf confirms a 64-bit position-independent executable; size reports section totals.

$ file app | sed 's/, BuildID[^,]*//'
app: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
$ nm app | grep -i sensor
000000000000119e T sensor_scale
$ objdump -d --no-show-raw-insn app | grep -A8 '<sensor_scale>:'
000000000000119e <sensor_scale>:
    119e:	endbr64 
    11a2:	push   %rbp
    11a3:	mov    %rsp,%rbp
    11a6:	mov    %edi,-0x4(%rbp)
    11a9:	mov    -0x4(%rbp),%eax
    11ac:	add    %eax,%eax
    11ae:	pop    %rbp
    11af:	ret    
$ readelf -h app | head -11
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060
$ size app
   text	   data	    bss	    dec	    hex	filename
   14
…

Quiz

? The run of `cc -Wconversion` printed a warning for `return raw * 2;`. What did the program then print for `sensor_scale(200)`?
- 400 :: The result is narrowed to `uint8_t`.
+ 144 :: 400 does not fit in 8 bits; 400 mod 256 = 144 — exactly what the warning predicted.
- 0 :: Narrowing keeps the low 8 bits.
- A runtime error :: Unsigned narrowing is defined in C and silently wraps.

? `cc -L. -lsensor main.o -o app2` failed with “undefined reference to `sensor_scale`”. Why?
+ The archive was scanned before `main.o` created the need for `sensor_scale`. :: Put libraries after the objects that use them.
- `libsensor.a` was empty. :: `nm` showed the symbol inside it.
- `-L.` is invalid. :: It adds the current directory to the search path.
- The compiler needs `-c`. :: This is a link step.

? What does a cross-compiling build need besides a different compiler executable? (select all that apply)
+ Target headers and libraries (sysroot) :: Host headers describe the wrong system.
+ Correct architecture and ABI flags :: For example FPU and instruction-set options.
+ Linker configuration for the target :: Such as a linker script and startup objects.
- The host's `/usr/include` :: That is preci
…