Guide: Using make for cross-development


Written by Julie Zelenski

What is a Makefile?

Make is a tool that automates building programs; a Makefile describes the commands and options used in the build process. As you will see soon enough, using a Makefile saves you a lot of retyping and makes your life as a developer a whole lot smoother!

This guide introduces Makefiles using examples of cross-development. For further information about Makefiles in general, check out the other resources on the bottom of this page.

An example Makefile

The example makefile below builds the blink.bin program out of the blink.c source file. Our labs and assignments will include similar Makefiles, so you will soon become familiar with the common structure.

NAME = blink

ARCH    = -march=rv64im -mabi=lp64
CFLAGS  = $(ARCH) -g -Og -I$$CS107E/include -Wall -ffreestanding
LDFLAGS = -nostdlib -L$$CS107E/lib -T memmap.ld

all: $(NAME).bin
 
%.bin: %.elf
	riscv64-unknown-elf-objcopy $< -O binary $@

%.elf: %.o
	riscv64-unknown-elf-gcc $(LDFLAGS) $< -o $@

%.o: %.c
	riscv64-unknown-elf-gcc $(CFLAGS) -c $< -o $@

run: $(NAME).bin
	mango-run $<

clean:
	rm -f *.o *.elf *.bin

This Makefile may look a bit cryptic at first! Let's try breaking it down step by step.

Rules and recipes

In lecture, Pat whipped up a simple "doit" script to automate retyping the commands to rebuild a program. The make tool is just a fancier version of doit. A Makefile is a text file that describes the steps needed to build a program. Here is an example of a very simple hard-coded Makefile containing three targets all, button.bin and clean:

all: button.bin

button.bin: button.c
	riscv64-unknown-elf-gcc -ffreestanding -c button.c -o button.o
	riscv64-unknown-elf-gcc -nostdlib button.o -o button.elf
	riscv64-unknown-elf-objcopy button.elf -O binary button.bin
    
clean: 
	rm -f *.bin *.o

Rules are written in the following way: "the dependencies on the right-hand-side are required to make the target on the left-hand-side." Thus the first line indicates that button.bin is required to make all. In other words, to make all, we must first make button.bin.

    all: button.bin

This brings us to the next rule for button.bin:

button.bin: button.c
	riscv64-unknown-elf-gcc -ffreestanding -c button.c -o button.o
	riscv64-unknown-elf-gcc -nostdlib button.o -o button.elf
	riscv64-unknown-elf-objcopy button.elf -O binary button.bin

The ingredients (dependencies on the right-hand-side) are needed as the starting point to create the desired output (target on the left-hand-side). The indented lines that follow the rule are the commands that turn the ingredients into the final product. These steps are collectively called the recipe. Thus, in order to make button.bin, we start with our ingredient (button.c) and then step through the commands in the recipe.

We could add a comment to explain the additional flags included when invoking the compiler. Lines starting with # are treated as comments.

# Build flag used:
#  -ffreestanding  generate code assuming environment is unhosted
#  -nostdlib       no link with standard libraries

The final rule indicates what should happen when we make clean; the recipe for the clean target removes any previous build products so the next compile starts fresh.

clean: 
	rm -f *.bin *.o

One particularly nifty thing make is that only rebuilds a target when one or more of the components it depends on has changed. If you attempt to re-build a target which is already up-to-date, make will tell you:

$ make
make: Nothing to be done for `all'.

Macros

After repeatedly copy-pasting the example Makefile to create a version for a new program, you can see the value in structuring it to be more general-purpose. After all, Makefiles are written for convenience!

NAME = blink
CFLAGS  = -ffreestanding
LDFLAGS = -nostdlib

all: $(NAME).bin

$(NAME).bin: $(NAME).c
	riscv64-unknown-elf-gcc $(CFLAGS) -c $(NAME).c -o $(NAME).o
	riscv64-unknown-elf-gcc $(LDFLAGS) $(NAME).o -o $(NAME).elf
	riscv64-unknown-elf-objcopy $(NAME).elf -O binary $(NAME).bin

clean: 
	rm -f *.bin *.o

We've added three macros up top. They're similar to variables in that they replace instances of the macro throughout the file with their assigned text. The $(macro_name) syntax is used to access the value of the macro. This makes it easy to change the name for a new program.

Pattern rules

We can further generalize our Makefile by using pattern rules that can be used to operate on any source file, without hard-coding to a particular name.

# This pattern rule compiles a C program into an object file.
# filename.o is built from filename.c
%.o: %.c
	riscv64-unknown-elf-gcc $(CFLAGS) -c $< -o $@

# This pattern rule converts assembly instructions into an object file.
# filename.o is built from filename.s
%.o: %.s
	riscv64-unknown-elf-as $(CFLAGS) $< -o $@

# This pattern rule links an object file into an executable ELF file.
# filename.elf is built from filename.o
%.elf: %.o
	riscv64-unknown-elf-gcc $(LDFLAGS) $< -o $@

# This pattern rule extract binary from an ELF executable
# filename.bin is built from filename.elf
%.bin: %.elf
	riscv64-unknown-elf-objcopy $< -O binary $@

The symbols that begin with $ and % in a pattern rule are handled by make using the following interpretations:

  • % is a wildcard symbol when used in a rule; %.o for example matches any file that ends with .o
  • $@ refers to the left part of the rule, before the :
  • $< refers to the first element in the right part of the rule, after the :

One more special variable $^ refers to all elements in the right part of the rule, after the :, which is to say all of the dependencies.

For further convenience, we can add a rule for the run target. We use this target to invoke the command ` blink.bin` to load and execute our newly-built program on the Pi.

# The run target uploads a freshly made binary image to the xfel bootloader
run: $(NAME).bin
	mango-run $<

With that finishing touch, you have a general Makefile that can be easily re-purposed for other projects. Now that you know that a Makefile is just a cookbook that culminates in the tasty program you wish to create, you're ready to add your favorite recipes and bon appetit!

Going further

Some follow up references on Makefiles:

Q. Make is failing with a cryptic error about Makefile: *** missing separator. What gives?

A. In what is widely considered one of the dumber decisions in the history of computing, a Makefile distinguishes between tabs and spaces. The recipe lines for a target must begin with a tab and an equivalent number of spaces just won't do. Edit your makefile and replace those errant spaces with a tab to restore Makefile joy.