第一步:最简结构
我们从能编译 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变量,不需要手动添加规则。 -
更清晰:模式规则是一种通用的、可重用的构建逻辑,减少冗余代码。