第一步:最简结构

我们从能编译 kernel/main.c 并生成一个简单内核镜像的 Makefile 开始。

假设目标

  • riscv64-unknown-elf-gcc 作为交叉编译器(你可以替换为你需要的架构)。
  • 最终目标是生成 kernel/kernel.elf

项目结构复习

.
├── kernel/
│   ├── entry.S      # 汇编入口
│   ├── main.c       # 内核入口
│   ├── proc.c       # 进程管理
│   └── kernel.ld    # 链接脚本
└── Makefile         # 我们要编写的文件

第一个版本的 Makefile

# 工具链前缀
CROSS = riscv64-unknown-elf-
CC = $(CROSS)gcc
LD = $(CROSS)ld
AS = $(CROSS)as
OBJCOPY = $(CROSS)objcopy

# 编译选项
CFLAGS = -Wall -O2 -fno-pic -march=rv64g -mabi=lp64 -ffreestanding -I.
ASFLAGS = -march=rv64g -mabi=lp64

# 路径
KERNEL_SRC = kernel
KERNEL_OBJS = kernel/entry.o kernel/main.o kernel/proc.o
LINKER_SCRIPT = kernel/kernel.ld

# 最终目标
KERNEL_ELF = kernel/kernel.elf

# 默认目标
all: $(KERNEL_ELF)

# 链接内核
$(KERNEL_ELF): $(KERNEL_OBJS) $(LINKER_SCRIPT)
	$(LD) -T $(LINKER_SCRIPT) -o $@ $(KERNEL_OBJS)

# C 源文件编译成 .o
kernel/%.o: kernel/%.c
	$(CC) $(CFLAGS) -c $< -o $@

# 汇编文件编译
kernel/%.o: kernel/%.S
	$(CC) $(ASFLAGS) -c $< -o $@

# 清理
clean:
	rm -f kernel/*.o kernel/*.elf

每部分解释

工具链与编译器设置

CROSS = riscv64-unknown-elf-

设置工具链前缀,用于调用交叉编译工具。

CC = $(CROSS)gcc
LD = $(CROSS)ld
AS = $(CROSS)as
OBJCOPY = $(CROSS)objcopy

定义用于编译 C、汇编和链接的工具。


编译参数

CFLAGS = -Wall -O2 -fno-pic -march=rv64g -mabi=lp64 -ffreestanding -I.

解释:

  • -Wall 开启所有警告;
  • -O2 优化;
  • -fno-pic 禁用位置无关代码,OS 内核不能用 PIC;
  • -ffreestanding 表示不依赖 libc;
  • -march, -mabi 指定 RISC-V 架构;
  • -I. 添加当前目录为头文件搜索路径。

链接器部分

KERNEL_OBJS = kernel/entry.o kernel/main.o kernel/proc.o

这里写出内核的目标文件。

$(KERNEL_ELF): $(KERNEL_OBJS) $(LINKER_SCRIPT)
	$(LD) -T $(LINKER_SCRIPT) -o $@ $(KERNEL_OBJS)

使用链接脚本 kernel.ld 来控制内核内存布局,生成 kernel.elf。


自动规则:C 和 S 编译

kernel/%.o: kernel/%.c
	$(CC) $(CFLAGS) -c $< -o $@

kernel/%.o: kernel/%.S
	$(CC) $(ASFLAGS) -c $< -o $@

这是自动模式匹配的规则,避免手动列出每个文件。


清理目标

clean:
	rm -f kernel/*.o kernel/*.elf

自动规则(模式规则)详解

在我们的 Makefile 中有如下两段:

kernel/%.o: kernel/%.c
	$(CC) $(CFLAGS) -c $< -o $@

kernel/%.o: kernel/%.S
	$(CC) $(ASFLAGS) -c $< -o $@

这些是 自动规则,也称 模式规则(Pattern Rules),是 Makefile 的核心功能之一,用于简化重复的编译任务。


1. 语法结构

通用形式如下:

target-pattern: dependency-pattern
	recipe

其中:

  • %.o 是目标(output file)的通配符形式,表示任何以 .o 结尾的文件;

  • %.c%.S 是源文件(source file)的通配符形式;

  • $< 是自动变量,代表第一个依赖(这里就是源文件);

  • $@ 是自动变量,代表目标(这里就是目标文件);

  • recipe 是命令行,通常是调用编译器进行编译。


2. 行为示例:C 文件编译

kernel/%.o: kernel/%.c
	$(CC) $(CFLAGS) -c $< -o $@

举例来说:

  • 如果你写了 kernel/main.o 作为某个目标的依赖,

  • 那么 make 会找对应的规则去生成 kernel/main.o

  • 它匹配这个模式规则,把 % 替换成 main,从而推导出:

    • 目标文件是 kernel/main.o

    • 源文件是 kernel/main.c

  • 最终执行命令变成:

      riscv64-unknown-elf-gcc $(CFLAGS) -c kernel/main.c -o kernel/main.o
    

3. 行为示例:汇编文件编译

kernel/%.o: kernel/%.S
	$(CC) $(ASFLAGS) -c $< -o $@

作用基本类似,只是作用在汇编源文件上(.S 大写表示汇编文件中可以包含 C 预处理器指令)。

例如编译 kernel/entry.S,得到 kernel/entry.o,使用命令:

riscv64-unknown-elf-gcc $(ASFLAGS) -c kernel/entry.S -o kernel/entry.o

4. 为什么使用模式规则?

好处是:

  • 避免重复写规则:如果我们有几十个 .c.S 文件,只写一次模式规则就行。

  • 易于扩展:添加新源文件时,只需要修改 KERNEL_OBJS 变量,不需要手动添加规则。

  • 更清晰:模式规则是一种通用的、可重用的构建逻辑,减少冗余代码。