4
\$\begingroup\$

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.

\$\endgroup\$
4
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 Mar 1 '17 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" to Makefile.am and all the other steps would automatically happen. \$\endgroup\$ – Edward Mar 1 '17 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 Mar 1 '17 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 Mar 2 '17 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 Mar 2 '17 at 13:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.