Command-line flags to see the code at each stage of compilation

An illustrated tutorial on how to see the code at each stage

Command-line flags to see the code at each stage of compilation

An illustrated tutorial on how to see the code at each stage

The Compilation Process in C/C++

Knowing how your code compiles can be very helpful when writing and debugging it.

Compiling a C/C++ program is a multi-stage process.

The complete compilation process in C/C++ can be split into four separate stages:

  1. Preprocessing
  2. Compilation
  3. Assembly
  4. Linking

Entire Compilation Process

By using appropriate compiler options, we can stop this process at any stage. These options are passed as command line parameters.

In the following examples, I will be using the C++ programming language to write the code and g++ compiler to compile it. However, these commands can be easily implemented with a program written in the C programming language and compiled in gcc compiler.

As an example, the code below is apt for understanding this.

/*
Name : div.cc

A C++ Program to check divisibility by 2 */

#include<iostream>

using namespace std;

int main()
{
    int x;

    cin >> x;

    if (x % 2 == 0)
    {
        cout << "Divisible";
    }

    else
    {
        cout << "Not Divisible";
    }
}

It is a simple program that asks the user for an input and expects an integer (int) type constant. It prints Divisible on the screen if the number is divisible by 2 and Not Divisible otherwise.

Preprocessing

The C++ preprocessor copies the contents of the included header files (in our case - iostream.h) into the source code file by using preprocessor directive (#include), expands macros, and replaces constants defined using #define with their values. It also removes comments from the main code.

A new source file is created that contains code from our program as well as the header files and this is what is used as input to the further stages for the compilation to be declared complete.

We can stop at this stage and see the output by using “-E” option in gcc or g++.

In g++: g++ -E div.cc

In gcc: gcc -E div.c

Output to a file using “>” or “-o” options.

  • g++ -E div.cc > div
  • g++ -E div.cc -o div

The above commands will store the code in .FILE extension.

The GNU standard for the preprocessed files is .i or .ii extensions. It signifies that the code inside the file is not to be preprocessed and is to be sent directly for compilation.

Preprocessed Code

Our 21 line program is now 16,014 lines after preprocessing and all of that is just the header file inclusion (iostream.h) till line 15,996!

You can see the complete preprocessed file, div.ii - here.

Compilation

The preprocessed code is compiled into the assembly language for the current machine. It is in the form of assembly language that is machine dependent. The file extension is .s.

We can stop at this stage and see the output by using “-S” option in gcc or g++.

In g++: g++ -S div.cc

In gcc: gcc -S div.c -o div.s

Assembly Code

You can see the complete compiled file, div.s - here.

Assembly

During this stage, the assembler code generated by the compiler is assembled into the object code for the platform by the assembler. It contains equivalent binary code for our program. It generates an object file with a .o extension.

We can stop at this stage and see the output by using “-c” option in gcc or g++.

In g++: g++ -c div.cc

In gcc: gcc -c div.c -o div.o

NOTE: Notice the use of lowercase (.c) instead of uppercase (.C) which is used for linking.

Machine Binary Code

To download the object file, div.o - click here.

Linking

In this stage, all the linking of function calls with their definitions are done. Linker identifies where all these functions are implemented with the help of the assembly code. Linker fills in the addresses with the actual definitions. The linker also does a few additional tasks such as combining our program with some standard routines that are needed to make our program executable.

It generates an executable file with a .exe extension.

The final executable size is way more than the input file.

If no option is specified, then all stages of the compilation process are performed and an executable file is created and placed in the current directory.

In g++: g++ div.cc

In gcc: gcc div.c

These create an executable with the default name as a.exe or a.out on linux. To name it something else: g++ -o div div.cc

This will create a div.exe file in the current directory. We can now run our program from the output executable.

exe

Shown above is our program executable run from the terminal by just typing its name.

Exploring Further

Produce all the intermediate files using “-save-temps” option.

The -save-temps option can generate all files from the four stages of the compilation process. Through this option, output at all the stages of compilation is stored in the current directory.

In g++: g++ -save-temps div.cc

In gcc: gcc -save-temps div.c

For example :

$ g++ -save-temps div.cc

$ ls
a.out  div.cc  div.ii  div.o  div.s

See that all the intermediate files, as well as the final executable, was also produced in the output.

Verbose Mode

You can see the detailed compilation process by using the verbose “-v” option.

In g++: g++ -v div.cc

In gcc: gcc -v -o div.exe div.c

To see for any intermediate stage: g++ -v -S div.cc

The details will be shown within the terminal itself as shown in the image below.

Verbose Mode

You can open the image above or save it, and zoom in to see the details.

References