I am building a bare bones freestanding kernel in ASM and C. My make file is below.
CFLAGS= -ffreestanding -nostdlib -gdwarf-4 -m32 -ggdb3 -Wall -Wextra -Werror -std=c11
ASMCFLAGS= -f elf32 -F dwarf -g -w+all
LDFLAGS= --nmagic -m elf_i386 --script=os.lds
BUILD_DIR=../build/os
BUILD_DIR_IMM=$(BUILD_DIR)/imm
BUILD_DIR_IMM_ASM=$(BUILD_DIR_IMM)/asm
BUILD_DIR_IMM_C=$(BUILD_DIR_IMM)/c
BUILD_OUTPUT=$(BUILD_DIR)/os
OS_C_SRCS := $(wildcard *.c)
OS_C_OBJS := $(patsubst %.c, $(BUILD_DIR_IMM_C)/%.o, $(OS_C_SRCS))
OS_ASM_SRCS := $(wildcard *.asm)
OS_ASM_OBJS := $(patsubst %.asm, $(BUILD_DIR_IMM_ASM)/%.o, $(OS_ASM_SRCS))
OS_SRCS := $(OS_C_SRCS) $(OS_ASM_SRCS)
OS_OBJS := $(OS_ASM_OBJS) $(OS_C_OBJS)
all: $(BUILD_OUTPUT)
prepdir:
mkdir -p $(BUILD_DIR_IMM_C)
mkdir -p $(BUILD_DIR_IMM_ASM)
$(OS_C_OBJS): $(OS_C_SRCS) $(OS_ASM_OBJS) prepdir
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD_DIR_IMM_ASM)/__init.o: __init.asm prepdir
nasm $(ASMCFLAGS) __init.asm -o $@
$(BUILD_DIR_IMM_ASM)/kinit.o: kinit.asm prepdir
nasm $(ASMCFLAGS) kinit.asm -o $@
$(BUILD_DIR_IMM_ASM)/keyboard.o: keyboard.asm prepdir
nasm $(ASMCFLAGS) keyboard.asm -o $@
$(BUILD_DIR_IMM_ASM)/interrupt.o: interrupt.asm prepdir
nasm $(ASMCFLAGS) interrupt.asm -o $@
$(BUILD_DIR_IMM_ASM)/string.o: string.asm prepdir
nasm $(ASMCFLAGS) string.asm -o $@
$(BUILD_DIR_IMM_ASM)/vid.o: vid.asm prepdir
nasm $(ASMCFLAGS) vid.asm -o $@
$(BUILD_OUTPUT): $(OS_C_OBJS) $(OS_ASM_OBJS)
ld $(LDFLAGS) $(OS_OBJS) -o $@
clean:
rm -rf $(BUILD_DIR)
This makefile is used as a child makefile.
My primary concern is that I cannot pass multiple files to NASM, so I need a rule for each assembly file I have. My secondary concern is cross dependencies on other files, for instance, my C files should be rebuild if any C header files change.
1 Answer 1
I see a number of things that may help you improve this.
Don't manually write a Makefile
Since you're using standard tools such as gcc
and ld
, you could use: autotools
(that is the collection of autoconf
and related software) instead of writing and maintaining your own Makefile
. There is a learning curve, of course, but the benefits are considerable, especially if you're writing software that will become open source. To do that here, you can start with these two files:
configure.ac
AC_PREREQ([2.69])
AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS])
AC_CONFIG_SRCDIR([foo.c])
AC_CONFIG_HEADERS([config.h])
AC_PROG_CC
PACKAGE_CFLAGS="-ffreestanding -nostdlib -gdwarf-4 -m32 -ggdb3 -Wall -Wextra -Werror -std=c11"
PACKAGE_LDFLAGS="--nmagic -m elf_i386 --script=os.lds"
PACKAGE_NASMFLAGS="-f elf32 -F dwarf -g -w+all"
AC_SUBST([PACKAGE_CFLAGS])
AC_SUBST([PACKAGE_LDFLAGS])
AC_SUBST([PACKAGE_NASMFLAGS])
AM_INIT_AUTOMAKE(foreign)
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
Makefile.am
bin_PROGRAMS = os
os_SOURCES = foo.c ${asm_sources}
os_NASMFLAGS = @PACKAGE_NASMFLAGS@
os_LDFLAGS = @PACKAGE_LDFLAGS@
AM_CPPFLAGS = @PACKAGE_CFLAGS@
NASM = /usr/bin/nasm
asm_sources = thing1.asm thing2.asm
.asm.o:
$(NASM) $(os_NASMFLAGS) $< -o $@
With these, you can execute autoreconf -i
in the same directory. Next, create a build directory anywhere (one of the advantages of automake
) and execute the generated configure
file from that directory. That will create a nice Makefile
for you which you can then execute in the usual way by typing make
.
Another tool I use very often is CMake
, which does something similar to autotools
in that it creates build scripts for you, but it's not tied to Linux and BSD and can work with Windows-based compilers as well.
With that said, if you're determined to do it the hard way, there are some things you can improve in your existing file.
Define an implicit rule
GNU Make (which I assume that you're using) knows how to create object files from assembly files, but only by using as
rather than nasm
. It's very simple to teach it a new trick, however, by defining an implicit rule. Here's one:
.asm.o:
$(NASM) $(NASMFLAGS) $< -o $@
This is a simple rule that tells make
that the "recipe" to create a .o
file from a .asm
file is to run the command listed below. That way you only need to list a dependency and not the rule each time. That is:
thing1.o: thing1.asm
However, even this is often not necessary if you've defined the implicit rule. The next suggestion explains.
Make make
work harder
The purpose to having make
is to offload some of the work of the programmer to quickly do only what's necessary to rebuild a new binary. You can make it work harder than you currently are, while making things easier to maintain and a bit shorter as well. If you're using GNU Make, you can define an implicit rule like this (note the slightly different syntax from the standard implicit rule listed above):
%.o : %.asm
$(NASM) $(NASMFLAGS) $< -o $@
Then simply tell make
the object files needed and let it figure out by itself that it needs to run the compiler on C files and nasm
on assembly files:
os : foo.o thing1.o thing2.o
ld $(LDFLAGS) $^ -o $@
Don't include paths in filenames
Inserting and maintaining paths everywhere is tedious and error prone. Better is to use VPATH
within the Makefile
like this:
VPATH=../src
Now assuming your directory tree looks like this:
.
├── build
└── src
You can navigate into the empty build
directory and type make -f ../src/Makefile
. All of the binaries will be created in the build
directory and the source directory will be untounched. VPATH
tells make where to look for sources it needs. This is a much nicer way to do things.
Mark phony targets .PHONY
There isn't really an all
file created and there really isn't a clean
file. These are actually just tags that identify a target whose rules should always be run. For this reason they should be explicitly declared as phony:
.PHONY: clean
clean:
rm -rf os $(OBJECT_FILES)
Explicitly name sources
Lines like this one:
OS_C_SRCS := $(wildcard *.c)
generally should be avoided. The reason is that if you create, say, a small test file to try out a concept while working on the source and then forget to delete it, it becomes part of the build whether you wanted it or not. It's usually far better to explicitly name files rather than using wildcards.
Putting it all together
Here's a revised Makefile
which implements these suggestions:
Makefile
CFLAGS= -ffreestanding -nostdlib -gdwarf-4 -m32 -ggdb3 -Wall -Wextra -Werror -std=c11
NASM=nasm
NASMFLAGS= -f elf32 -F dwarf -g -w+all
LDFLAGS= --nmagic -m elf_i386 -L$(VPATH) --script=os.lds
VPATH=../src
OBJECT_FILES=foo.o thing1.o thing2.o
.PHONY: all
all: os
%.o : %.asm
$(NASM) $(NASMFLAGS) $< -o $@
os : foo.o thing1.o thing2.o
$(LD) $(LDFLAGS) $^ -o $@
.PHONY: clean
clean:
rm -rf os $(OBJECT_FILES)
Create libraries
One thing I forgot to mention was that it's often good practice to create libraries. By grouping related object code together into a library, you gain a number of advantages. First, recompiling the project typically becomes a bit faster. Second, it allows (forces?) you to think carefully about the interface. A well-defined interface to library code not only makes it easier to use (and potentially replace with an improved or alternate version) but also aids in testing. Third, by having a separate library, testing can often be simplified by writing tests that thoroughly exercise each library. When all libraries are tested, it's easier to test the main application with confidence.
-
\$\begingroup\$ Thanks for your feedback! Wouldn't autoconfig require me to re-execute it everytime I add a new source file to the project? Secondly, it appears your makefile does not output files to a
build
folder, am I right? \$\endgroup\$Sebazzz– Sebazzz2017年03月01日 21:15:15 +00:00Commented Mar 1, 2017 at 21:15 -
\$\begingroup\$ Actually, the makefile created via autotools will automatically take care of rerunning
configure
without any further intervention. That is, after you've built it once, you could simply add "thing3.asm" toMakefile.am
and all the other steps would automatically happen. \$\endgroup\$Edward– Edward2017年03月01日 21:26:48 +00:00Commented Mar 1, 2017 at 21:26 -
\$\begingroup\$ Also, if you do it the way I suggest, by navigating into a build directory and executing
configure
from there, any generated files will be created there (including.o
or.so
and the final linked target binary) and only there, leaving your source tree untouched. This allows, among many other advantages, building from source code on non-writable media (e.g. CD or DVD). \$\endgroup\$Edward– Edward2017年03月01日 21:26:54 +00:00Commented Mar 1, 2017 at 21:26 -
\$\begingroup\$ The reason I'm asking is that I have a slightly larger build. I have a bootloader which is outputted to build/bootloader, and the kernel which is emitted to build/os, which is then put on a disk image emitted in
build/disk.img
. Also a lot of open source projects actually place ./configure in the root of the project and you can execute it there, am I right? \$\endgroup\$Sebazzz– Sebazzz2017年03月02日 07:54:38 +00:00Commented Mar 2, 2017 at 7:54 -
\$\begingroup\$ Yes the
configure
script is most often placed in the root directory of a project and checked in to the repository. The reason is that while anyone could regenerate it, providing it in the repo means that users of the project (as contrasted with developers) only need a toolchain (compiler, linker, etc.) and don't need autotools. An example that doesn't do that: github.com/dogecoin/dogecoin \$\endgroup\$Edward– Edward2017年03月02日 13:17:52 +00:00Commented Mar 2, 2017 at 13:17