GNUMakefile及注释

发布时间 2024-01-09 11:08:28作者: Pril
#
# 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

# Run 'make V=1' to turn on verbose commands, or 'make V=0' to turn them off.
#make V=1,这个选项能显示出编译过程中的详细信息,
#而make V=0或直接make(V=0是默认值)则是quiet编译模式,只会显示出简单的编译信息。
ifeq ($(V),1)
override V =
endif
ifeq ($(V),0)
override V = @
endif

#If you want make to simply ignore a makefile which does not exist or cannot be remade, with no error message, use the -include directive instead of include
#相当于把.mk的内容都拿过来,.mk中的变量,在Makefile文件中都可以直接使用。
-include conf/lab.mk

-include conf/env.mk

#There is another assignment operator for variables, ‘?=’. This is called a conditional variable assignment operator, because it only has an effect if the variable is not yet defined. 
#./应该是根目录的意思吧
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

# try to infer the correct GCCPREFIX
#在 Makefile 中调用shell 命令有两种形式。第一种是为了获取命令在shell环境中的执行结果。利用 $(shell commmand) 作为基本结构,例如CUR_TIME := $(shell date)
#shell后面的一大串都属于command,我们首先使用ifndef判断GCCPREFIX是否为空,如果为空将执行command,并且执行后的返回值赋予给了GCCPREFIX
#管道符号,是unix一个很强大的功能,符号为一条竖线:"|"。用法:command 1 | command 2,他的功能是把第一个命令command 1执行的结果作为command2的输入传给command 2

#objdump是GCC的套件之一Binutils中的工具命令,可用于查看目标文件的信息,最主要的作用是反汇编。i386-jos-elf指明了其适用的系统。

#2>&1:每个程序在运行后,都会至少打开三个文件描述符,分别是0:标准输入;1:标准输出;2:错误输出。
#默认情况:0:从键盘获得输入;1:输出到屏幕(即控制台);2:输出到屏幕(即控制台)
#2>&1这条命令用到了重定向绑定,采用&可以将两个输出绑定在一起。这条命令的作用是错误输出将和标准输出同用一个文件描述符,说人话就是错误输出将会和标准输出输出到同一个地方。为什么1前面需要&?当没有&时,1会被认为是一个普通的文件,有&表示重定向的目标不是一个文件,而是一个文件描述符。2>是一个整体,表示标准错误输出重定向,重定向至&1,即标准输出,&1是一个文件
#grep (global regular expression) 是Linux命令,用于查找文件里符合条件的字符串或正则表达式。
#/dev/null:在 Linux 系统中,/dev/null 是一个特殊的文件,它被称为“空设备”。它没有任何数据,读取它永远不会产生任何输出,写入它永远不会导致任何数据被存储。/dev/null 起着丢弃数据的作用,可以用于一些需要忽略输出或者输入的场合。
#linux在执行shell命令之前,就会确定好所有的输入输出位置,并且从左到右依次执行重定向的命令,所以>/dev/null 2>&1的作用就是让标准输出重定向到/dev/null中(丢弃标准输出),然后错误输出由于重用了标准输出的描述符,所以错误输出也被定向到了/dev/null中,错误输出同样也被丢弃了。执行了这条命令之后,该条shell命令将不会输出任何信息到控制台,也不会有任何信息输出到文件中。

#使用i386-jos-elf-objdump -b/-m可以指定目标码格式/反汇编目标文件时使用的架构
#i386-jos-elf-objdump -i则是显示所有可以指定的目标码格式/反汇编目标文件时使用的架构
#使用grep查询i386-jos-elf-objdump -i得到的信息中是否存在符合正则表达式'^elf32-i386$$'格式的字符,如果存在则输出'i386-jos-elf-'
#这样做是为了检测下载的binutils工具(objdump是binutils的命令之一)是否支持生成可以运行在elf32-i386的目标代码
#binutils包含有汇编器,可以把汇编语言代码转换为机器码(目标文件)。使用objdump -i 可以检测binutils的汇编器支持生成什么机器平台的目标代码。如果支持生成i386的目标代码,则输出'i386-jos-elf-',否则输出错误信息
#正常运行的话,最后GCCPREFIX的值应该是'i386-jos-elf-'?不是 这里是将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

