MIT 6.828 OS lab Makefile 分析

 lab1 中的 Makefile 主要是根目录下的 GNUMakefile, kern/Makefrag, boot/Makefrag, 后两者通过 include 直接包含到 GNUMakefile 中。

#
# This makefile system follows the structuring conventions
# recommended by Peter Miller in his excellent paper:
#
#    Recursive Make Considered Harmful
#    http://aegis.sourceforge.net/auug97.pdf
#
OBJDIR := obj

# 10-16行, 如果编译时使用make V=1,则会输出详细编译信息,否则输出类似 “CC MAIN”的简单信息
# 很多Makefile都采用这种方式, 包括Linux kernel.
# Run 'make V=1' to turn on verbose commands, or 'make V=0' to turn them off.
ifeq ($(V),1)
override V =
endif
ifeq ($(V),0)
override V = @
endif

-include conf/lab.mk

-include conf/env.mk

LABSETUP ?= ./

TOP = .

# Cross-compiler jos toolchain
#
# This Makefile will automatically use the cross-compiler toolchain
# installed as 'i386-jos-elf-*', if one exists.  If the host tools ('gcc',
# 'objdump', and so forth) compile for a 32-bit x86 ELF target, that will
# be detected as well.  If you have the right compiler toolchain installed
# using a different name, set GCCPREFIX explicitly in conf/env.mk

# 34-48, 通过执行shell objdump 命令得到GCCPREFIX, 即如果 i386-jos-elf-objdump存在
# GCCPREFIX=i386-jos-elf-objdump, 否则GCCPREFIX=""
# try to infer the correct GCCPREFIX
ifndef GCCPREFIX
GCCPREFIX := $(shell if i386-jos-elf-objdump -i 2>&1 | grep '^elf32-i386$$' >/dev/null 2>&1; \
    then echo 'i386-jos-elf-'; \
    elif objdump -i 2>&1 | grep 'elf32-i386' >/dev/null 2>&1; \
    then echo ''; \
    else echo "***" 1>&2; \
    echo "*** Error: Couldn't find an i386-*-elf version of GCC/binutils." 1>&2; \
    echo "*** Is the directory with i386-jos-elf-gcc in your PATH?" 1>&2; \
    echo "*** If your i386-*-elf toolchain is installed with a command" 1>&2; \
    echo "*** prefix other than 'i386-jos-elf-', set your GCCPREFIX" 1>&2; \
    echo "*** environment variable to that prefix and run 'make' again." 1>&2; \
    echo "*** To turn off this error, run 'gmake GCCPREFIX= ...'." 1>&2; \
    echo "***" 1>&2; exit 1; fi)
endif

# 50-64, 和上面类似, 得到QEMU=qemu,或者QEMU=qemu-system-i386
# try to infer the correct QEMU
ifndef QEMU
QEMU := $(shell if which qemu > /dev/null; \
    then echo qemu; exit; \
        elif which qemu-system-i386 > /dev/null; \
        then echo qemu-system-i386; exit; \
    else \
    qemu=/Applications/Q.app/Contents/MacOS/i386-softmmu.app/Contents/MacOS/i386-softmmu; \
    if test -x $$qemu; then echo $$qemu; exit; fi; fi; \
    echo "***" 1>&2; \
    echo "*** Error: Couldn't find a working QEMU executable." 1>&2; \
    echo "*** Is the directory containing the qemu binary in your PATH" 1>&2; \
    echo "*** or have you tried setting the QEMU variable in conf/env.mk?" 1>&2; \
    echo "***" 1>&2; exit 1)
endif

# try to generate a unique GDB port
GDBPORT    := $(shell expr `id -u` % 5000 + 25000)

CC    := $(GCCPREFIX)gcc -pipe
AS    := $(GCCPREFIX)as
AR    := $(GCCPREFIX)ar
LD    := $(GCCPREFIX)ld
OBJCOPY    := $(GCCPREFIX)objcopy
OBJDUMP    := $(GCCPREFIX)objdump
NM    := $(GCCPREFIX)nm

