uCore-实验第 1 章 - 应用程序与基本执行环境

了解系统调用

操作系统的系统调用(syscall)是操作系统提供给应用程序使用的一种接口。它允许应用程序通过向操作系统发送请求,来执行一些必须由操作系统来完成的任务,例如读取文件、创建进程、分配内存等。

通俗地说,可以把操作系统看作一个巨大的服务员,而应用程序就像是顾客。应用程序不能直接访问硬件或执行特权操作,因为这样可能会导致系统不稳定或不安全。所以,应用程序需要通过系统调用来与操作系统进行交互,请求操作系统代表它完成某些任务。

当应用程序需要操作系统执行特定的功能时,它会调用适当的系统调用函数,并传递参数给它。然后操作系统会接收到这个请求,并根据请求的类型和参数来执行相应的操作。完成后,操作系统会将执行结果返回给应用程序。

在 RISC-V 架构中,系统调用是通过使用特定的指令来实现的。具体来说,RISC-V 架构提供了一个称为 ecall(environment call)的指令来触发系统调用。

要使用 syscall,在 RISC-V 汇编代码中可以通过以下步骤来完成:

  1. 将系统调用编号(syscall number)放入寄存器 a7 中,该编号对应于所需的系统调用功能。
  2. 将系统调用所需的参数放入其他相应的寄存器中。例如,参数传递给文件读取系统调用可能需要将文件描述符放入 a0 寄存器,缓冲区地址放入 a1 寄存器,以及读取的字节数放入 a2 寄存器。
  3. 执行 ecall 指令。这会触发操作系统处理当前运行的程序的系统调用请求。
  4. 操作系统接收到系统调用请求后,根据寄存器 a7 中的系统调用编号和其他寄存器中的参数来执行相应的操作。
  5. 当操作系统完成系统调用请求时,它将结果放入适当的寄存器中,通常是 a0 寄存器。
  6. 程序继续执行,可以检查结果并进行后续的处理。

需要注意的是,具体的系统调用编号以及参数的传递方式会根据操作系统的实现而有所不同。所以在编写 RISC-V 汇编代码时,需要参考操作系统的相关文档来了解具体的系统调用接口和参数传递方式。

makr run 之后发生了什么?

当执行make run命令后,以下是运行流程的概述:

  1. 内核代码编译:执行make run会触发 Makefile 中的相应规则,从而编译生成内核(kernel)二进制文件。

  2. 加载 kernel 并启动 QEMU:根据 QEMUOPTS 变量指定的参数,QEMU 加载生成的 kernel 二进制文件,并启动模拟器。

  3. 引导代码执行:在模拟器启动后,CPU 的通用寄存器被清零,程序计数器(PC)指向 0x1000 的位置,这里有硬件固化的一小段引导代码。该引导代码会迅速跳转到 0x80000000 处的 RustSBI(Rust Supervisor Binary Interface)。

  4. RustSBI 完成硬件初始化:RustSBI 是一个用于与操作系统进行交互的接口层。在跳转到 RustSBI 之后,它会完成必要的硬件初始化工作。

  5. 执行操作系统第一条指令:RustSBI 在完成硬件初始化后,会跳转到 kernel 二进制文件所在内存位置 0x80200000 处,并开始执行我们操作系统的第一条指令。

综上所述,执行make run命令会完成内核的编译和加载,启动 QEMU 虚拟机,并经过引导代码和 RustSBI 的处理,最终开始执行操作系统的第一条指令。

了解链接脚本

# kernel.ld
BASE_ADDRESS = 0x80200000;
SECTIONS
{
   . = BASE_ADDRESS;
   skernel = .;

   stext = .;
   .text : {
      *(.text.entry)   # 第一行代码
      *(.text .text.*)
   }

   ...
}

kernel.ld 中的 BASE_ADDRESS = 0x80200000 指定了内核的加载地址,这个地址哪来的?

以下内容摘自参考rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档

