朋友供稿,是南京大学计算机系统基础的小型项目(PA)第一部分,也就是PA1。

RTFSC

框架初探

NEMU由四部分构成:monitor, CPU, memory, 设备
monitor负责和Linux交互,调试的功能

kconfig

这一块是简单讲解 kconfig 的作用。

在大型项目中,有很多可配置选项,配置选项之间会有依赖和关联。比如我们玩的游戏设置:

  • 图形质量:(高 / 低)
  • 光线追踪:(开 / 关) —— 这个选项只有在“高”画质时才能开启
    而这些设置,在代码里面通常要用来实现,比如:
1
2
3
4
// config.h
#define CONFIG_DEBUG 1
#define CONFIG_WELCOME_MSG "Welcome to NEMU!"

如果手动配置这些配置文件宏,会非常麻烦且容易出错。因此引入新的软件 kconfig,具体描述见讲义。
nemu的步骤 make menuconfig

  1. 定义 (Kconfig)

    • 开发者在 nemu/Kconfig 文件中定义了所有可配置的选项。
    • 包含了各种选项和各种依赖。
  2. 交互 (mconf)

    • 运行 make menuconfigmake 会启动 mconf 程序。
    • mconf 读取 nemu/Kconfig,生成交互式菜单
  3. 保存 (.config)

    • 在菜单中进行交互设置(启用 CONFIG_DEBUG,关闭 CONFIG_CACHE 等)。
    • 退出保存时,mconf 将选择结果写入 nemu/.config 文件。
  4. 翻译 (conf)

    • make 继续执行,自动运行 conf 程序。
    • conf 读取 nemu/Kconfig(为了了解所有选项的默认值和类型)和 nemu/.config(为了了解您的选择)。
    • conf 翻译这些设置,生成两个核心文件。
  5. 生成 (autoconf.h / auto.conf)

    • conf 生成 nemu/include/generated/autoconf.h,里面是给 C 代码用的
    • conf 生成 nemu/include/config/auto.conf,里面是给 Makefile 用的变量

例如:

  1. nemu/Kconfig :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 定义一个布尔型(bool)选项,叫 DEBUG
    config DEBUG
    bool "Enable debugging features"
    default n # 默认不开启
    help
    This enables extra printf messages for debugging.

    # 定义一个字符串(string)选项,叫 WELCOME_MSG
    config WELCOME_MSG
    string "The welcome message to show on boot"
    depends on DEBUG # 关键:这个选项只在 DEBUG=y 时才出现!
    default "Hello"
    • config DEBUG: 定义了一个叫 DEBUG 的选项,它是个开关(bool)。

    • config WELCOME_MSG: 定义了一个叫 WELCOME_MSG 的选项,它是个字符串(string)。

    • depends on DEBUG: 建立依赖关系。这说明:只有当 DEBUG 被选中后,WELCOME_MSG 这个选项才允许被设置。

  2. mconf :

    • 读取 nemu/Kconfig,显示一个菜单:
    1
    [ ] Enable debugging features
    • 选中:
    1
    [*] Enable debugging features
    • 因为 depends on DEBUG 被满足了,mconf 显示了新选项:
    1
    2
    [*] Enable debugging features
    ("Hello") The welcome message to show on boot
    • 修改了欢迎语为 "Welcome to NEMU!"
    • 保存并退出,把结果写入 nemu/.config 文件。
  3. nemu/.config (mconf 的输出):

    1
    2
    3
    4
    5
    #
    # Automatically generated file; DO NOT EDIT.
    #
    CONFIG_DEBUG=y
    CONFIG_WELCOME_MSG="Welcome to NEMU!"
  4. conf 程序(翻译 .config ):

    • 生成给 C 代码的 autoconf.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // nemu/include/generated/autoconf.h
    /*
    * Automatically generated C config header
    * DO NOT EDIT
    */
    #define CONFIG_DEBUG 1
    #define CONFIG_WELCOME_MSG "Welcome to NEMU!"

    /* (注意:bool 类型的 'y' 被翻译成了 C 语言的 '1') */
    • 生成给 Makefile 的 auto.conf
    1
    2
    3
    4
    5
    6
    7
    8
    # nemu/include/config/auto.conf
    #
    # Automatically generated make config: DO NOT EDIT
    #
    CONFIG_DEBUG=y
    CONFIG_WELCOME_MSG="Welcome to NEMU!"

    /* (注意:这里的内容和 .config 基本一样,格式是 make 变量) */
  5. 项目文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <stdio.h>
    // 1. 包含 Kconfig 生成的 C 语言宏
    #include "generated/autoconf.h"

    int main() {

    // 2. 使用布尔型配置 (DEBUG) 来控制代码
    #ifdef CONFIG_DEBUG
    // 这部分代码只有在 `make menuconfig` 中
    // 勾选了 DEBUG 时才会被编译进去
    printf("Debug mode is ON.\n");

    // 3. 使用字符串配置 (WELCOME_MSG)
    printf("Message: %s\n", CONFIG_WELCOME_MSG);
    #else
    // 如果没勾选 DEBUG,则只编译这部分
    printf("Release mode.\n");
    #endif

    return 0;
    }

