前言
有没有一些朋友和我一样,在做工程开发时,修改了一点文件内容后究竟是要build还是rebuild all?文件与文件之间的互相依赖关系究竟是如何体现的呢?
makefile
简言之,makefile就是表达文件之间的依赖关系的东西。咱们的编译器比较棒,帮咱们实现了这一步,所以很多朋友不了解如何写一个工程的makefile文件。
make是如何发现文件有更新呢? 这是和文件的时间属性有关系。当我们修改了文件后,文件的时间属性则会跟着改变。但是该文件对应编译出的过程文件还是旧的,于是make就认为此文件需要重新编译一次,包括牵连的其他上层文件。
好了啰嗦了这么多,开整。
./ …/
这俩大家应该都不陌生
./表示当前路径
…/表示当前路径的父亲路径
基本格式
目标文件:依赖文件
[tab]命令(一个命令单独占用一行)
伪目标
伪目标:
[tab]命令(一个命令单独占用一行)
看起来好像是缺少依赖文件,但是连依赖文件都没有,那肯定也就没有生成文件了(单独一个c文件编译出来又没人用,有啥用)
为了防止伪目标和目标文件重名,就这样写:
.PHONY:伪目标
伪目标:
[tab]命令(一个命令单独占用一行)
然后执行的时候可以
make 伪目标即可
递归推导
make执行某一操作时发现依赖项没有生成,于是make会找到生成该依赖项的操作,优先执行。(就像是dfs)
定义变量
其实makefile中变量的定义就是定义字符串。定义时
变量名=值(字符串)
使用时
$(变量名)
就非常简单对吧。这东西还是能帮你节省很多书写的。
系统变量
预定义变量 | 含义 |
---|---|
$* | 不包含扩展名的目标文件名称。 |
$+ | 所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件。 |
$< | 第一个依赖文件的名称。 |
$? | 所有的依赖文件,以空格分开,这些依赖文件的修改日期比目标的创建日期晚。 |
$@ | 目标的完整名称。 |
$^ | 所有的依赖文件,以空格分开,不包含重复的依赖文件。 |
$% | 如果目标是归档成员,则该变量表示目标的归档成员名称。例如,如果目标名称为mytarget.so(image.o),则 $@ 为 mytarget.so,而 $% 为 image.o。 |
AR | 归档维护程序的名称,默认值为 ar。 |
ARFLAGS | 归档维护程序的选项。 |
AS | 汇编程序的名称,默认值为 as。 |
ASFLAGS | 汇编程序的选项。 |
CC | C 编译器的名称,默认值为 cc。 |
CCFLAGS | C 编译器的选项。 |
CPP | C 预编译器的名称,默认值为 $(CC) -E。 |
CPPFLAGS | C 预编译的选项。 |
CXX | C++ 编译器的名称,默认值为 g++。 |
CXXFLAGS | C++ 编译器的选项。 |
FC | FORTRAN 编译器的名称,默认值为 f77。 |
FFLAGS | FORTRAN 编译器的选项。 |
实战
以内核工程代码为例。此工程下包含汇编文件与c文件,因此需要将c代码汇编编译,将汇编代码编译。而编译过程中输出的过程文件.obj可以单独存放在一个文件夹下。
BUILD_DIR = ./build
接下来我们决定汇编的编译工具、c的编译工具以及各自的编译配置参数(这些东西重复性很高的,直接定义成变量)配置参数看不懂没关系,知道有这么回事情就行。
AS = nasm
CC = gcc
ASFLAGS = -f elf
CFLAGS = -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes \
-Wmissing-prototypes
各自都通过编译阶段和汇编阶段,那么最后一个阶段自然是将obj文件链接成总的可执行文件,所以链接器工具、链接器参数也需要定义一下:
LD = ld
LDFLAGS = -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map
OK,下一步咱就把c文件编译出来
$(BUILD_DIR)/main.o: kernel/main.c lib/kernel/print.h \
lib/stdint.h kernel/init.h
$(CC) $(CFLAGS) $< -o $@
这一步需要注意的是,CFLAGS是c编译器的参数,这里咱们选择的是gcc编译器,参数中-I表示指定头文件的路径,咱们需要事先定义出来:
LIB = -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog/ -I fs/ -I shell/
这个时候你再回头看看CFLAGS就清楚了。
好了下一步是将汇编文件编译出来:
$(BUILD_DIR)/kernel.o: kernel/kernel.S
$(AS) $(ASFLAGS) $< -o $@
OK接下来是链接这些obj过程文件,输出一个可以直接在内存中运行的bin文件:
$(BUILD_DIR)/kernel.bin: $(OBJS)
$(LD) $(LDFLAGS) $^ -o $@
这里你一定发现了,我是偷懒将生成的obj文件定义成了变量:
OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/init.o $(BUILD_DIR)/interrupt.o \
$(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o
很棒,只剩下最后一步了,就是写点伪目标给make用。我们是自己计划将obj编译阶段的文件放在一个文件夹里,万一文件夹不存在咋办呢?文件夹如果存在了,那得给出一个build伪目标对吧。用户决定删除obj文件,可以吧?再给我一个一劳永逸的伪目标,可以不?
这样一分析,四条伪目标就出现了:
.PHONY : mk_dir build clean all
mk_dir:
if [[ ! -d $(BUILD_DIR) ]];then mkdir $(BUILD_DIR);fi
clean:
cd $(BUILD_DIR) && rm -f ./*
build: $(BUILD_DIR)/kernel.bin
all: mk_dir build
各位不知道有没有发现,build伪目标就很简洁,且居然有依赖项。这里不要惊讶,make发现依赖kernel.bin时就会向上搜索相关的依赖项,逐个生成,所以也就达到了build的效果。
于是用户就可以make all或者make clean,非常开心。