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 a Makefile.
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:
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
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.