在 Qemu 模拟的 virt 硬件平台上,物理内存的起始物理地址为 0x80000000,物理内存的默认大小为 128MiB,它可以通过 -m 选项进行配置。如果使用默认配置的 128MiB 物理内存则对应的物理地址区间为 [0x80000000,0x88000000) 。如果使用上面给出的命令启动 Qemu,那么在 Qemu 开始执行任何指令之前,首先把两个文件加载到 Qemu 的物理内存中:即作把作为 bootloader 的 rustsbi-qemu.bin 加载到物理内存以物理地址 0x80000000 开头的区域上,同时把内核镜像 os.bin 加载到以物理地址 0x80200000 开头的区域上。

为什么加载到这两个位置呢?这与 Qemu 模拟计算机加电启动后的运行流程有关。一般来说,计算机加电之后的启动流程可以分成若干个阶段,每个阶段均由一层软件或 固件 负责,每一层软件或固件的功能是进行它应当承担的初始化工作,并在此之后跳转到下一层软件或固件的入口地址,也就是将计算机的控制权移交给了下一层软件或固件。Qemu 模拟的启动流程则可以分为三个阶段:第一个阶段由固化在 Qemu 内的一小段汇编程序负责;第二个阶段由 bootloader 负责;第三个阶段则由内核镜像负责。

第一阶段:将必要的文件载入到 Qemu 物理内存之后,Qemu CPU 的程序计数器(PC, Program Counter)会被初始化为 0x1000,因此 Qemu 实际执行的第一条指令位于物理地址 0x1000,接下来它将执行寥寥数条指令并跳转到物理地址 0x80000000 对应的指令处并进入第二阶段。从后面的调试过程可以看出,该地址 0x80000000 被固化在 Qemu 中,作为 Qemu 的使用者,我们在不触及 Qemu 源代码的情况下无法进行更改。

第二阶段:由于 Qemu 的第一阶段固定跳转到 0x80000000,我们需要将负责第二阶段的 bootloader rustsbi-qemu.bin 放在以物理地址 0x80000000 开头的物理内存中,这样就能保证 0x80000000 处正好保存 bootloader 的第一条指令。在这一阶段,bootloader 负责对计算机进行一些初始化工作,并跳转到下一阶段软件的入口,在 Qemu 上即可实现将计算机控制权移交给我们的内核镜像 os.bin。这里需要注意的是,对于不同的 bootloader 而言,下一阶段软件的入口不一定相同,而且获取这一信息的方式和时间点也不同:入口地址可能是一个预先约定好的固定的值,也有可能是在 bootloader 运行期间才动态获取到的值。我们选用的 RustSBI 则是将下一阶段的入口地址预先约定为固定的 0x80200000,在 RustSBI 的初始化工作完成之后,它会跳转到该地址并将计算机控制权移交给下一阶段的软件——也即我们的内核镜像。

第三阶段:为了正确地和上一阶段的 RustSBI 对接,我们需要保证内核的第一条指令位于物理地址 0x80200000 处。为此,我们需要将内核镜像预先加载到 Qemu 物理内存以地址 0x80200000 开头的区域上。一旦 CPU 开始执行内核的第一条指令,证明计算机的控制权已经被移交给我们的内核,也就达到了本节的目标。

以上过程是 QEMU 中的启动流程,真实计算机的加电启动流程大致如下:
第一阶段:加电后 CPU 的 PC 寄存器被设置为计算机内部只读存储器(ROM,Read-only Memory)的物理地址,随后 CPU 开始运行 ROM 内的软件。我们一般将该软件称为固件(Firmware),它的功能是对 CPU 进行一些初始化操作,将后续阶段的 bootloader 的代码、数据从硬盘载入到物理内存,最后跳转到适当的地址将计算机控制权转移给 bootloader。它大致对应于 Qemu 启动的第一阶段,即在物理地址 0x1000 处放置的若干条指令。可以看到 Qemu 上的固件非常简单,因为它并不需要负责将 bootloader 从硬盘加载到物理内存中,这个任务此前已经由 Qemu 自身完成了。
第二阶段:bootloader 同样完成一些 CPU 的初始化工作,将操作系统镜像从硬盘加载到物理内存中,最后跳转到适当地址将控制权转移给操作系统。可以看到一般情况下 bootloader 需要完成一些数据加载工作,这也就是它名字中 loader 的来源。它对应于 Qemu 启动的第二阶段。在 Qemu 中,我们使用的 RustSBI 功能较弱,它并没有能力完成加载的工作,内核镜像实际上是和 bootloader 一起在 Qemu 启动之前加载到物理内存中的。
第三阶段:控制权被转移给操作系统。由于篇幅所限后面我们就不再赘述了。
值得一提的是,为了让计算机的启动更加灵活,bootloader 目前可能非常复杂:它可能也分为多个阶段,并且能管理一些硬件资源,从复杂性上它已接近一个传统意义上的操作系统。

