2.1 `gcc` Compiler
1. An in-depth look at GCC Compiler
Let us have a closer look at what happens with the following compilation statement
gcc -Wall -ansi -pedantic util.c main.c -o prog
During a typical generation of an executable file, the following takes place.
-
C Code -> Preprocessed C Code // Preprocessor does this
-
Preprocessed C Code -> Assembly Code (not machine code, but hardware dependant barely human-readable code) // Compiler does this
-
Assembly Code -> Object File (machine code) // Assembler does this
-
Object File -> Executable File (machine code) // Linker
Detail Explanation of the Steps above
In Step No1:
The preprocessor will resolve all preprocessor directives (#include, #define, #ifdef - #endif, #ifndef - #endif, etc.)
gcc
-Emain.c
// will output the preprocessed C code to the console. Feel free to have a look and see what a preprocessed C code looks like.
In Step No2:
The compiler will see the preprocessed C code and compiles it to the assembly code.
- Preprocessed C code will not have any preprocessor directives (
#include
,#define
,#ifdef
-#endif
,#ifndef
-#endif
, statements proceeded by #) since they are already processed at step No 1. - The initial compilation that happens at step No. 2 is completely unaware of any preprocessor directives. In fact, it will fail if it sees any.
gcc
-Smain.c
// will output the assembly code. Feel free to have a look at the assembly code which is barely human-readable.
In Step No3:
The assembler will generate the object file for assembly code.
gcc
-cmain.c
// will output the object file
In Step No4:
The linker will link all object files to generate the executable file.
gcc main.o
-oprog
// will output the executable file
1.1 GCC Compiler Flags
You may find the following flags for gcc
are useful in understanding what’s going on under the hood during the compilation process.
gcc -E main.c // stops after preprecessing (console output)gcc -S main.c // stops after initial compilation (Assembly file)gcc -c main.c // stops after "compiling" but no linking (object file test.o)gcc main.c // stops after linking (executable file a.out or a.exe)gcc main.c -o prog // stops after linking (executable file is prog)
gcc -Wall -ansi -pedantic util.c main.c -o prog // stops after linking. Compilation happens with the flags (executable file is prog) - Recommended in UCP Unit (Compiling + Linking)gcc -Wall -ansi -pedantic util.c main.c -o prog -D DEBUG=1 // Preprocessor name DEBUG can be defined on the compiler command-line as wellgcc -g - Wall -ansi -pedantic util.c main.c -o prog // Compile the information required for valgrind detailed error messages
1.2 What are Compiler and Linker Errors?
These are the errors that could occur during compilation. But not at runtime. Errors could be categorized as follows:
- Compile-time Errors/Warnings (includes Linker Errors) - happens at compile time
- Runtime Errors (runtime-errors but exceptions)
- Logical Errors (runtime-errors, but wrong logic)
Please refer to: for mode details.
It is important to distinguish compiler errors/warnings (syntax errors) from linker errors. Compiler errors (syntax errors) will be identified during the compilation stage (step no2 in gcc compiler), while linker errors will be identified in step no4 linking stage.
1.2.1 Compilation Errors
gcc can provide extra details on compilation errors such as filename, line no, column no as well as some description about the error. This is because the compilation process (step 02 in gcc compiler) takes the .c files as the input. compilation errors refer to the errors before linking happens.
During compilation, the function definitions will not be looked into by the compiler. The compilation process is happy to go ahead with just the function declarations. This is the reason why the following code could be compiled successfully without any errors popping up for not being able to find function definitions.
int add(int a, int b);
int main() { print("sum: %d\n", add(10, 20));}
# Compile the code using -c optiongcc -Wall -ansi -pedantic main.c -c
# No errors will be reported here for add() function.# During the linking process, linker will find the# body (function definition) of the `add()` function in other `.o` files.
1.2.2 Linker Errors
gcc can provide less details on linker errors since the linking stage (step04 in gcc compiler) takes the .o files (machine code) as the input. This is where you see weird looking errors.
Remember linker is responsible to combine .o files together to produce the executable file and more importantly find the function definitions across all .o files. If not found, it will report an error.
1.3 Compiling Multiple .c
Files
gcc -Wall -ansi -pedantic -c main.c // Recommended (Compile only)gcc -Wall -ansi -pedantic -c util.cgcc main.o util.o -o prog // Recommended (For Linking)
- Linker will link all object files to generate the executable file.
- During this process, Linker will look for the function definitions (prototype + body) in all object files.
- Linker errors are not descriptive as compiler errors since linker works only with the object files and line no, column no information can not be provided in linker errors.
- In contrast, compilation errors refer to the errors before linking happens. They are descriptive (line number, column no, file name, etc can be provided) since it works with the .c files.
2. Preprocessor
The preprocessor in C is a tool that processes the source code before the actual compilation begins. It handles directives, which are commands starting with #
, such as #include
, #define
, and #ifdef
. The preprocessor modifies the code to make it ready for the compiler by performing tasks like file inclusion, macro expansion, and conditional compilation.
2.1 Preprocessor Directives
-
#include
: Includes the contents of a specified file (usually a header file) into the source file.#include <stdio.h> // Standard library file#include "myheader.h" // User-defined file -
#define
: Defines macros or constants, which are simple text substitutions.#define PI 3.14 // Replaces PI with 3.14 -
#undef
: Undefines a previously defined macro.#undef PI -
#ifdef
/#ifndef
/#endif
: Conditional compilation. It includes or excludes parts of the code depending on whether a macro is defined.#ifdef DEBUGprintf("Debug mode\n");#endif -
#pragma
: Provides special instructions to the compiler (implementation-specific).#pragma once // Prevents multiple inclusions of the same file
2.2 Role of the Preprocessor
- Text Substitution: Replaces macros with their values.
- File Inclusion: Adds the contents of header files to the source code.
- Conditional Compilation: Compiles code conditionally based on whether macros are defined.
The preprocessor prepares the source code for the compiler, making it easier to manage, reuse, and optimize code.
2.3 Macro
A macro in C is a fragment of code that is defined using the #define
preprocessor directive. Macros perform text substitution before the actual compilation starts. Macros are different from functions in several ways, especially in how they are handled by the preprocessor and compiler.
Example:
#define SQUARE(x) (x * x)
In this case, SQUARE(x)
is a macro that substitutes the expression x * x
wherever it appears in the code.
Differences Between Macros and Functions
-
Preprocessing vs Compilation:
- Macro: Handled by the preprocessor through simple text substitution before compilation.
- Function: Compiled into machine code during the compilation phase.
-
Speed:
- Macro: Can be faster since it’s a direct substitution and avoids the overhead of a function call (no stack operations).
- Function: Introduces overhead due to function calls, parameter passing, and return statements.
-
Type Checking:
- Macro: No type checking, as the preprocessor only does text substitution.
- Function: Enforces type checking, which ensures that the function receives and returns the correct data types.
-
Side Effects:
- Macro: Can cause unexpected behavior due to lack of evaluation order control.
int result = SQUARE(2 + 3); // Expands to (2 + 3 * 2 + 3) = 11, not 25
- Function: Evaluates the arguments first, preventing unintended side effects.
int result = square(2 + 3); // Correctly computes 25
- Macro: Can cause unexpected behavior due to lack of evaluation order control.
-
Code Size:
- Macro: Expands code at each occurrence, possibly increasing code size.
- Function: Code is defined once and reused, potentially saving space, especially in large projects.
-
Recursion:
- Macro: Cannot be recursive, as it’s just a substitution.
- Function: Can be recursive.
Function Example
int square(int x) { return x * x;}
3. Makefiles in C
A Makefile is a script used by the make
build automation tool. In C programming, it defines how to compile and link programs. The Makefile specifies rules for building targets (e.g., executable files) from source files (.c
, .h
). It helps automate the compilation process, reducing manual effort, especially in large projects.
Basic structure of a Makefile:
target: dependencies command to build the target
Example:
myprogram: main.o utils.o gcc -o myprogram main.o utils.o
main.o: main.c utils.h gcc -c main.c
utils.o: utils.c utils.h gcc -c utils.c
clean: rm *.o myprogram
In this example:
myprogram
is the target.main.o
andutils.o
are dependencies.- Commands (indented with tabs) are executed to build the target.
Common Makefile-Related Errors
-
Missing Dependencies: Failing to specify correct dependencies (e.g., forgetting a
.h
file).- Error:
file not found
or undefined references.
- Error:
-
Incorrect Commands: Using the wrong command or flag for compilation.
- Error:
gcc: error: unrecognized command line option
.
- Error:
-
Tab Error: Makefiles require commands to be indented with a tab, not spaces.
- Error:
missing separator
(indicating a tab issue).
- Error:
-
Out-of-Date Object Files: Modifying source files without recompiling related object files.
- Solution: Use
make clean
to remove old object files and force recompilation.
- Solution: Use
-
Cyclic Dependencies: Defining a circular dependency between targets.
- Error:
Circular dependency dropped
.
- Error:
-
File Naming Conflicts: Using targets or filenames that clash.
- Error:
recipe for target failed
.
- Error: