Makefile in Unix/Linux

So far, we have used individual gcc commands to compile-link the source files of C programs. For convenience, we may also use a sh script which includes all the commands. These schemes have a major drawback. If we only change a few of the source files, the sh commands or script would still compile all the source files, including those that are not modified, which is unnecessary and time­consuming. A better way is to use the Unix/Linux make facility (GNU make 2008). make is a program, which reads a makefile, or Makefile in that order, to do the compile-link automatically and selectively. This section covers the basics of makefiles and shows their usage by examples.

1. Makefile Format

A make file consists of a set of targets, dependencies and rules. A target is usually a file to be created or updated, but it may also be a directive to, or a label to be referenced by, the make program. A target depends on a set of source files, object files or even other targets, which are described in a Dependency List.Rules are the necessary commands to build the target by using the Dependency List. Figure 2.16 shows the format of a makefile.

2. The make Program

When the make program reads a makefile, it determines which targets to build by comparing the timestamps of source files in the Dependency List. If any dependency has a newer timestamp since last build, make will execute the rule associated with the target. Assume that we have a C program consisting of three source files:

  •  type.h file: // header file

int mysum(int x, int y) // types, constants, etc

  •  mysum.c file: // function in C

#include <stdio.h>

#incldue “type.h”

int mysum(int x, int y)

{

return x+y;

}

  •  t.c file: // main() in C

#include <stdio.h>

#include “type.h”

int main()

{

int sum = mysum(123,456);

printf(“sum = %d\n”, sum);

}

Normally, we would use the sh command

gcc -o myt main.c mysum.c

to generate a binary executable named myt. In the following, we shall demonstrate compile-link of C programs by using makefiles.

3. Makefile Examples

Makefile Example 1

(1). Create a makefile named mk1 containing:

myt: type.h t.c mysum.c           # target: dependency list

gcc -o myt t.c mysum.c       # rule: line MUST begin with a TAB

The resulting executable file name, myt in this example, usually matches that of the target name. This allows make to decide whether or not to build the target again later by comparing its timestamp against those in the dependency list.

(2). Run make using mk1 as the makefile: make normally uses the default makefile or Makefile, whichever is present in the current directory. It can be directed to use a different makefile by the -f flag, as in

make -f mk1

make will build the target file myt and show the command execution as

gcc -o myt t.c mysum.c

(3). Run the make command again. It will show the message

make: ‘myt’ is up to date

In this case, make does not build the target again since none of the files has changed since last build.

(4). On the other hand, make will execute the rule command again if any of the files in the dependency list has changed. A simple way to modify a file is by the touch command, which changes the timestamp of the file. So if we enter the sh commands

touch type.h      // or touch *.h, touch *.c, etc.

make -f mk1

make will recompile-link the source files to generate a new myt file

(5). If we delete some of the file names from the dependency list, make will not execute the rule command even if such files are changed. The reader may try this to verify it.

As can be seen, mk1 is a very simple makfile, which is not much different than sh commands. But we can refine makefiles to make them more flexible and general.

Makefile Example 2: Macros in Makefile

(1). Create a makefile named mk2 containing:

CC = gcc             # define CC as gcc
CFLAGS = -Wall       # define CLAGS as flags to gcc
OBJS = t.o mysum.o   # define Object code files
INCLUDE = -Ipath     # define path as an INCLUDE directory

myt: type.h $(OBJS)  # target: dependency: type.h and .o files

$(CC) $(CFLAGS) –o t $(OBJS) $(INCLUDE)

In a makefile, macro defined symbols are replaced with their values by $(symbol), e.g. $(CC) is replaced with gcc, $(CFLAGS) is replaced with -Wall, etc. For each .o file in the dependency list, make will compile the corresponding .c file into .o file first. However, this works only for .c files. Since all the .c files depend on .h files, we have to explicitly include type.h (or any other .h files) in the dependency list also. Alternatively, we may define additional targets to specify the dependency of .o files on .h files, as in

If we add the above targets to a makefile, any changes in either .c files or type.h will trigger make to recompile the .c files. This works fine if the number of .c files is small. It can be very tedious if the number of .c files is large. So there are better ways to include .h files in the dependency list, which will be shown later.

(3). Run make using mk2 as the makefile:

make -f mk2

(4). Run the resulting binary executable myt as before.

The simple makefiles of Examples 1 and 2 are sufficient for compile-link most small C programs. The following shows some additional features and capabilities of makefiles.

Makefile Example 3: Make Target by Name

