Makefile for building ASM/C project 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 $&lt; -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.
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) $&lt; -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) $&lt; -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) $&lt; -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) $&lt; -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.