终端是如何控制颜色的?

enum LOG_COLOR {
	RED = 31,
	GREEN = 32,
	BLUE = 34,
	GRAY = 90,
	YELLOW = 93,
};

#if defined(USE_LOG_ERROR)
#define errorf(fmt, ...)                                               \
	do {                                                               \
		int tid = threadid();                                          \
		printf("\x1b[%dm[%s %d]" fmt "\x1b[0m\n", RED, "ERROR", tid,   \
		       ##__VA_ARGS__);                                         \
	} while (0)
#else

ANSI 转义码是一种用于控制终端输出的特殊字符序列。它们由\x1b(或\033)开头,后面跟着一系列数字和分号组成。

ANSI 转义码中的数字部分用于指定不同的控制操作,如设置文本颜色、背景颜色、光标位置等等。其中,用于设置颜色的转义码包括三个主要的部分:\x1b[颜色代码m

具体来说,\x1b[表示开始使用控制序列,接下来的数字代表不同的颜色代码,最后的m表示结束控制序列。例如,\x1b[31m表示将文本颜色设置为红色,而\x1b[0m用于重置所有属性为默认值。

当终端遇到这样的转义序列时,它会解析并执行相应的控制操作,从而实现对文本颜色、背景颜色和其他属性的控制。

需要注意的是,不同的终端可能支持不同的 ANSI 转义码,并且不同操作系统也可能有不同的实现。因此,在编写使用 ANSI 转义码的代码时,建议先测试并确保其在目标终端上正常工作。

更多详细解释可以参考文章:终端颜色控制 - 简书

应用程序输出字符会调用 SBI 服务,SBI 中发生了什么?

因为对 Rust 语言不熟悉,所以这里的分析是基于 C 语言的 OpenSBI 来分析的,他们的逻辑是一样的。如果有熟悉 Rust 的可以查看 RustSBI 源码

根据指导书中的解释以及阅读代码,我们知道调用了 printf 最终实际上是调用了 sbi_call。那么 sbi_call 是如何实现的呢?因为我是做驱动开发以及固件开发的,也经常需要使用 OpenSBI,所想多问一句,OpenSBI 是如何实现的呢?OpenSBI 是如何提供服务的呢?它是如何打印出字符的呢?

内核中的 SBI 调用

我们先看一下内核中的 sbi_call 都做了写啥。

// uCore-Tutorial-Code-2023S/os/sbi.c
const uint64 SBI_CONSOLE_PUTCHAR = 1;

void console_putchar(int c)
{
	sbi_call(SBI_CONSOLE_PUTCHAR, c, 0, 0);
}
// uCore-Tutorial-Code-2023S/os/sbi.c
int inline sbi_call(uint64 which, uint64 arg0, uint64 arg1, uint64 arg2)
{
    // 使用寄存器变量来保存参数值和系统调用编号
    register uint64 a0 asm("a0") = arg0;  // 将 'arg0' 的值保存在寄存器 'a0' 中
    register uint64 a1 asm("a1") = arg1;  // 将 'arg1' 的值保存在寄存器 'a1' 中
    register uint64 a2 asm("a2") = arg2;  // 将 'arg2' 的值保存在寄存器 'a2' 中
    register uint64 a7 asm("a7") = which; // 将 'which' 的值保存在寄存器 'a7' 中
    // 内联汇编代码使用 ecall 指令进行系统调用
    asm volatile(
        "ecall"  // 使用 ecall 指令进行系统调用
        // 在这段代码中,指令 "ecall" 的输入参数是寄存器 a0 a1 a2 和 a7,输出参数是寄存器 a0
        : "=r"(a0)  // 输出操作数:将返回值存储在变量 'a0' 中
        : "r"(a0), "r"(a1), "r"(a2), "r"(a7)  // 输入操作数:传递参数和系统调用编号
        : "memory"  //  "memory" 标志告诉编译器,这条指令可能会修改内存中的数据,需要进行内存屏障操作来保证数据的正确性。 
    );
    return a0;  // 返回存储在变量 'a0' 中的值
}

那么 OpenSBI 如何提供服务?在include/sbi/sbi_ecall.h这种定义了每个ecall服务全局变量。

//include/sbi/sbi_ecall.h
extern struct sbi_ecall_extension ecall_base;
extern struct sbi_ecall_extension ecall_legacy;
extern struct sbi_ecall_extension ecall_time;
extern struct sbi_ecall_extension ecall_rfence;
extern struct sbi_ecall_extension ecall_ipi;
extern struct sbi_ecall_extension ecall_vendor;
extern struct sbi_ecall_extension ecall_hsm;
extern struct sbi_ecall_extension ecall_srst;

lib/sbi/sbi_ecall.c中注册了所有的ecall服务,并将其加到链表ecall_exts_list中。

int sbi_ecall_init(void)
{
	int ret;
	struct sbi_ecall_extension *ext;
	unsigned long i;

	for (i = 0; i < sbi_ecall_exts_size; i++) {
		ext = sbi_ecall_exts[i];
		ret = sbi_ecall_register_extension(ext);
		if (ret)
			return ret;
	}

	return 0;
}
int sbi_ecall_register_extension(struct sbi_ecall_extension *ext)
{
	struct sbi_ecall_extension *t;

	if (!ext || (ext->extid_end < ext->extid_start) || !ext->handle)
		return SBI_EINVAL;

	sbi_list_for_each_entry(t, &ecall_exts_list, head) {
		unsigned long start = t->extid_start;
		unsigned long end = t->extid_end;
		if (end < ext->extid_start || ext->extid_end < start)
			/* no overlap */;
		else
			return SBI_EINVAL;
	}

	SBI_INIT_LIST_HEAD(&ext->head);
	sbi_list_add_tail(&ext->head, &ecall_exts_list);

	return 0;
}

/**
 * Iterate over list of given type
 * @param pos the type * to use as a loop cursor.
 * @param head the head for your list.
 * @param member the name of the list_struct within the struct.
 */
#define sbi_list_for_each_entry(pos, head, member) \
	for (pos = sbi_list_entry((head)->next, typeof(*pos), member);	\
	     &pos->member != (head); 	\
	     pos = sbi_list_entry(pos->member.next, typeof(*pos), member))

那么服务 id 如何和相对应的服务绑定的呢?以ecall_time为例,查看其结构体原型struct sbi_ecall_extension

// include/sbi/sbi_ecall.h: 23
struct sbi_ecall_extension {
	struct sbi_dlist head;
	unsigned long extid_start;
	unsigned long extid_end;
	int (* probe)(unsigned long extid, unsigned long *out_val);
	int (* handle)(unsigned long extid, unsigned long funcid,
		       const struct sbi_trap_regs *regs,
		       unsigned long *out_val,
		       struct sbi_trap_info *out_trap);
};

可以看到有 extid_startextid_endhandle

目前 OpenSBI 逐步将每个服务的实现都放在了lib/sbi单独文件中,以ecall_time为例,其实现在lib/sbi/sbi_ecall_time.c中。单独为其绑定回调处理函数sbi_ecall_time_handler。但是还有很多服务的实现还是放在了lib/sbi/sbi_ecall_legacy.c中,后续应该会逐步迁移。我们上文使用的SBI_CONSOLE_PUTCHAR服务就是在这里实现的。

// lib/sbi/sbi_ecall_legacy.c
struct sbi_ecall_extension ecall_legacy = {
	.extid_start = SBI_EXT_0_1_SET_TIMER,
	.extid_end = SBI_EXT_0_1_SHUTDOWN,
	.handle = sbi_ecall_legacy_handler,
};

static int sbi_ecall_legacy_handler(unsigned long extid, unsigned long funcid,
				    const struct sbi_trap_regs *regs,
				    unsigned long *out_val,
				    struct sbi_trap_info *out_trap)
{
	int ret = 0;
	struct sbi_tlb_info tlb_info;
	u32 source_hart = current_hartid();
	ulong hmask = 0;

	switch (extid) {
	case SBI_EXT_0_1_SET_TIMER:
		sbi_timer_event_start((u64)regs->a0);
		break;
	case SBI_EXT_0_1_CONSOLE_PUTCHAR:
		sbi_putc(regs->a0);
		break;
	case SBI_EXT_0_1_CONSOLE_GETCHAR:
		ret = sbi_getc();
		break;
	// ...
	};

	return ret;
}

这就把 id 与相应的服务函数绑定。一个extid对应一个handler

我们可以在找到SBI_EXT_0_1_CONSOLE_PUTCHAR的值,是与 Linux 内核里定义的值是一致的。

// include/sbi/sbi_ecall_interface.h
/* SBI Extension IDs */
#define SBI_EXT_0_1_CONSOLE_PUTCHAR		0x1

ecall 服务调用流程

  1. firmware/fw_base.S 中注册了 Machine Modetrap handler,即 sbi_trap_handler

    _start_warm:
        /* Setup trap handler */
        la	a4, _trap_handler
        csrw	CSR_MTVEC, a4  /* CSR_MTVEC = _trap_handler */
    
    _trap_handler:
        TRAP_SAVE_AND_SETUP_SP_T0
    
        TRAP_SAVE_MEPC_MSTATUS 0
    
        TRAP_SAVE_GENERAL_REGS_EXCEPT_SP_T0
    
        TRAP_CALL_C_ROUTINE
    
        TRAP_RESTORE_GENERAL_REGS_EXCEPT_SP_T0
    
        TRAP_RESTORE_MEPC_MSTATUS 0
    
        TRAP_RESTORE_SP_T0
    
        mret
    
    .macro	TRAP_CALL_C_ROUTINE
        /* Call C routine */
        add	a0, sp, zero
        call	sbi_trap_handler
    .endm
  2. lib/sbi/sbi_trap.c 中定义了 sbi_trap_handler,处理各种 mcause,比如 Illegal InstructionsMisaligned Load & Store, Supervisor & Machine Ecall 等。

    // lib/sbi/sbi_trap.c
    void sbi_trap_handler(struct sbi_trap_regs *regs)
    {
        ...
    
        switch (mcause) {
        case CAUSE_ILLEGAL_INSTRUCTION:
            rc  = sbi_illegal_insn_handler(mtval, regs);
            break;
        case CAUSE_MISALIGNED_LOAD:
            rc = sbi_misaligned_load_handler(mtval, mtval2, mtinst, regs);
            break;
        case CAUSE_MISALIGNED_STORE:
            rc  = sbi_misaligned_store_handler(mtval, mtval2, mtinst, regs);
            break;
        case CAUSE_SUPERVISOR_ECALL:
        case CAUSE_MACHINE_ECALL:
            rc  = sbi_ecall_handler(regs);
            break;
        default:
            /* If the trap came from S or U mode, redirect it there */
            trap.epc = regs->mepc;
            trap.cause = mcause;
            trap.tval = mtval;
            trap.tval2 = mtval2;
            trap.tinst = mtinst;
            rc = sbi_trap_redirect(regs, &trap);
            break;
        };
    
        ...
  3. lib/sbi/sbi_ecall.c 中定义了处理 ecall mcausesbi_ecall_handler,它遍历上面 ecall_exts_list 中注册的各种 ecall 服务。

  4. sbi_ecall_handler 根据 Linux 内核传递的 ext (extension id) 找到链表中对应的 ecall 服务,执行其中的 handle 函数,该函数根据 fid 执行具体的服务内容。

    // lib/sbi/sbi_ecall.c
    int sbi_ecall_handler(struct sbi_trap_regs *regs)
    {
        // ...
        unsigned long extension_id = regs->a7;
        unsigned long func_id = regs->a6;
        struct sbi_trap_info trap = {0};
        unsigned long out_val = 0;
        
        // 遍历所有 ecall 服务
        ext = sbi_ecall_find_extension(extension_id);
        if (ext && ext->handle) {
            // 如果找到了就执行
            ret = ext->handle(extension_id, func_id,
                    regs, &out_val, &trap);
            if (extension_id >= SBI_EXT_0_1_SET_TIMER &&
                extension_id <= SBI_EXT_0_1_SHUTDOWN)
                is_0_1_spec = 1;
        } else {
            ret = SBI_ENOTSUPP;
        }
    
        ...
    }

    我们可以发现 extension_id 就是 a7 寄存器,他和我们在 uCore OS 中定义的 SBI_EXT_0_1_CONSOLE_PUTCHAR 是一致的。

程序的内存布局与编译流程

程序的内存布局

uCore 的编译系统

.PHONY: clean build user
# 设置伪目标clean、build和user,可以通过命令make来执行这些目标

all: build_kernel
# 默认目标为build_kernel,即执行build_kernel目标下的指令

LOG ?= error
# 定义一个变量LOG,默认值是error

K = os
TOOLPREFIX = riscv64-unknown-elf-

CC = $(TOOLPREFIX)gcc
AS = $(TOOLPREFIX)gcc
LD = $(TOOLPREFIX)ld
OBJCOPY = $(TOOLPREFIX)objcopy
OBJDUMP = $(TOOLPREFIX)objdump
PY = python3
GDB = $(TOOLPREFIX)gdb
CP = cp

MKDIR_P = mkdir -p

BUILDDIR = build

C_SRCS = $(wildcard $K/*.c)
# 定义一个变量C_SRCS,使用wildcard函数匹配所有以.c为后缀的文件,并存储在$K目录下

AS_SRCS = $(wildcard $K/*.S)
# 定义一个变量AS_SRCS,使用wildcard函数匹配所有以.S为后缀的文件,并存储在$K目录下

C_OBJS = $(addprefix $(BUILDDIR)/, $(addsuffix .o, $(basename $(C_SRCS))))
# 定义一个变量C_OBJS,通过addprefix和addsuffix函数将$(C_SRCS)中的路径替换为$(BUILDDIR),并将后缀修改为.o

AS_OBJS = $(addprefix $(BUILDDIR)/, $(addsuffix .o, $(basename $(AS_SRCS))))
# 定义一个变量AS_OBJS,通过addprefix和addsuffix函数将$(AS_SRCS)中的路径替换为$(BUILDDIR),并将后缀修改为.o

OBJS = $(C_OBJS) $(AS_OBJS)
# 定义一个变量OBJS,其值为$(C_OBJS)和$(AS_OBJS)的组合

HEADER_DEP = $(addsuffix .d, $(basename $(C_OBJS)))
# 定义一个变量HEADER_DEP,通过addsuffix函数将$(C_OBJS)中的后缀修改为.d

-include $(HEADER_DEP)
# 包含$(HEADER_DEP)中的.d文件

CFLAGS = -Wall -Werror -O -fno-omit-frame-pointer -ggdb
# 定义一个变量CFLAGS,并赋值为-Wall -Werror -O -fno-omit-frame-pointer -ggdb

CFLAGS += -MD
# 将-MD选项追加到CFLAGS变量中,用于自动生成依赖关系文件

CFLAGS += -mcmodel=medany
# 将-mcmodel=medany选项追加到CFLAGS变量中,用于指定内存模型

CFLAGS += -ffreestanding -fno-common -nostdlib -mno-relax
# 将-ffreestanding -fno-common -nostdlib -mno-relax选项追加到CFLAGS变量中,用于编译无操作系统环境下的程序

CFLAGS += -I$K
# 将-I$K选项追加到CFLAGS变量中,用于指定头文件搜索路径为$K目录下

CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)
# 将$(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector命令执行结果追加到CFLAGS变量中,用于禁用栈保护机制

ifeq ($(LOG), error)
CFLAGS += -D LOG_LEVEL_ERROR
else ifeq ($(LOG), warn)
CFLAGS += -D LOG_LEVEL_WARN
else ifeq ($(LOG), info)
CFLAGS += -D LOG_LEVEL_INFO
else ifeq ($(LOG), debug)
CFLAGS += -D LOG_LEVEL_DEBUG
else ifeq ($(LOG), trace)
CFLAGS += -D LOG_LEVEL_TRACE
endif
# 根据$(LOG)变量的值,向CFLAGS变量追加相应的预处理器选项,相当于添加了一个宏定义,log.h中的LOG_LEVEL_ERROR等宏定义会根据这个宏定义来决定是否生效

# Disable PIE when possible (for Ubuntu 16.10 toolchain)
ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]no-pie'),)
CFLAGS += -fno-pie -no-pie
endif
ifneq ($(shell $(CC) -dumpspecs 2>/dev/null | grep -e '[^f]nopie'),)
CFLAGS += -fno-pie -nopie
endif
# 根据系统环境判断是否支持PIE(位置无关执行)选项,并根据情况向CFLAGS变量追加相应的选项

LDFLAGS = -z max-page-size=4096
# 定义一个变量LDFLAGS,并赋值为-z max-page-size=4096

$(AS_OBJS): $(BUILDDIR)/$K/%.o : $K/%.S
    @mkdir -p $(@D)
    $(CC) $(CFLAGS) -c $< -o $@
# 规则:生成$(AS_OBJS)目标所需的依赖文件$(BUILDDIR)/$K/%.o,依赖于$K/%.S,并通过$(CC)命令编译生成目标文件

$(C_OBJS): $(BUILDDIR)/$K/%.o : $K/%.c  $(BUILDDIR)/$K/%.d
    @mkdir -p $(@D)
    $(CC) $(CFLAGS) -c $< -o $@
# 规则:生成$(C_OBJS)目标所需的依赖文件$(BUILDDIR)/$K/%.o,依赖于$K/%.c和$(BUILDDIR)/$K/%.d,并通过$(CC)命令编译生成目标文件

$(HEADER_DEP): $(BUILDDIR)/$K/%.d : $K/%.c
    @mkdir -p $(@D)
    @set -e; rm -f $@; $(CC) -MM $< $(INCLUDEFLAGS) > $@.$$$$; \
        sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
        rm -f $@.$$$$
# 规则:生成$(HEADER_DEP)目标所需的依赖文件$(BUILDDIR)/$K/%.d,依赖于$K/%.c,并通过$(CC)命令生成依赖关系文件

build: build/kernel
# 定义一个目标build,其依赖于build/kernel

build/kernel: $(OBJS)
    $(LD) $(LDFLAGS) -T os/kernel.ld -o $(BUILDDIR)/kernel $(OBJS)
    $(OBJDUMP) -S $(BUILDDIR)/kernel > $(BUILDDIR)/kernel.asm
    $(OBJDUMP) -t $(BUILDDIR)/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $(BUILDDIR)/kernel.sym
    @echo 'Build kernel done'
# 规则:生成build/kernel目标,依赖于$(OBJS),通过$(LD)命令连接生成kernel,并通过$(OBJDUMP)命令生成汇编文件和符号表

clean:
    rm -rf $(BUILDDIR)

# BOARD
BOARD		?= qemu
SBI			?= rustsbi
BOOTLOADER	:= ./bootloader/rustsbi-qemu.bin

QEMU = qemu-system-riscv64
QEMUOPTS = \
	-nographic \
	-machine virt \
	-bios $(BOOTLOADER) \
	-kernel build/kernel	\

run: build/kernel
	$(QEMU) $(QEMUOPTS)

# QEMU's gdb stub command line changed in 0.11
QEMUGDB = $(shell if $(QEMU) -help | grep -q '^-gdb'; \
	then echo "-gdb tcp::15234"; \
	else echo "-s -p 15234"; fi)

# 启动QEMU并通过GDB调试,此时QEMu会进入后台运行,并暂停执行,等待GDB连接
# 连接的GDB端口为15234
debug: build/kernel .gdbinit
	$(QEMU) $(QEMUOPTS) -S $(QEMUGDB) &
	sleep 1
	$(GDB)

编译、运行 uCore 的一些常用命令有如下一些,涉及了后续章节中引入的测试用例中的命令:

make run
make debug
make clean
# 编译测试用例的前四章
make user CHAPTER=4 LOG=trace
# 编译测试用例的第四章
make user CHAPTER=4_only LOG=trace
# 只运行测试用例的第四章
make test CHAPTER=4_only    

附录

makefile 和 qemu

AS = $(TOOLPREFIX)gas > AS = $(TOOLPREFIX)as

参考资料