© Igor Zhirkov 2017

Igor Zhirkov, Low-Level Programming, 10.1007/978-1-4842-2403-8_19

19. Appendix B. Using Make

Igor Zhirkov

(1)Saint Petersburg, Russia

This appendix will introduce you to the most basic notions of writing Makefiles. For more information refer to [2].

To build a program you might need to perform multiple actions: launch compiler with the right flags, probably for each source file, and use linker. Sometimes you have to launch scripts written to generate source code files as well. At times the program consists of several parts written in different programming languages!

Moreover, if you changed only a part of the program, you might not want to rebuild everything but only those parts that depend on the source file changed. Huge programs can take hours of CPU (central processing unit) time to build!

In this book we are going to use GNU Make. It is a common tool used to control the generation of artifacts such as executable files, dynamic libraries, resource files, etc.

19.1 Simple Makefile

When you write a program, you should write a special Makefile for it, so that it is possible to use Make to build it. This text file describes the source files and the dependencies between them in a declarative manner. Then make will choose the right order in which the files should be worked so that when each file is being processed, its dependencies are already processed.

To start the building process, execute make in the directory where Makefile is created. It is usually a root directory of your project.

You can explicitly select another Makefile by providing -f flag, for example: make -f Makefile_other.

The basic Makefile is composed of the following blocks, each of them is called rule:

<target> : <prerequisites>
[tab] <recipe>

A rule describes how to generate a specific file, which is the rule’s <target>. <prerequisites> describe which other targets should be generated first.

A recipe consists of one or many actions to be carried out by make. Every recipe line should be preceded by [tab] character!

Let us say, we have a simple program consisting of two assembly files: main.asm and lib.asm. We want to produce the object file for each of them and then link these into an executable program.

Listing 19-1 shows an example of a simple Makefile.

Listing 19-1. Makefile_simple
program: main.o lib.o
    ld -o program main.o lib.o


lib.o: lib.asm
    nasm -f elf64 -o lib.o lib.asm


main.o: main.asm
    nasm -f elf64 -o main.o main.asm


clean:
    rm main.o lib.o program

When the Makefile with these contents is created, executing make in the same directory will launch the recipe for the first target described. If a target named all is present, its recipe will be executed instead. Otherwise, typing make targetname will execute the recipe for the target targetname.

The target program should produce the file program. To do it we should build files main.o and lib.o first. If we change the file main.o and launch make again, only main.o will be rebuilt before refreshing program, but not lib.o. The same mechanism forces rebuilding lib.o when lib.asm is changed.

So, the recipe is launched when there is no file corresponding to the target name or this file should be changed (because one of its dependencies has been updated).

Traditionally, every Makefile has a target named clean to get rid of all produced files, leaving only the sources. The targets such as clean are called Phony Targets, because they do not correspond to a certain file. It is best to enumerate them in a separate recipe corresponding to a special .PHONY target as follows:

clean:
    rm -f *.o


help:
    echo 'This is the help'


.PHONY: clean help

19.2 Throwing in Variables

It is not very appropriate to duplicate a lot of text in Makefiles. As soon as there are many source files that are compiled alike, we grow tired of repeatedly copying the same compile options. The variables solve this problem.

The variables are declared as follows:

variable = value

They are not the same thing as environmental variables such as PWD. Their values are substituted using a dollar sign and a pair of parentheses as follows:

$(variable)

Now, we are going to use the variables in at least the following cases:

  • To abstract the compiler (we will be able to easily switch between Clang, GCC, MSVC, or whatever else compiler as long as they support the same set of flags).

  • To abstract the compilation flags.

Traditionally, in case of C, these variables are named

  • CC for “C compiler.”

  • CFLAGS for “C compiler flags.”

  • LD for “link editor” (linker).

  • AS as “assembly language compiler.”

  • ASFLAGS as “assembly language compiler flags.”

An additional benefit is that whenever we want to choose compilation flags we only need to do it in one place. Listing 19-2 shows the modified Makefile.

Listing 19-2. Makefile_vars
AS = nasm
LD = ld
ASFLAGS = -f elf64


program: main.o lib.o
    $(LD) -o program main.o lib.o


lib.o: lib.asm
    $(AS) $(ASFLAGS) -o lib.o lib.asm


main.o: main.asm
    $(AS) $(ASFLAGS) -o main.o main.asm


clean:
    rm main.o lib.o program


.PHONY: clean

A variable can be left empty, and it will be expanded to an empty string:

EMPTYVAR  =

A variable can include other variables’ values:

INCLUDEDIR   = include
CFLAGS       = -c -std=c99 -I$(INCLUDEDIR) -ggdb -Wno-attributes

Target names support the wildcard symbol %. There should be only one such wildcard in a target name. The substring that % matches is called the stem. The occurences of % in prerequisites are replaced with exactly the stem. For example, this rule

%.o : %.c
    echo "Building an object file"

specifies how to build any object file from a .c file with the matching name. However, right now we do not know how to use these rules, because once we try to write a command to compile the file we face a problem: we do not know the exact names of the files involved, and the stem is inaccessible inside the recipe. The automatic variables solve this problem.

19.3 Automatic Variables

Automatic variables are a special feature of make. They are computed afresh for each rule that is executed, and their values depend on the target and its prerequisites. They can only be used within the recipe itself, not inside prerequisites or inside the target itself.

Imagine you want to compile each .c file into an .o file with the same flags. Should we really duplicate all the rules? No, we can use the wildcards in conjunction with automatic variables.

There are many automatic variables, but the most commonly used are

  • $* The stem.

  • $@ The file name of the target of the rule.

  • $< The name of the first prerequisite.

  • The names of all the prerequisites separated by spaces.

  • $? The names of all the prerequisites that are newer than the target.

Listing 19-3 shows an exemplary Makefile which uses all knowledge from this tutorial.

Listing 19-3. makefile_ autovars
CC = gcc
CFLAGS = -std=c11 -Wall
LD = gcc


all: main

main: main.o lib.o
   $(LD) $ˆ -o $@


%.o: %.c %.h
   $(CC) $(CFLAGS) -c $< -o $@


clean:
   rm -f *.o main


.PHONY: clean

It assumes the following project tree:

.
 lib.c
 lib.h
 main.c
 main.h
 Makefile
0 directories, 8 files

A clean make will execute the following commands:

> make
gcc -std=c11 -Wall -c main.c -o main.o
gcc -std=c11 -Wall -c lib.c -o lib.o
gcc  main.o lib.o -o main

Refer to the well-written GNU Make Manual [2] for further instructions.