# try to infer the correct QEMU
#Linux shell which 功能简述which命令的作用是在PATH变量指定的路径中搜索某个系统命令的位置并且返回第一个搜索结果。也就是说使用which命令就可以看到某个系统命令是否存在以及执行的到底是哪一个位置的命令。如果你想知道你的命令放在那里了那么可以用which去查找一下。
#test -x命令,检测判断文件是否存在,并且是否拥有执行权限。
#默认Linux环境下查找qemu命令,如果存在就执行,如果不存在则默认环境为MacOS,直接给出路径并执行。执行失败输出错误信息
QEMU := $(shell if which qemu >/dev/null 2>&1; \
	then echo qemu; exit; \
        elif which qemu-system-i386 >/dev/null 2>&1; \
        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
#expr 是Linux的一个手工命令行计数器
#Linux id命令用于显示用户的ID,以及所属群组的ID。id -u 显示用户ID。
#将GDBPORT设为用户ID % 5000 + 25000
#root 的 UID为0,系统用户的UID范围为1~499,普通用户的UID默认从1000开始顺序编号
#经查,我系统中的当前用户ID为1000 所以GDBPORT = 1000 % 5000 =26000(使用print-gdbport命令可验证)
GDBPORT	:= $(shell expr `id -u` % 5000 + 25000)

#正常运行的话GCCPREFIX:= 'i386-jos-elf-'
CC	:= $(GCCPREFIX)gcc -pipe	#正常运行的话结果是i386-jos-elf-gcc -pipe
AS	:= $(GCCPREFIX)as			#正常运行的话结果是i386-jos-elf-as
AR	:= $(GCCPREFIX)ar			#正常运行的话结果是i386-jos-elf-ar
LD	:= $(GCCPREFIX)ld			#正常运行的话结果是i386-jos-elf-ld
OBJCOPY	:= $(GCCPREFIX)objcopy	#正常运行的话结果是i386-jos-elf-objcopy
OBJDUMP	:= $(GCCPREFIX)objdump	#正常运行的话结果是i386-jos-elf-objdump
NM	:= $(GCCPREFIX)nm			#...参考上面...

# Native commands
NCC	:= gcc $(CC_VER) -pipe
NATIVE_CFLAGS := $(CFLAGS) $(DEFS) $(LABDEFS) -I$(TOP) -MD -Wall
TAR	:= gtar
PERL	:= perl

# 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.
#在我们平时写代码的时候,有可能定义一个函数名字和C语言运行库里面已经存在的函数名冲突的情况,比如说,我定义了一个叫void exit()的函数,如果直接编译,就会报错,但其实是有办法让这个源文件顺利编译的,就是在编译时候添加-fno-builtin选项。
CFLAGS := $(CFLAGS) $(DEFS) $(LABDEFS) -O1 -fno-builtin -I$(TOP) -MD
CFLAGS += -fno-omit-frame-pointer
CFLAGS += -std=gnu99
CFLAGS += -static
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)

# Common linker flags
LDFLAGS := -m elf_i386

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

#正常运行的话 CC=i386-jos-elf-gcc -pipe(前面的定义,i386-jos-elf-gcc作为命令,-pipe作为参数)
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
#OBJDIR:=obj
#.PRECIOUS标记的中间文件在命令执行结束后不会被删除
.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


# Include Makefrags for subdirectories
include boot/Makefrag
include kern/Makefrag

#OBJDIR:=obj
QEMUOPTS = -drive file=$(OBJDIR)/kern/kernel.img,index=0,media=disk,format=raw -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 -n参数说明:Do not execute commands found in any initialization files 不执行任何初始化文件中的命令
#gdb -x file参数说明:Execute commands from file file. The contents of this file is evaluated exactly as the source command would.
gdb:
	gdb -n -x .gdbinit

pre-qemu: .gdbinit


#Make 命令教程:https://ruanyifeng.com/blog/2015/02/make.html
#Makefile文件由一系列规则(rules)构成。每条规则的形式如下。

#<target> : <prerequisites> 
#[tab]  <commands>

#上面第一行冒号前面的部分,叫做"目标"(target),冒号后面的部分叫做"前置条件"(prerequisites);第二行必须由一个tab键起首,后面跟着"命令"(commands)。
#"目标"是必需的,不可省略;"前置条件"和"命令"都是可选的,但是两者之中必须至少存在一个。
#每条规则就明确两件事:构建目标的前置条件是什么,以及如何构建。下面就详细讲解,每条规则的这三个组成部分。

