Appendix A Using GNU make
to Build Programs
This discussion covers the fundamental concepts employed in a Makefile
. My intent is to show you how to write a Makefile
that helps you to debug your programs. The problem with many discussions of make
is that they show how to use many of the “features” of the program. Many problems students have when debugging their programs are actually caused by errors in their Makefile
that cause make
to compile/assemble their program without the debugging option.
I know from personal experience that it is easy not to notice that this has occurred. It is much easier to avoid these problems if you:
Keep your
Makefile
very simple.Read what
make
writes on the screen very carefully when it executes aMakefile
.
A Makefile
consists of a series of entries, each of which consists of:
-
One dependency line, which has the format:
target: [prerequisite1] [prerequisite2] ...
Prerequisites are names of files or other targets in this
Makefile
. Use spaces as separators between the prerequisites, not tabs. -
Zero or more Linux command lines, each of which has the format:
Each line must begin with a tab character (not a group of spaces). Be careful to make sure that your editor does not replace tabs with spaces.
The remainder of the line is a Linux command.
There can also be (should be!) comment lines, which begin with a ‘#
’ character.
If you invoke make
with no argument:
pi@rpi3:~$ make
make
will look for its instructions in a file in the current directory named Makefile
(or makefile
). Because you can only have one file named Makefile
in a directory, you need to have a separate directory for each program if you are using the make
program.
Starting with the prerequisites of the first target—the first entry or the argument used in the command—make
follows the hierarchical tree of target/prerequisite entries until it gets to the lowest level—either a file or no prerequisites. It works its way back up through this tree. If any entry along the tree has a target that is not at least as recent as all of its prerequisites, the commands for that entry are executed.
A common organization is to have an entry for the program name, with each of the object files that make up the program as prerequisites. The command(s) in this entry link the object files together. Then there is an entry for each object file, with its source file and any required local header files as prerequisites. The command(s) in each of these entries compile/assemble the source file and produce the corresponding object file. It is also common to have various utility targets in a Makefile
.
These concepts are illustrated in Listing A.0.1.
The functions in real programs are distributed amongst many files. When changes are made, it is clearly a waste of time to recompile all the functions. With a properly designed Makefile
, make
only recompiles files that have been changed. (This is a motivation for placing each function is its own file.) Listing A.0.2 illustrates a Makefile
for a program where the main function and one subfunction are written in C and one subfunction is written in assembly language. Notice that header files have been created for both subfunctions to provide prototype statements for the main
function, which is written in C. The assembly language source file does not #include
its own header file because prototype statements do not apply to assembly language.
As you can see, there is quite a bit of repetition in a Makefile
. Variables provide a good way to reduce the chance of typing errors. The Makefile
I used for the program in Listing A.0.3 illustrates the use of variables to simplify the Makefile
.
Executing the Makefile
in Listing A.0.3 gives:
pi@rpi3:~/bookProgs/appendixB/biggerProg $make
gcc -g -c -O1 -Wall -o biggerProg.o biggerProg.c
gcc -g -c -O1 -Wall -o sub1.o sub1.c
as --gstabs -o sub2.o sub2.s
gcc -o biggerProg biggerProg.o sub1.o sub2.o
Now let us look at what occurs if we omit some of the entries from the Makefile
in Listing A.0.3 to create the INCOMPLETE!! Makefile
in Listing A.0.9.
When I execute the INCOMPLETE!! Makefile
in Listing A.0.9, unfortunately make
does not show any errors:
pi@rpi3:~/bookProgs/appendixB/biggerProg $make
cc -c -o biggerProg.o biggerProg.c
cc -c -o sub1.o sub1.c
as -o sub2.o sub2.s
gcc -o biggerProg biggerProg.o sub1.o sub2.o
make
created the program just fine, but since the Makefile
does not include specific entries for compiling/assembling each individual function, make
uses its default behavior, which does not include the debugging options. Thus I am unable to use gdb
to debug my program.
The default behavior of make
is such that if you compile the program myProg
with the command:
pi@rpi3:~/chp02 $ make myProg
make
will look for its instructions in a file in the current directory named Makefile
(or makefile
) and searches the file for a target named myProg
. If there is no Makefile
(or makefile
), make
searches for a file named myProg.s
, myProg.c
, or myProg.cc
. If either of the .s
or .c
source files exists, make
issues the command:
cc myProg.c -o myProg
or if the .cc
source file exits,
g++ myProg.cc -o myProg
Notice that the compiler is invoked with only the -o
option. You will not be able to use gdb
to debug the program because the -g
option does not get used. This means that if one of the entries in your Makefile
is incorrect, the default behavior may cause make
to compile a source file without the debugging option.