When make runs on a makefile, it normally tries to build the first target in the makefile. The behavior of make can be changed by specifying a target name, which causes make to build the specific named target. As an example, consider the makefile named mk3, in which the new features are highlighted in bold face letters.

#——————- mk3 file——————-

CC = gcc                 #  define CC as gcc

CFLAGS = -Wall           #  define CLAGS as flags to gcc

OBJS = t.o mysum.o       #  define Object code files

INCLUDE = -Ipath         #  define path as an INCLUDE directory

all: myt install         #  build all listed targets: myt, install

myt: t.o mysum.o         #  target: dependency list of .o files

$(CC) $(CFLAGS) -o myt $(OBJS) $(INCLUDE)

t.o:    t.c type.h      # t.o depend on t.c and type.h

gcc -c t.c

mysum.o: mysum.c type.h # mysum.o depend mysum.c and type.h gcc -c mysum.c

install: myt            # depend on myt: make will build myt first

echo install myt to /usr/local/bin

sudo mv myt /usr/local/bin/ # install myt to /usr/local/bin/

run:    install          # depend on install, which depend on myt

echo run executable image myt

myt || /bin/true # no make error 10 if main() return non-zero

clean:

rm -f *.o 2> /dev/null              # rm all *.o files

sudo rm -f /usr/local/bin/myt       # rm myt

The reader may test the mk3 file by entering the following make commands:

(1). make [all] –f mk3     # build all targets: myt and install
(2). make install –f mk3   # build target myt and install myt
(3). make run –f mk3       # run /usr/local/bin/myt
(4). make clean –f mk3     # remove all listed files

Makefile Variables: Makefiles support variables. In a makefile, % is a wildcard variable similar to * in sh. A makefile may also contain automatic variables, which are set by make after a rule is matched. They provide access to elements from the target and dependency lists so that the user does not have to explicitly specify any filenames. They are very useful for defining general pattern rules. The following lists some of the automatic variables of make.

$@ : name of current target.

$< : name of first dependency

$^ : names of all dependencies

$* : name of current dependency without extension

$? : list of dependencies changed more recently than current target.

In addition, make also supports suffix rules, which are not targets but directives to the make program. We illustrate make variables and suffix rules by an example.

In a C program, .c files usually depend on all .h files. If any of the .h files is changed, all .c files must be re-compiled again. To ensure this, we may define a dependency list containing all the .h files and specify a target in a makefile as

DEPS = type.h          # list ALL needed .h files

%.o: %.c $(DEPS)       #  for  all .o files: if its  .c or .h file changed

$(CC) -c -o  $@   # compile corresponding .c    file again

In the above target, %.o stands for all .o files and $@ is set to the current target name, i.e. the current .o file name. This avoids defining separate targets for individual .o files.

Makefile Example 4: Use make variables and suffix rules

#———- mk4 file————–

CC = gcc CFLAGS = -I.

OBJS = t.o mysum.o

AS = as     # assume we have    .s files in assembly  also

DEPS = type.h               #  list all .h files in  DEPS

.s.o: # for each fname.o, assemble fname.s into fname.o

$(AS) -o $< -o $@   # -o $@ REQUIRED for .s files

.c.o: # for each fname.o, compile fname.c into fname.o

$(CC) -c $< -o $@   # -o $@ optional for .c files

%.o: %.c $(DEPS) # for all .o files: if its .c or .h file changed

$(CC) -c -o $@ $< # compile corresponding .c file again

myt: $(OBJS)

$(CC) $(CFLAGS) -o $@ $^

In the makefile mk4, the lines .s.o: and .c.o: are not targets but directives to the make program by the suffix rule. These rules specify that, for each .o file, there should be a corresponding .s or .c file to build if their timestamps differ, i.e. if the .s or .c file has changed. In all the target rules, $@ means the current target, $< means the first file in the dependency list and $^ means all files in the dependency list. For example, in the rule of the myt target, -o $@ specifies that the output file name is the current target, which is myt. $^ means it includes all the files in the dependency list, i.e. both t.o and mysum.o. If we change $^ to $< and touch all the .c files, make would generate an “undefined reference to mysum” error. This is because $< specifies only the first file (t.o) in the dependency list, make would only recompile t.c but not mysum.c, resulting a linking error due to missing mysum.o file. As can be seen from the example, we may use make variables to write very general and compact makefiles. The downside is that such makefiles are rather hard to understand, especially for beginning programmers.

Makfiles in Subdirectories