# Native commands
NCC    := gcc $(CC_VER) -pipe
# 79行, -I表示 include 包含路径, -M表示生成以来关系,比如:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
        printf("Hello world\n");
        return 0;
}
那么调用gcc -M hello.c, 则会得到类似如下输出:
hello.o: hello.c /usr/include/stdio.h /usr/include/features.h \
  /usr/include/bits/predefs.h /usr/include/sys/cdefs.h \
  /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \
  /usr/include/gnu/stubs-32.h \
  /usr/lib/gcc/i486-linux-gnu/4.3.5/include/stddef.h \
  /usr/include/bits/types.h /usr/include/bits/typesizes.h \
  /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
  /usr/lib/gcc/i486-linux-gnu/4.3.5/include/stdarg.h \
  /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h \
  /usr/include/stdlib.h /usr/include/sys/types.h /usr/include/time.h \
  /usr/include/endian.h /usr/include/bits/endian.h \
  /usr/include/bits/byteswap.h /usr/include/sys/select.h \
  /usr/include/bits/select.h /usr/include/bits/sigset.h \
  /usr/include/bits/time.h /usr/include/sys/sysmacros.h \
  /usr/include/bits/pthreadtypes.h /usr/include/alloca.h
-MD的意思是将依赖关系保存在相应的.d文件. -Wall表示显示所有warning信息.
NATIVE_CFLAGS := $(CFLAGS) $(DEFS) $(LABDEFS) -I$(TOP) -MD -Wall
TAR    := gtar
PERL    := perl

# 83-91, -O1表示编译器优化级别, 一般有-O0(没有优化)/O1/O2/O3, 如果同时启用多个-O选项,无论有没有级别数字,只有最后一个选项有效。
# -fno-buildin: 不接受没有 __builtin_ 前缀的函数作为内建函数。
# -W前缀表示Warning, -Werror:把warning当做error. 
# -gstabs:生成汇编代码中包含stab格式的调试信息
# Compiler flags
# -fno-builtin is required to avoid refs to undefined functions in the kernel.
# Only optimize to -O1 to discourage inlining, which complicates backtraces.
CFLAGS := $(CFLAGS) $(DEFS) $(LABDEFS) -O1 -fno-builtin -I$(TOP) -MD
CFLAGS += -fno-omit-frame-pointer
CFLAGS += -Wall -Wno-format -Wno-unused -Werror -gstabs -m32
# -fno-tree-ch prevented gcc from sometimes reordering read_ebp() before
# mon_backtrace()'s function prologue on gcc version: (Debian 4.7.2-5) 4.7.2
CFLAGS += -fno-tree-ch

# Add -fno-stack-protector if the option exists.
CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)

# 模拟器类型为i386
# Common linker flags
LDFLAGS := -m elf_i386

# Linker flags for JOS user programs
ULDFLAGS := -T user/user.ld

GCC_LIB := $(shell $(CC) $(CFLAGS) -print-libgcc-file-name)

# Lists that the */Makefrag makefile fragments will add to
OBJDIRS :=

# Make sure that 'all' is the first target
# 默认目标
all:

# Eliminate default suffix rules
.SUFFIXES:

# Delete target files if there is an error (or make is interrupted)
.DELETE_ON_ERROR:

# make it so that no intermediate .o files are ever deleted
.PRECIOUS: %.o $(OBJDIR)/boot/%.o $(OBJDIR)/kern/%.o \
       $(OBJDIR)/lib/%.o $(OBJDIR)/fs/%.o $(OBJDIR)/net/%.o \
       $(OBJDIR)/user/%.o

KERN_CFLAGS := $(CFLAGS) -DJOS_KERNEL -gstabs
USER_CFLAGS := $(CFLAGS) -DJOS_USER -gstabs

# Update .vars.X if variable X has changed since the last make run.
#
# Rules that use variable X should depend on $(OBJDIR)/.vars.X.  If
# the variable's value has changed, this will update the vars file and
# force a rebuild of the rule that depends on it.
$(OBJDIR)/.vars.%: FORCE
    $(V)echo "$($*)" | cmp -s $@ || echo "$($*)" > $@
.PRECIOUS: $(OBJDIR)/.vars.%
.PHONY: FORCE