#一个目标(target)就构成一条规则。目标通常是文件名,指明Make命令所要构建的对象。除了文件名,目标还可以是某个操作的名字,这称为"伪目标"(phony target)
#例如:
#clean:
#      rm *.o
#上面代码的目标是clean,它不是文件名,而是一个操作的名字,属于"伪目标 ",作用是删除对象文件。



#$(QEMU):调用前面定义的检测QEMU的函数,正常执行的话其返回值是qemu(或者qemu-system-i386)
#若$(QEMU)的返回值是qemu,那么我们得到qemu $(QEMUOPTS),由于$(QEMUOPTS)在前面已经定义过(可以使用下面的print-qemu验证)
#所以在代码正常运行的情况下,当我们执行$(QEMU) $(QEMUOPTS)的时候,实际上是在执行 qemu -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::$(GDBPORT)
#qemu -drive 命令说明:Define a new drive.
    #-drive index参数说明:This option defines where the drive is connected by using an index in the list of available connectors of a given interface type.
    #-drive media参数说明:This option defines the type of the media: disk or cdrom.
    #-drive format参数说明:Specify which disk format will be used rather than detecting the format. Can be used to specify format=raw to avoid interpreting an untrusted format header.
#qemu -serial 命令说明:QEMU 具有模拟输入设备的能力, 在 QEMU 的命令行接口, 提供了 -serial 参数供用户设置把虚拟的串口重定向到哪里.
    #-serial mon:stdio:-serial 命令用于重定向串口,串口的主要作用于主机与虚拟机的通信,-serial mon:stdio将串口定向到标准输入输出(方便调试), 同时可以使用 Ctrl + a 然后按 c 访问 Monitor 终端
    #-s -S或-gdb tcp::1234 -S选项用于启动gdb服务,启动后qemu不立即运行guest,而是等待主机gdb发起连接,此时使用gdb输入target remote:1234可以进行相关调试,与真机调试无异。
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)

# 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://6828.scripts.mit.edu/2018/handin.py

handin: tarball-pref myapi.key
	@SUF=$(LAB); \
	test -f .suf && SUF=`cat .suf`; \
	curl -f -F file=@lab$$SUF-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 -s; \
		echo; \
		echo "You have uncomitted changes.  Please commit or stash them."; \
		false; \
	fi
	@if test -n "`git status -s`"; then \
		git status -s; \
		read -p "Untracked files will not be handed in.  Continue? [y/N] " r; \
		test "$$r" = y; \
	fi

UPSTREAM := $(shell git remote -v | grep "pdos.csail.mit.edu/6.828/2018/jos.git (fetch)" | awk '{split($$0,a," "); print a[1]}')

tarball-pref: handin-check
	@SUF=$(LAB); \
	if test $(LAB) -eq 3 -o $(LAB) -eq 4; then \
		read -p "Which part would you like to submit? [a, b, c (c for lab 4 only)]" p; \
		if test "$$p" != a -a "$$p" != b; then \
			if test ! $(LAB) -eq 4 -o ! "$$p" = c; then \
				echo "Bad part \"$$p\""; \
				exit 1; \
			fi; \
		fi; \
		SUF="$(LAB)$$p"; \
		echo $$SUF > .suf; \
	else \
		rm -f .suf; \
	fi; \
	git archive --format=tar HEAD > lab$$SUF-handin.tar; \
	git diff $(UPSTREAM)/lab$(LAB) > /tmp/lab$$SUF-diff.patch; \
	tar -rf lab$$SUF-handin.tar /tmp/lab$$SUF-diff.patch; \
	gzip -c lab$$SUF-handin.tar > lab$$SUF-handin.tar.gz; \
	rm lab$$SUF-handin.tar; \
	rm /tmp/lab$$SUF-diff.patch; \

myapi.key:
	@echo Get an API key for yourself by visiting $(WEBSUB)/
	@read -p "Please enter your API key: " k; \
	if test `echo "$$k" |tr -d '\n' |wc -c` = 32 ; then \
		TF=`mktemp -t tmp.XXXXXX`; \
		if test "x$$TF" != "x" ; then \
			echo "$$k" |tr -d '\n' > $$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