游戏的配置
实际上游戏是运行时 (Run-Time) 配置,一些软件比如OS内核是编译时 (Compile-Time) 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
// (游戏启动时从 settings.ini 读取设置)
int graphics_quality = read_setting("quality"); // 假设读到 3 (高)

void draw_game_frame() {
if (graphics_quality == 3) {
draw_high_quality_shadows();
draw_raytracing();
} else if (graphics_quality == 2) {
draw_medium_quality_shadows();
} else {
draw_low_quality_shadows();
}
}

内核内的软件需要性能、体积、稳定,如果采用上述方法,会带来几个问题:

  • if 指令本身会消耗 CPU 周期,会污染 CPU 缓存。当这个检查每秒发生一亿次时,累积的开销是巨大的。
  • 编译之后体积巨大,比如在服务器上的系统完全不需要蓝牙功能,编译进去造成冗余。

Makefile

filelist

用来管理需要编译的源文件,使用方法如下:

  1. 在 kconfig 步骤生成 Make 变量,例如:
    • 如果选中了 TARGET_AM,它会生成:CONFIG_TARGET_AM=y
    • 如果没选中,它会生成:CONFIG_TARGET_AM=n (或者干脆不定义)
  2. filelist.mk 中维护四个变量:
    • SRCS-y - 参与编译的源文件的候选集合
    • SRCS-BLACKLIST-y - 不参与编译的源文件的黑名单集合
    • DIRS-y - 参与编译的目录集合, 该目录下的所有文件都会被加入到SRCS-y
    • DIRS-BLACKLIST-y - 不参与编译的目录集合, 该目录下的所有文件都会被加入到SRCS-BLACKLIST-y
  3. 实际使用过程,定义的变量为 DIRS-BLACKLIST-$(CONFIG_TARGET_AM) ,make 程序读取过程中自动展开。

编译和链接

Makefile 基本规则:

1
2
目标 (Target): 依赖 (Prerequisites)
<TAB> 命令 (Command)
  • 目标: 想要生成的文件,比如可执行文件 program 或目标文件 main.o
  • 依赖: 生成这个“目标”所需要的文件
  • 命令: 从“依赖”生成“目标”的具体步骤。
  • 变量: 通常在文件顶部用大写字母定义,使用 $(VAR) 来引用。(类似于c语言的宏定义,直接字符替换)

详细拆解编译规则:

1
2
3
4
5
$(OBJ_DIR)/%.o: %.c
@echo + CC $<
@mkdir -p $(dir $@)
@$(CC) $(CFLAGS) -c -o $@ $<
$(call call_fixdep, $(@:.o=.d), $@)
  • 规则:$(OBJ_DIR)/%.o: %.c

    • $(OBJ_DIR) 在初始化已经自动设定
    • 需要生成 $(OBJ_DIR)/%.o 文件(例如 build/src/main.o),需要同名的.c 文件例如 (src/main.c)。
    • % 是一个通配符,它会匹配两个文件名中相同的部分(既 src/main)。
  • 自动变量:

    • $@:目标 - Target
    • $<:第一个依赖 - First Prerequisite
  • 命令1:@echo + CC $<

    • @ (静音符):只执行不打印
      • 如果没有 @make 会先打印 echo + CC src/utils/timer.c,然后再执行它,导致输出 + CC src/utils/timer.c,非常啰嗦。
      • + CC $< (打印的内容):
        • + CC 是一个固定的字符串,让输出更美观。
        • $< 是一个第一个依赖(例如在构建 timer.o 时,$< 的值就是 src/utils/timer.c)。
  • 命令2:@mkdir -p $(dir $@)

    • $(dir $@) (要创建的目录):
      • $@ 是目标名,例如 build/obj/src/utils/timer.o
        • $(dir ...) 是一个 make 内置函数,用于提取路径中的目录部分。例如 $(dir build/obj/src/utils/timer.o) 的计算结果是 build/obj/src/utils/
  • 命令3:@$(CC) $(CFLAGS) -c -o $@ $<

    • $(CC):代表 C 编译器,一般是 gcc
    • $(CFLAGS):代表所有编译选项(C Flags),例如 -O2 -Wall -Werror -I... 等。
    • -c:编译器选项,告诉 gcc:“只编译,不链接”。即,把 .c 文件编译成 .o 目标文件。
    • -o $@:
      • -o 是编译器的输出文件选项。
      • $@ 在这里是输出文件名,例如 -o build/obj/src/utils/timer.o
    • $<:在这里是输入源文件,例如 src/utils/timer.c
  • 命令4:$(call call_fixdep, $(@:.o=.d), $@)

    • 修复优化依赖关系