# 包含的两个makefile, 后续分析
# Include Makefrags for subdirectories
include boot/Makefrag
include kern/Makefrag


# QEMU执行参数
QEMUOPTS = -hda $(OBJDIR)/kern/kernel.img -serial mon:stdio -gdb tcp::$(GDBPORT)
QEMUOPTS += $(shell if $(QEMU) -nographic -help | grep -q '^-D '; then echo '-D qemu.log'; fi)
IMAGES = $(OBJDIR)/kern/kernel.img
QEMUOPTS += $(QEMUEXTRA)

# sed 将 .gdbinit.teml中1234端口替换为GDBPORT导入.gdbinit中.
.gdbinit: .gdbinit.tmpl
    sed "s/localhost:1234/localhost:$(GDBPORT)/" < $^ > $@

gdb:
    gdb -x .gdbinit

# 以下为qemu启动命令.
pre-qemu: .gdbinit

qemu: $(IMAGES) pre-qemu
    $(QEMU) $(QEMUOPTS)

qemu-nox: $(IMAGES) pre-qemu
    @echo "***"
    @echo "*** Use Ctrl-a x to exit qemu"
    @echo "***"
    $(QEMU) -nographic $(QEMUOPTS)

qemu-gdb: $(IMAGES) pre-qemu
    @echo "***"
    @echo "*** Now run 'make gdb'." 1>&2
    @echo "***"
    $(QEMU) $(QEMUOPTS) -S

qemu-nox-gdb: $(IMAGES) pre-qemu
    @echo "***"
    @echo "*** Now run 'make gdb'." 1>&2
    @echo "***"
    $(QEMU) -nographic $(QEMUOPTS) -S

print-qemu:
    @echo $(QEMU)

print-gdbport:
    @echo $(GDBPORT)

# clean, realclean, distclean依次向上依赖, 删除逐渐彻底,distclean回复成直接git下来时的状态.
# For deleting the build
clean:
    rm -rf $(OBJDIR) .gdbinit jos.in qemu.log

realclean: clean
    rm -rf lab$(LAB).tar.gz \
        jos.out $(wildcard jos.out.*) \
        qemu.pcap $(wildcard qemu.pcap.*) \
        myapi.key

distclean: realclean
    rm -rf conf/gcc.mk

ifneq ($(V),@)
GRADEFLAGS += -v
endif

grade:
    @echo $(MAKE) clean
    @$(MAKE) clean || \
      (echo "'make clean' failed.  HINT: Do you have another running instance of JOS?" && exit 1)
    ./grade-lab$(LAB) $(GRADEFLAGS)

git-handin: handin-check
    @if test -n "`git config remote.handin.url`"; then \
        echo "Hand in to remote repository using 'git push handin HEAD' ..."; \
        if ! git push -f handin HEAD; then \
            echo ; \
            echo "Hand in failed."; \
            echo "As an alternative, please run 'make tarball'"; \
            echo "and visit http://pdos.csail.mit.edu/6.828/submit/"; \
            echo "to upload lab$(LAB)-handin.tar.gz.  Thanks!"; \
            false; \
        fi; \
    else \
        echo "Hand-in repository is not configured."; \
        echo "Please run 'make handin-prep' first.  Thanks!"; \
        false; \
    fi

WEBSUB = https://ccutler.scripts.mit.edu/6.828/handin.py

handin: tarball-pref myapi.key
    @curl -f -F file=@lab$(LAB)-handin.tar.gz -F key=\<myapi.key $(WEBSUB)/upload \
        > /dev/null || { \
        echo ; \
        echo Submit seems to have failed.; \
        echo Please go to $(WEBSUB)/ and upload the tarball manually.; }

handin-check:
    @if ! test -d .git; then \
        echo No .git directory, is this a git repository?; \
        false; \
    fi
    @if test "$$(git symbolic-ref HEAD)" != refs/heads/lab$(LAB); then \
        git branch; \
        read -p "You are not on the lab$(LAB) branch.  Hand-in the current branch? [y/N] " r; \
        test "$$r" = y; \
    fi
    @if ! git diff-files --quiet || ! git diff-index --quiet --cached HEAD; then \
        git status; \
        echo; \
        echo "You have uncomitted changes.  Please commit or stash them."; \
        false; \
    fi
    @if test -n "`git ls-files -o --exclude-standard`"; then \
        git status; \
        read -p "Untracked files will not be handed in.  Continue? [y/N] " r; \
        test "$$r" = y; \
    fi

