Skip to main content

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

A Makefile consists of a series of entries, each of which consists of:

  1. 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.

  2. Zero or more Linux command lines, each of which has the format:

    1. 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.

    2. 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.

# makefile to create myProg

# link object file to system libraries to create executable
myProg: myProg.o 
      gcc -o myProg myProg.o

# assemble source file to create object file
myProg.o: myProg.s
      as --gstabs myProg.s -o myProg.o

# remove object files and backup files
clean:
      rm -i *.o *~
Listing A.0.1. An example of a Makefile for an assembly language program with one source file.

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.

# makefile to create biggerProg

# link object files and system libraries to create executable
biggerProg: biggerProg.o sub1.o sub2.o
      gcc -o biggerProg biggerProg.o sub1.o sub2.o

# compile/assemble source files to create object files
biggerProg.o: biggerProg.c sub1.h sub2.h
      gcc -g -c biggerProg.c

sub1.o: sub1.c sub1.h
      gcc -g -c sub1.c

sub2.o: sub2.s
      as --gstabs sub2.s -o sub2.o

# remove object files and backup files
clean:
      rm -i *.o *~
Listing A.0.2. An example of a Makefile for a program with both C and assembly language source files.

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.

# Makefile for biggerProg
# 2017-09-29: Bob Plantz

# Specify the compiler and assembler options.
COMPFLAGS = -g -c -O1 -Wall
ASMFLAGS = --gstabs

# The object files are specific to this program.
OBJECTS = biggerProg.o sub1.o sub2.o

biggerProg: $(OBJECTS)
	gcc -o biggerProg $(OBJECTS)

biggerProg.o: biggerProg.c sub1.h sub2.h
	gcc $(COMPFLAGS) -o biggerProg.o biggerProg.c

sub1.o: sub1.c sub1.h
	gcc $(COMPFLAGS) -o sub1.o sub1.c

sub2.o: sub2.s
	as $(ASMFLAGS) -o sub2.o sub2.s

clean:
	rm $(OBJECTS) biggerProg *~
Listing A.0.3. Makefile using variables for a program written in C and assembly language.
/* biggerProg.c
 * Demonstrate simple Makefile design
 * 2017-09-29: Bob Plantz
 */

#include <stdio.h>
#include "sub1.h"
#include "sub2.h"

int main()
{
  printf("Starting in main, about to call sub1...\n");
  sub1();
  printf("Back in main, about to call sub2...\n");
  sub2();
  printf("Back in main.\nProgram ending.\n");
    
  return 0;
}
Listing A.0.4. main function for Makefile demonstration. (C)
/* sub1.h
 * used with biggerProg.c to demonstrate make
 * 2017-09-29: Bob Plantz
 */
 
#ifndef SUB1_H
#define SUB1_H

void sub1();

#endif
Listing A.0.5. Header file for function written in C. (C)
/* sub1.c
 * used with biggerProg.c to demonstrate make
 * 2017-09-29: Bob Plantz
 */
 
#include <stdio.h>
#include "sub1.h"

void sub1()
{
  printf("In sub1\n");
}
Listing A.0.6. Function written in C for Makefile demonstration. (C)
/* sub2.h
 * used with biggerProg.c to demonstrate make
 * 2017-09-29: Bob Plantz
 */
 
#ifndef SUB2_H
#define SUB2_H

void sub2();

#endif
Listing A.0.7. Header file for function written in assembly language. This file is written in C, allowing this function to be called in C. (C)
@ sub2.s
@ used with biggerProg.c to demonstrate make
@ Bob Plantz - 14 September

@ Define my Raspberry Pi
        .cpu    cortex-a53
        .fpu    neon-fp-armv8
        .syntax unified         @ modern syntax

@ Constant program data
        .section .rodata
        .align  2
message:
        .asciz        "In sub2\n"

@ The code
        .text
        .align  2
        .global sub2
        .type   sub2, %function
sub2:
        stmfd   sp!, {r4, r5, fp, lr}   @ save regs
        add     fp, sp, 12      @ our frame pointer

        ldr     r0, messageAddr @ a message
        bl      printf

        mov     r0, 0           @ return 0;
        ldmfd   sp!, {r4, r5, fp, lr}  @ restore caller's info
        bx      lr              @ return

messageAddr:
        .word    message
Listing A.0.8. Function written in assembly language for Makefile demonstration. (prog asm)

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.

# INCOMPLETE!! Makefile for biggerProg
# 2017-09-29: Bob Plantz

# Specify the compiler and assembler options.
COMPFLAGS = -g -c -O1 -Wall
ASMFLAGS = --gstabs

# The object files are specific to this program.
OBJECTS = biggerProg.o sub1.o sub2.o

biggerProg: $(OBJECTS)
	gcc -o biggerProg $(OBJECTS)

clean:
	rm $(OBJECTS) biggerProg *~
Listing A.0.9. Incomplete!! Makefile for the program in Listing A.0.3.

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.