A large C programming project usually consists of tens or hundreds of source files. For ease of maintenance, the source files are usually organized into different levels of directories, each with its own makefile. It’s fairly easy to let make go into a subdirectory to execute the local makefile in that directory by the command

(cd DIR; $(MAKE)) OR cd DIR && $(MAKE)

After executing the local makefile in a subdirectory, control returns to the current directory form where make continues. We illustrate this advanced capability of make by a real example.

Makefile Example 5: PMTX System Makefiles

PMTX (Wang 2015) is a Unix-like operating system designed for the Intel x86 architecture in 32-bit protect mode. It uses 32-bit GCC assembler, compiler and linker to generate the PMTX kernel image. The source files of PMTX are organized in three subdirectories:

Kernel : PMTX kernel files; a few GCC assembly files, mostly in C

Fs : file system source files; all in C

Driver : device driver source files; all in C

The compile-link steps are specified by Makefiles in different directories. The top level makefile in the PMTX source directory is very simple. It first cleans up the directories. Then it goes into the Kernel subdirectory to execute a Makefile in the Kernel directory. The Kernel Makefile first generates .o files for both .s and .c files in Kernel. Then it directs make to go into the Driver and Fs subdirectories to generate .o file by executing their local Makfiles. Finally, it links all the .o files to a kernel image file. The following shows the various Makefiles of the PMTX system.

# ———- PMTX Top level Makefile—————

all: pmtx_kernel

pmtx_kernel:

make clean

cd Kernel && $(MAKE)

clean: # rm mtx_kerenl, *.o file in all directories

#———- PMTX Kernel Makefile —————-

AS = as -Iinclude

CC = gcc

LD = ld

CPP = gcc -E -nostdinc

CFLAGS = -W -nostdlib -Wno-long-long -I include -fomit-frame-pointer

KERNEL_OBJS = entry.o init.o t.o ts.o traps.o trapc.o queue.o \

fork.o exec.o wait.o io.o syscall.o loader.o pipe.o mes.o signal.o \

threads.o sbrk.o mtxlib.o

K_ADDR=0x80100000       # kernel start virtual address

all: kernel

.s.o:   # build each .o if its .s file has changed

${AS} -a $< -o $*.o > $*.map

pmtx kernel: $(KERNELOBJS)         # kernel target: depend on all OBJs

cd ../Driver && $(MAKE)      # cd to Driver, run local Makefile

cd ../Fs && $(MAKE)          # cd to Fs/, run local Makefile

# link all .o files with entry=pm_entry, start VA=0x80100000

${LD} –oformat binary -Map k.map -N -e pm_entry \

-Ttext ${K_ADDR} -o $@ \

${KERNEL_OBJS} ../DRIVER/*.o ../FS/*.o

clean:

rm -f *.map *.o

rm -f ../DRIVER.*.map ../DRIVER/*.o

rm -f ../FS/*.map ../FS/*.o

The PMTX kernel makfile first generates .o files from all .s (assembly) files. Then it generates other .o files from .c files by the dependency lists in KERNEL_OBJ. Then it goes into Driver and Fs directories to execute the local makefiles, which generate .o files in these directories. Finally, it links all the .o files to generate the pmtx_kernel image file, which is the PMTX OS kernel. In contrast, since all files in Fs and Driver are in C, their Makefiles only compile .c files to .o files, so there are no .s or ld related targets and rules.

#———— PMTX Driver Makefile—————–

CC=gcc

CPP=gcc -E -nostdinc

CFLAGS=-W -nostdlib -Wno-long-long -I include -fomit-frame-pointer

DRIVER_OBJS = timer.o pv.o vid.o kbd.o fd.o hd.o serial.o pr.o atapi.o

driverobj: ${DRIVER_OBJS}

#———— PMTX Fs Makefile———————

CC=gcc

CPP=gcc -E -nostdinc

CFLAGS=-W -nostdlib -Wno-long-long -I include -fomit-frame-pointer

FS_OBJS = fs.o buffer.o util.o mount_root.o alloc_dealloc.o \

mkdir_creat.o cd_pwd.o rmdir.o link_unlink.o stat.o touch.o \

open_close.o read.o write.o dev.o mount_umount.o

fsobj:   ${FS_OBJS}

#———— End of Makefile———————

Source: Wang K.C. (2018), Systems Programming in Unix/Linux, Springer; 1st ed. 2018 edition.

Leave a Reply

Your email address will not be published. Required fields are marked *