tarball: handin-check
    git archive --format=tar HEAD | gzip > lab$(LAB)-handin.tar.gz

tarball-pref: handin-check
    git archive --prefix=lab$(LAB)/ --format=tar HEAD | gzip > lab$(LAB)-handin.tar.gz

myapi.key:
    @echo Get an API key for yourself by visiting $(WEBSUB)
    @read -p "Please enter your API key: " k; \
    if test `echo -n "$$k" |wc -c` = 32 ; then \
        TF=`mktemp -t tmp.XXXXXX`; \
        if test "x$$TF" != "x" ; then \
            echo -n "$$k" > $$TF; \
            mv -f $$TF $@; \
        else \
            echo mktemp failed; \
            false; \
        fi; \
    else \
        echo Bad API key: $$k; \
        echo An API key should be 32 characters long.; \
        false; \
    fi;

handin-prep:
    @./handin-prep


# This magic automatically generates makefile dependencies
# for header files included from C source files we compile,
# and keeps those dependencies up-to-date every time we recompile.
# See 'mergedep.pl' for more information.
$(OBJDIR)/.deps: $(foreach dir, $(OBJDIRS), $(wildcard $(OBJDIR)/$(dir)/*.d))
    @mkdir -p $(@D)
    @$(PERL) mergedep.pl $@ $^

-include $(OBJDIR)/.deps

always:
    @:

.PHONY: all always \
    handin git-handin tarball tarball-pref clean realclean distclean grade handin-prep handin-check

kern/makefrag 内容如下:

#
# Makefile fragment for JOS kernel.
# This is NOT a complete makefile;
# you must run GNU make in the top-level directory
# where the GNUmakefile is located.
#

OBJDIRS += kern

KERN_LDFLAGS := $(LDFLAGS) -T kern/kernel.ld -nostdlib

# entry.S must be first, so that it's the first code in the text segment!!!
#
# We also snatch the use of a couple handy source files
# from the lib directory, to avoid gratuitous code duplication.
# 需要编译的源文件.
KERN_SRCFILES :=    kern/entry.S \
            kern/entrypgdir.c \
            kern/init.c \
            kern/console.c \
            kern/monitor.c \
            kern/pmap.c \
            kern/env.c \
            kern/kclock.c \
            kern/picirq.c \
            kern/printf.c \
            kern/trap.c \
            kern/trapentry.S \
            kern/sched.c \
            kern/syscall.c \
            kern/kdebug.c \
            lib/printfmt.c \
            lib/readline.c \
            lib/string.c

# Only build files if they exist.
# wildcard:扩展通配符
KERN_SRCFILES := $(wildcard $(KERN_SRCFILES))

# Binary program images to embed within the kernel.
KERN_BINFILES := 

KERN_OBJFILES := $(patsubst %.c, $(OBJDIR)/%.o, $(KERN_SRCFILES))
KERN_OBJFILES := $(patsubst %.S, $(OBJDIR)/%.o, $(KERN_OBJFILES))
KERN_OBJFILES := $(patsubst $(OBJDIR)/lib/%, $(OBJDIR)/kern/%, $(KERN_OBJFILES))

KERN_BINFILES := $(patsubst %, $(OBJDIR)/%, $(KERN_BINFILES))

# How to build kernel object files
# 48-61,把kern下的.c和.S, lib/下的.c编译成.o 放在kern目录下
# V如果是 @ ,表示这句只执行不显示.
$(OBJDIR)/kern/%.o: kern/%.c $(OBJDIR)/.vars.KERN_CFLAGS
    @echo + cc $<
    @mkdir -p $(@D)
    $(V)$(CC) -nostdinc $(KERN_CFLAGS) -c -o $@ $<

$(OBJDIR)/kern/%.o: kern/%.S $(OBJDIR)/.vars.KERN_CFLAGS
    @echo + as $<
    @mkdir -p $(@D)
    $(V)$(CC) -nostdinc $(KERN_CFLAGS) -c -o $@ $<

$(OBJDIR)/kern/%.o: lib/%.c $(OBJDIR)/.vars.KERN_CFLAGS
    @echo + cc $<
    @mkdir -p $(@D)
    $(V)$(CC) -nostdinc $(KERN_CFLAGS) -c -o $@ $<

# Special flags for kern/init
$(OBJDIR)/kern/init.o: override KERN_CFLAGS+=$(INIT_CFLAGS)
$(OBJDIR)/kern/init.o: $(OBJDIR)/.vars.INIT_CFLAGS

# How to build the kernel itself
# 71: 把编译产物用ld命令连接成kernel
# 72: 将kernel反汇编
# 73: 到处符号表
$(OBJDIR)/kern/kernel: $(KERN_OBJFILES) $(KERN_BINFILES) kern/kernel.ld \
      $(OBJDIR)/.vars.KERN_LDFLAGS
    @echo + ld $@
    $(V)$(LD) -o $@ $(KERN_LDFLAGS) $(KERN_OBJFILES) $(GCC_LIB) -b binary $(KERN_BINFILES)
    $(V)$(OBJDUMP) -S $@ > $@.asm
    $(V)$(NM) -n $@ > $@.sym

# How to build the kernel disk image

# 上面的kernel仅是elf文件, 此处将boot,kernel导成镜像.
$(OBJDIR)/kern/kernel.img: $(OBJDIR)/kern/kernel $(OBJDIR)/boot/boot
    @echo + mk $@
    $(V)dd if=/dev/zero of=$(OBJDIR)/kern/kernel.img~ count=10000 2>/dev/null
    $(V)dd if=$(OBJDIR)/boot/boot of=$(OBJDIR)/kern/kernel.img~ conv=notrunc 2>/dev/null
    $(V)dd if=$(OBJDIR)/kern/kernel of=$(OBJDIR)/kern/kernel.img~ seek=1 conv=notrunc 2>/dev/null
    $(V)mv $(OBJDIR)/kern/kernel.img~ $(OBJDIR)/kern/kernel.img

all: $(OBJDIR)/kern/kernel.img

grub: $(OBJDIR)/jos-grub

$(OBJDIR)/jos-grub: $(OBJDIR)/kern/kernel
    @echo + oc $@
    $(V)$(OBJCOPY) --adjust-vma=0x10000000 $^ $@

xxxxx

#
# Makefile fragment for the JOS kernel.
# This is NOT a complete makefile;
# you must run GNU make in the top-level directory
# where the GNUmakefile is located.
#

OBJDIRS += boot

BOOT_OBJS := $(OBJDIR)/boot/boot.o $(OBJDIR)/boot/main.o

$(OBJDIR)/boot/%.o: boot/%.c
    @echo + cc -Os $<
    @mkdir -p $(@D)
    $(V)$(CC) -nostdinc $(KERN_CFLAGS) -Os -c -o $@ $<

$(OBJDIR)/boot/%.o: boot/%.S
    @echo + as $<
    @mkdir -p $(@D)
    $(V)$(CC) -nostdinc $(KERN_CFLAGS) -c -o $@ $<

$(OBJDIR)/boot/main.o: boot/main.c
    @echo + cc -Os $<
    $(V)$(CC) -nostdinc $(KERN_CFLAGS) -Os -c -o $(OBJDIR)/boot/main.o boot/main.c

# 将目标文件连接成elf文件.
# -e start: 入口函数为start.
# -Ttext 0x7C00:代码段入口地址为0x7C00. BIOS加电自检后会自动跳到次地址来执行,相当于开机后软件第一条指令地址.
$(OBJDIR)/boot/boot: $(BOOT_OBJS)
    @echo + ld boot/boot
    $(V)$(LD) $(LDFLAGS) -N -e start -Ttext 0x7C00 -o $@.out $^
    $(V)$(OBJDUMP) -S $@.out >$@.asm
    $(V)$(OBJCOPY) -S -O binary -j .text $@.out $@
    $(V)perl boot/sign.pl $(OBJDIR)/boot/boot