Topics:
- Compilation process
- Make for building software
-
Although you can go from source code to an executable in one command, the process is actually made up of 4 steps
-
Preprocessing
-
Compilation
-
Assembly
-
Linking
-
-
g++
andclang++
(andgcc
orclang
for C code) are driver programs that invoke the appropriate tools to perform these steps -
This is a high level overview. The compilation process also includes optimization phases during compilation and linking.
We can inspect the compilation process in more detail with the -v
compiler
argument. -v
typically stands for "verbose".
!run g++ -v -Wall -Wextra -Wconversion src/hello1.cpp -o src/hello1 !end
GNU compiler flags:
-E
: preprocess-S
: compile-c
: assemble
!run cat src/hello1.cpp g++ -E -o src/hello1.i src/hello1.cpp g++ -S -o src/hello1.s src/hello1.i g++ -c -o src/hello1.o src/hello1.s g++ -o src/hello1 src/hello1.o ./src/hello1 !end
-
The preprocessor handles the lines that start with
#
#include
#define
#if
- etc.
-
You can invoke the preprocessor with the
cpp
command
From src/hello1.i
:
# 1 "hello1.cpp"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 30 "/usr/include/stdc-predef.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/predefs.h" 1 3 4
# 31 "/usr/include/stdc-predef.h" 2 3 4
# 1 "<command-line>" 2
# 1 "hello1.cpp"
# 1 "/usr/include/c++/4.8/iostream" 1 3
# 36 "/usr/include/c++/4.8/iostream" 3
// approximately 17,500 lines omitted!
int main()
{
std::cout << "Hello" << std::endl;
return 0;
}
-
Compilation is the process of translating source code to assembly commands
-
The assembly commands are still human readable text (if the human knows assembly)
From src/hello.s
:
main:
.LFB1020:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %esi
movl $_ZSt4cout, %edi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
movq %rax, %rdi
call _ZNSolsEPFRSoS_E
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
-
This step translates the text representation of the assembly instructions into the binary machine code in a
.o
file -
.o
files are called object files -
Linux uses the Executable and Linkable Format (ELF) for these files
-
If you try to look at these files with a normal text editor you will just see garbage, intermixed with a few strings
-
Sometimes it is helpful to inspect object files with the
nm
command to see what symbols are defined:
!run nm ./src/hello1.o !end
-
Linking is the process of building the final executable by combining (linking) the
.o
file(s), and possibly library files as well -
The linker makes sure all of the required functions are present
-
If for example
foo.o
contains a call to a function calledbar()
, there has to be another.o
file or library file that provides the implementation of thebar()
function
!include src/foobar.hpp c++
!include src/foo.cpp c++
!include src/bar.cpp c++
!include src/main.cpp c++
Inspect the files:
!run ls src !end
Compile and assemble source files, but don't link:
!run g++ -c src/foo.cpp -o src/foo.o g++ -c src/bar.cpp -o src/bar.o g++ -c src/main.cpp -o src/main.o !end
Let's inspect the output:
!run ls src/*.o !end
What symbols are present in the object files?
!run nm src/foo.o nm src/bar.o nm src/main.o !end
What happens if we try to link main.o
into an executable with out pointing to
the other object files?
!run g++ src/main.o -o src/main !end
Ahhh, linker errors! Let's do it right:
!run g++ src/main.o src/foo.o src/bar.o -o src/main ./src/main !end
-
Libraries are really just a file that contain one or more
.o
files -
On Linux these files typically have a
.a
(static library) or.so
(dynamic library) extension -
.so
files are analogous to.dll
files on Windows -
.dylib
files on Mac OS X and iOS are also very similar to.so
files -
Static libraries are factored into the executable at link time in the compilation process.
-
Shared (dynamic) libraries are loaded up at run time.
From src/hw6.cpp
:
// code omitted
#include <jpeglib.h>
#include "hw6.hpp"
void ReadGrayscaleJPEG(std::string filename, boost::multi_array<unsigned char,2> &img)
{
/* Open the file, read the header, and allocate memory */
FILE *f = fopen(filename.c_str(), "rb");
if (not f)
{
std::stringstream s;
s << __func__ << ": Failed to open file " << filename;
throw std::runtime_error(s.str());
}
// code omitted
}
// code omitted
#ifdef DEBUG
int main()
{
boost::multi_array<unsigned char,2> img;
ReadGrayscaleJPEG("stanford.jpg", img);
WriteGrayscaleJPEG("test.jpg", img);
return 0;
}
#endif /* DEBUG */
Let's try to compile:
!run g++ -std=c++11 -Wall -Wextra -Wconversion src/hw6.cpp -o src/hw6 !end
That did not work. The linker looks for the main
symbol when trying to build
and executable. This linker also cannot find all of the symbols from the JPEG
library.
Let's find the jpeglib.h
header file:
!run locate jpeglib.h !end
Let's find libjpeg
:
!run locate libjpeg !end
Note that the library files may be in a different location on your system.
Now let's compile:
!run g++ -std=c++11 -Wall -Wextra -Wconversion src/hw6.cpp -o src/hw6 -DDEBUG -I/usr/local/include -L/usr/local/lib -ljpeg ./src/hw6 !end
-I/usr/local/include
: look in this directory for include files (optional in this case)-L/usr/local/lib
: look in this directory for library files (optional in this case, maybe required on Ubuntu)-ljpeg
: link to thelibjpeg.{a,so}
file (not optional here)
-
Utility that compiles programs based on rules read in from a file called Makefile
-
Widely used on Linux/Unix platforms
-
Setup and maintenance of Makefile(s) can become rather complicated for major projects
-
We will look at a few simple examples
!include src/ex1/sum.cpp c++
!include src/ex1/sum.hpp c++
!include src/ex1/main.cpp c++
!include src/ex1/makefile makefile
Anatomy of a make
rule:
target: dependencies
build_command
-
target
: is the thing you want the rule to create. The target should be a file that will be created in the file system. For example, the final executable or intermediate object file. -
dependencies
: space separated list files that the target depends on (typically source or header files) -
build_command
: a tab-indented shell command (or sequence) to build the target from dependencies.
Let's run make!
$ ls
main.cpp makefile sum.cpp sum.hpp
$ make
g++ -Wall -Wextra -Wconversion -o main main.cpp sum.cpp
$ ls
main main.cpp makefile sum.cpp sum.hpp
$ make
make: 'main' is up to date.
$
Make looks at time stamps on files to know when changes have been made and will
recompile accordingly (from src/ex1
directory):
$ make
make: 'main' is up to date.
$ touch main.cpp
$ make
g++ -Wall -Wextra -Wconversion -o main main.cpp sum.cpp
$ touch sum.hpp
$ make
g++ -Wall -Wextra -Wconversion -o main main.cpp sum.cpp
$ make
make: 'main' is up to date.
!include src/ex2/makefile makefile
Output (from src/ex2
directory):
$ ls
main.cpp makefile sum.cpp sum.hpp
$ make
g++ -Wall -Wextra -Wconversion -fsanitize=address -o main main.cpp sum.cpp
$ ls
main main.cpp makefile sum.cpp sum.hpp
$ make clean
rm -f main
$ ls
main.cpp makefile sum.cpp sum.hpp
!include src/ex3/makefile makefile
Output (from src/ex3
directory):
$ ls
bar.cpp foobar.hpp foo.cpp main.cpp makefile sum.cpp sum.hpp
$ make
g++ -c -o main.o main.cpp -O3 -Wall -Wextra -Wconversion -std=c++11
g++ -c -o sum.o sum.cpp -O3 -Wall -Wextra -Wconversion -std=c++11
g++ -c -o foo.o foo.cpp -O3 -Wall -Wextra -Wconversion -std=c++11
g++ -c -o bar.o bar.cpp -O3 -Wall -Wextra -Wconversion -std=c++11
g++ -o main main.o sum.o foo.o bar.o
$ ls
bar.cpp bar.o foobar.hpp foo.cpp foo.o main main.cpp main.o makefile sum.cpp sum.hpp sum.o
$ make clean
rm -f main.o sum.o foo.o bar.o main
$ ls
bar.cpp foobar.hpp foo.cpp main.cpp makefile sum.cpp sum.hpp
!include src/ex4/makefile makefile
Output (from src/ex4
directory):
$ ls
hw6.cpp hw6.hpp makefile stanford.jpg
$ make
g++ -c -o hw6.o hw6.cpp -DDEBUG -O3 -std=c++11 -Wall -Wextra -Wconversion
g++ -o hw6 hw6.o -ljpeg
$ ./hw6
$ make clean
rm -f hw6.o hw6 *~
$ make run
g++ -c -o hw6.o hw6.cpp -DDEBUG -O3 -std=c++11 -Wall -Wextra -Wconversion
g++ -o hw6 hw6.o -ljpeg
./hw6
$ ls
hw6 hw6.cpp hw6.hpp hw6.o makefile stanford.jpg test.jpg
-
Automation tool for expressing how your C/C++/Fortran code should be compiled
-
Good for small projects
-
But be careful with dependencies. It is very important to understand this process for larger projects.
-
Some people would not recommend hand writing Makefile(s) for larger projects (use CMake or similar)
-
With discipline, I believe that Make is a good tool for large projects. This is what I use. Sometimes CMake and other tools make it harder to build projects.