Skip to main content
\(\newcommand{\doubler}[1]{2#1} \newcommand{\binary}{\texttt} \newcommand{\hex}{\texttt} \newcommand{\octal}{\texttt} \newcommand{\prog}{\texttt} \newcommand{\lt}{ < } \newcommand{\gt}{ > } \newcommand{\amp}{ & } \)

AppendixCUsing 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 the Makefile that cause make to use its default behavior.

For example, if you try to compile the program myProg with the command:

make myProg

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.

Assuming Makefile exists, make searches the file for a target (defined later in this chapter) named myProg. If there is no 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.

I know from personal experience that it is easy to not 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 \texttt{make} with no argument:

make
it starts with the first entry in the Makefile.

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 C.0.9.

# 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 *~
ListingC.0.9An 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 C.0.10 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 *~
ListingC.0.10An 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 C.0.11 illustrates the use of variables to simplify the Makefile.

# Makefile for biggerProg
# Bob Plantz - 14 September 2016

# 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 *~
/* biggerProg.c
 * Demonstrate simple Makefile design
 * Bob Plantz - 14 September
 */

#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;
}
/* sub1.h
 * used with biggerProg.c to demonstrate make
 * Bob Plantz - 14 September 2016
 */
 
#ifndef SUB1_H
#define SUB1_H

void sub1();

#endif
/* sub1.c
 * used with biggerProg.c to demonstrate make
 * Bob Plantz - 14 September 2016
 */
 
#include <stdio.h>
#include "sub1.h"

void sub1()
{
  printf("In sub1\n");
}
/* sub2.h
 * used with biggerProg.c to demonstrate make
 * Bob Plantz - 14 September 2016
 */
 
#ifndef SUB2_H
#define SUB2_H

void sub2();

#endif
@ 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
ListingC.0.11Makefile using variables for a program written in C and assembly language. (C; prog asm)

Executing the Makefile in Listing C.0.11 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 omit some of the entries from the Makefile in Listing C.0.11 to create the INCOMPLETE!! Makefile in Listing C.0.12.

# INCOMPLETE!! Makefile for biggerProg
# Bob Plantz - 14 September 2016

# 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 *~
ListingC.0.12Incomplete!! Makefile for the program in Listing C.0.11.

When I execute the INCOMPLETE!! Makefile in Listing C.0.12, I get:

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 it was created without the debugging options. So I am unable to use gdb to debug my program.