操作系统开发番外篇(1)——make和makefile

发布于 2022-07-31  35 次阅读


我可真是太喜欢操作系统开发这个系列了 :eileen_love: :eileen_love: :eileen_love:

这一部分本来是写在第三章里面的,不过后来发现make工具需要介绍的内容还挺多的,就单独拿出来做个番外篇罢。
我打算在番外篇里面写一些与操作系统开发主线关联性不太强,但是又需要用到的知识,这部分知识可能有同学已经会用了,就可以直接跳过番外篇看正文咯。

make工具可以按照编写的规则来进行自动化的编译和链接,在使用make的时候需要编写和提供makefile(规则文件),下面先简单介绍一下makefile的语法。

基本规则格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 在makefile当中,使用"#"进行注释
# 对于一般编译规则,有如下结构:
目标... : 依赖文件集合...
    命令1
    命令2
    ......
# 需要注意的是,上方的结构每个命令前均有tab缩进
# 静态模式的规则格式如:
<targets...> : <target-pattern> : <prereq-patterns...>
    命令1
    命令2
    ......
# 例:
# bootloader
bin/boot.bin : bootloader/boot.asm
    nasm $< -o $@ $(INCLUDE)
bin/loader.bin : bootloader/loader.asm
    nasm $< -o $@ $(INCLUDE)
# 在上面两个例子中,分别表示通过bootloader/boot.asm生成bin/boot.bin,以及通过bootloader/loader.asm生成bin/loader.bin
# 下面的nasm $< -o $@ $(INCLUDE)即是生成过程中所使用的指令
# 在指令中出现了一些莫名其妙的符号,后续会详细解释

变量及相关操作符

在Makefile中,变量都是存储的字符串,一般程序语言里面的变量赋值都是用“=”,makefile也是一样。不过不同的是,不仅仅只有等号,makefile还有下面几个操作符:

  • "=":只有一个等号,表示使用变量最后一次定义的有效值,与大多数程序语言的使用方法相同。
  • ":=":冒号加等号,表示使用变量最前面定义的有效值。
  • "?=":问号加等号,表示如果前面没有被赋值,则使用当前值,否则使用前面所赋的值。
  • "+=":加号加等号,表示变量追加,在已经定义的变量后面追加新的变量,这一个用法与很多程序语言(如Python)类似。

在使用变量时,需要按照类似$(变量名)这样的格式进行调用,同时,变量的值也可以指向另一个变量,如:v1 = $(v2)

内置变量

在makefile中,有一些内置变量可以直接进行调用,详细有哪些内置变量可以在这里找到。

自动变量

在介绍自动变量以前,还是先来看一段示例代码:

1
2
3
4
5
6
7
# 其实这一段代码和上面基本规则格式那一段是一样的
INCLUDE = -I include
# bootloader
bin/boot.bin : bootloader/boot.asm
    nasm $< -o $@ $(INCLUDE)
bin/loader.bin : bootloader/loader.asm
    nasm $< -o $@ $(INCLUDE)

在上面的代码中,出现了刚才介绍过的变量,即$(INCLUDE),但同时还出现了几个我们不认识的符号,比如$<$@。这两个符号其实也是变量,而且是自动变量。

  1. $@:指代当前目标(构建目标),在上面的示例代码中分别为bin/boot.binbin/loader.bin,即(其中一句)指令实际上是nasm $< -o bin/boot.bin -I include
  2. $<:指代第一个前置条件,在上面的示例代码中分别为bootloader/boot.asmbootloader/loader.asm(注意,只指定第一个,也就是说即便boot.asm后面跟了其他内容或文件,也只表示boot.asm),即(其中一句)指令实际上是nasm bootloader/boot.asm -o bin/boot.bin -I include
  3. $?:指代目标更新的所有前置条件,比如某个编译规则如:bin/loader.bin : bootloader/loader.asm bootloader/loader_new.asm,此时$?仅指代bootloader/loader_new.asm,因为这个条件是最新的
  4. $^:指代所有前置条件,还是以上一个规则为例,此时$^指代的就是bootloader/loader.asm bootloader/loader_new.asm
  5. $+:指代所有前置条件,且不去除重合,这个变量与$^类似,唯一区别是在指代是不会去除重合部分。
  6. $*:指代目标模式中”%“及其之前的部分,例:某个编译规则的目标为dir/a.foo.b,目标规则为a.%.b,则$*匹配dir/a.foo
  7. $(@D) 和 $(@F):这两个变量分别指向$@的目录名和文件名,例:当$@表示bin/boot.bin时,$(@D)表示bin, $(@F)表示boot.bin
  8. $(<D) 和 $(<F):可以结合第2条和第7条来猜一猜这两个变量的使用方法,这里就不多说了。

通配符

makefile的通配符和bash一样,比如*、?、...等。

更加高级的用法

这一部分的用法会比较少接触到,或者是开发后期才会接触到,建议有需要使用到的时候再详细学习。

回声

一般情况下,make会将执行的每条指令先打印出来,再执行,如下:

1
2
3
4
INCLUDE = -I include
bin/boot.bin : bootloader/boot.asm
    # 这里有一句注释
    nasm $< -o $@ $(INCLUDE)

上述规则在执行时,会得到以下结果:

1
2
# 这里有一句注释
nasm $< -o $@ $(INCLUDE)

如果我们不希望在命令执行时将其内容打印出来,可以在前面加@进行屏蔽,例如将上面的第4行修改为@# 这里有一句注释,此时这句注释就不会被打印出来,但是nasm $< -o $@ $(INCLUDE)仍会被打印出来。

条件判断

makefile支持条件判断,使用方法如下:

1
2
3
4
5
ifeq ($(CC),gcc)
    libs=$(libs_for_gcc)
else
    libs=$(normal_libs)
endif

上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。

循环

makefile支持循环结构,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

# 等同于

all:
    for i in one two three; do \
        echo $i; \
    done

上面的代码运行结果为:

1
2
3
one
two
three

函数

makefile中同样支持函数封装,但我们的开发中暂时用不上,后续有需要再进行补充吧。 :ava_bitipop:


学而不思则罔,思而不学则殆