YSOS-rust lab1

YSOS-lab1

1. 实验要求

  1. 请参考 代码与提交规范 进行实验代码编写。
  2. 依据 实验任务 完成实验。
    • 代码编写任务:观察提供的代码,完善所有标记为 FIXME: 的部分,并验证结果是否符合预期。请在报告中介绍实现思路,截图展示关键结果。
    • 思考任务:完成 “思考题” 和 “实验任务” 部分的内容,在报告中简要进行回答注:思考题可能也是理解代码、实现功能的重要提示。
    • Bonus 加分项:学有余力的同学可以任选 Bonus 部分完成,尝试完成更多的功能,并在报告中进行展示。这部分内容不是必须的要求。
  3. 请在实验报告中涵盖相关任务的实现截图、实验任务对应问题的解答、实验过程中遇到的问题与解决方案等内容。

2. 实验过程

初始化项目

YatSenOS-Tutorial-Volume-2/src/0x00文件夹复制到当前目录下,名称为0x00

YatSenOS-Tutorial-Volume-2/src/0x01文件夹下的所有内容复制到0x00文件夹,替换其中重复的文件

编译内核ELF

pkg/kernel 目录下运行 cargo build --release,在target目录下得到release/.rustc_info.jsonx86_64-unknown-none/

执行readelf -a target/x86_64-unknown-none/release/ysos_kernel > readelf.txt,可以查看编译产物的基本信息

实验任务

  1. 请查看编译产物的架构相关信息,与配置文件中的描述是否一致?

readelf.txt第9行 Machine: Advanced Micro Devices X86-64说明架构是X86-64, 和配置文件pkg/kernel/config/x86_64-unknown-none.json第8行"arch": "x86_64"一致

  1. 找出内核的入口点,它是被如何控制的?结合源码、链接、加载的过程,谈谈你的理解。

源码:

  • pkg/kernel/src/main.rs目录下定义了内核的入口
  • 它定义了一个 入口点(entry point),即 boot::entry_point!(kernel_main);。这表示在程序启动时,kernel_main 函数将被调用。
  • 查看pkg/boot/src/lib.rs中对boot::entry_point!宏的定义,
    • 它要求入口函数必须拥有签名fn(&'static BootInfo) -> !
    • 这个宏会创建一个名为 _start 的函数。链接器会将这个函数作为程序的入口点。
    • 使用这个宏的优势在于,它确保了函数和参数类型的正确性。

链接:

查看pkg/kernel/config/kernel.ld, 这是一个链接器脚本, 它定义了如何将不同的代码和数据段组合成最终的可执行内核映像

其中第一行为ENTRY(_start):这行指令指定了程序的入口点,即从 _start 标签处开始执行

加载:

  • 操作系统运行用户程序时将其映射到内存中;

  • 当它看到可执行文件中的PT_INERP时,操作系统将PT_INTERP指定的动态链接器映射进内存,并通过栈向其传递它所需要的参数,并跳到动态链接器的入口处开始执行;

  • 动态连接器的入口是_start, 这样源码中_start指定的kernel_main 函数就能开始执行

  1. 请找出编译产物的 segments 的数量,并且用表格的形式说明每一个 segments 的权限、是否对齐等信息。

执行readelf -l target/x86_64-unknown-none/release/ysos_kernel, 可以看到程序有8个segments

  • R表示可读
  • W表示可写
  • E表示可执行
Segment 权限 对齐
00 R 0x1000
01 R E 0x1000
02 RW 0x1000
03 RW 0x1000
04 RW 0x8
05 R 0x1
06 R 0x4
07 RW 0x0

在UEFI中加载内核

代码见关键代码部分, [点击跳转](# 3. 关键代码)

完成代码后,输入python ysos.py build -p debug以编译内核

使用 python ysos.py launch -d 启动 QEMU 并进入调试模式,这时候 QEMU 将会等待 GDB 的连接。

VScode中安装好CodeLLDB,然后启动调试,就可以进入调试环境,如下面的截图

image-20240309144053301


实验任务

1.set_entry 函数做了什么?为什么它是 unsafe 的?

源码如下

1
2
3
unsafe {
set_entry(elf.header.pt2.entry_point() as usize);
}

首先,代码调用了elf.header.pt2.entry_point(),可以在/root/.cargo/registry/src/index.crates.io-6f17d22bba15001f/xmas-elf-0.9.1/src/header.rs查看函数的源码

源码是一个getter宏: getter!(entry_point, u64); , 用于获取结构体字段值, 它会被展开为类似以下的形式:

1
2
3
4
5
6
7
8
impl<'a> HeaderPt2<'a> {
pub fn entry_point(&self) -> u64 {
match *self {
HeaderPt2::Header32(h) => h.entry_point as u64,
HeaderPt2::Header64(h) => h.entry_point as u64,
}
}
}

可以看出, 调用elf.header.pt2.entry_point()会返回一个u64作为内核的入口地址, 它会被类型转换为usize类型

pkg/boot/src/lib.rs可以看到set_entry()函数的源码

1
2
3
4
5
6
7
/// The entry point of kernel, set by BSP.
static mut ENTRY: usize = 0;
/// This function is unsafe because the caller must ensure that the kernel entry point is valid.
#[inline(always)]
pub unsafe fn set_entry(entry: usize) {
ENTRY = entry;
}

可以看出, set_entry()函数需要修改静态可变变量ENTRY的值, 它可能会被多个线程同时改变,造成未定义的行为, 所以是危险的, 必须要放在unsafe块中才能通过编译.

除此之外, 从代码注释可以看出, 函数不安全的原因是调用者必须保证内核跳转地址是有效的, 否则, 程序可能会跳转到未知的内存区域执行, 是不安全的


2.jump_to_entry 函数做了什么?要传递给内核的参数位于哪里?查询 call 指令的行为和 x86_64 架构的调用约定,借助调试器进行说明

jump_to_entry函数首先判断变量ENTRY是否为0, 如果为0, 则说明内核入口ENTRY未被成功修改

随后执行以下汇编指令:

  • mov rsp, {}将栈指针 rsp 设置为 stacktop 的值

  • call {} 调用 ENTRY 所指向的函数。这里使用了 rdi 寄存器传递 bootinfo的地址

最后,有 unreachable!()。这是一个永远不会执行的代码,用于标记函数的返回类型为 !(即“never”类型)。这意味着函数不会正常返回,而是直接终止程序。

如下是调试jump_to_entry函数可以看到的汇编代码

image-20240311112126861

代码asm!("mov rsp, {}; call {}", in(reg) stacktop, in(reg) ENTRY, in("rdi") bootinfo);对应的汇编指令位于043BB3D5, 指令如下:

1
2
3
4
5
043BB3D5: 48 89 CF          movq  %rcx, %rdi

043BB3D8: 48 89 D4 movq %rdx, %rsp

043BB3DB: FF D0 callq *%rax

%rcx是作为函数参数传入的bootinfo, 即BootInfo结构体的地址, 它被赋值给%rdi

%rdx是作为函数参数传入的stacktop, 它被赋值给%rsprsp 寄存器指向了当前栈顶的地址,所以这句指令可以实现设置栈顶地址的目的

callq *%rax 会从 %rax 中加载一个**四字(64位)**作为地址,然后调用从该地址开始的函数。

x86_64 架构的调用约定,用call调用函数时,前 6 个整型参数通过寄存器传递,分别存放在 %rdi%rsi%rdx%rcx%r8%r9

BootInfo结构体的地址就被存在%rdi中,可以传递给内核


3.entry_point! 宏做了什么?内核为什么需要使用它声明自己的入口点?

entry_point! 宏接受一个参数作为内核入口函数的函数名,它会生成一个新的函数, 名为__impl_start, 这个函数会验证传递的函数的签名, 确保它接受一个 'static 生命周期的 BootInfo 引用, 并且返回一个 ! 类型

然后, __impl_start 函数会调用传递的函数,将 boot_info 参数传递给它(BootInfo结构体的地址已经被存在%rdi寄存器中了, 它对应函数的第一个参数)

内核为什么需要使用它声明自己的入口点?

  • 链接器会将标记有_start的函数作为内核的入口点, 这个宏会让__impl_start 函数带上_start标记, 以便链接器找到内核入口

  • 比起直接创建一个标记了_start的函数, 这个宏还能保证内核入口函数接受的参数类型是正确的


4.如何为内核提供直接访问物理内存的能力?你知道几种方式?代码中所采用的是哪一种?可以参考这篇文章进行学习。

几种方式:

  • Identity Mapping: 页表的物理地址也是有效的虚拟地址

  • Map at a Fixed Offset: 虚拟地址=物理地址+固定的偏移量

  • Map the Complete Physical Memory: 映射整个物理内存到虚拟内存,这样内核就能够访问到任意物理内存

  • Temporary Mapping: 对于物理内存非常小的设备,我们只能在需要访问页表帧时临时映射它们。

  • Recursive Page Tables: 将页表中的条目映射到表本身

代码采用了Map the Complete Physical Memory方式,由esp/EFI/BOOT/boot.conf中的配置可知, 有一项为physical_memory_offset=0xFFFF800000000000, 设置了整个物理内存到虚拟内存的偏移量. map_physical_memory函数会根据这一设置映射物理内存


5.为什么 ELF 文件中不描述栈的相关内容?栈是如何被初始化的?它可以被任意放置吗?

因为ELF 文件的目标是让处理器正确执行我们编写的代码,而栈是在程序运行时动态分配的. 栈的大小和位置在运行时由操作系统决定,不是在编译或链接阶段确定的. 因此,ELF 文件不直接描述栈的内容,而是依赖于操作系统在加载时设置栈的相关信息.

所以在加载内核时, 需要手动映射内核栈, 通过下面的代码实现

1
2
3
4
5
6
7
let _page_range = map_range(
config.kernel_stack_address,
match config.kernel_stack_auto_grow{
0 => config.kernel_stack_size,
_ => config.kernel_stack_auto_grow / 4096,
},
&mut page_table, &mut frame_allocator).unwrap();

它不能被任意放置, 而要根据配置文件中设置的栈起始地址kernel_stack_address, 栈初始分配大小kernel_stack_auto_grow和栈总大小kernel_stack_size决定


根据上述调试过程,回答以下问题,并给出你的回答与必要的截图:

请解释指令 layout asm 的功能。倘若想找到当前运行内核所对应的 Rust 源码,应该使用什么 GDB 指令?

layout asmGDB 中的一个强大命令,用于在调试会话期间显示汇编代码窗口, 可以在 GDB 中同时查看源代码和对应的汇编代码。效果如下图

image-20240317134817465

使用list命令可以查看当前运行内核对应的rust源码,如下图

image-20240317134728885


假如在编译时没有启用 DBG_INFO=true,调试过程会有什么不同?

重新编译, 执行python ysos.py build(去掉了-p debug参数)

然后执行python ysos.py launch -d, 启动gdb

image-20240317135513628

gdb窗口中只能显示汇编代码, 而无法显示rust源码

默认情况下,编译生成的可执行文件中没有可供 GDB 调试使用的特殊信息。为了将必要的调试信息整合到可执行文件中,我们需要在编译阶段加上DBG_INFO=true


你如何选择了你的调试环境?截图说明你在调试界面(TUI 或 GUI)上可以获取到哪些信息?

我使用了2种调试环境

  1. gef

可以获得的信息如下图指出

image-20240317140236793

  1. VScode

可以获得的信息如下图指出

image-20240317140910551

UART和日志输出

IO端口偏移 DLAB的设置 映射到该端口的寄存器 Register mapped to this port
+0 0 数据寄存器。读取该寄存器是从接收缓冲区读取的。写入该寄存器将写入发送缓冲区。 Data register. Reading this registers read from the Receive buffer. Writing to this register writes to the Transmit buffer.
+1 0 中断使能寄存器。 Interrupt Enable Register.
+0 1 当 DLAB 设置为 1 时,这是用于设置波特率的除数值的最低有效字节。 With DLAB set to 1, this is the least significant byte of the divisor value for setting the baud rate.
+1 1 当 DLAB 设置为 1 时,这是除数值的最高有效字节。 With DLAB set to 1, this is the most significant byte of the divisor value.
+2 - 中断识别和 FIFO 控制寄存器 Interrupt Identification and FIFO control registers
+3 - 线路控制寄存器。该寄存器的最高有效位是 DLAB。 Line Control Register. The most significant bit of this register is the DLAB.
+4 - 调制解调器控制寄存器。 Modem Control Register.
+5 - 线路状态寄存器。 Line Status Register.
+6 - 调制解调器状态寄存器。 Modem Status Register.
+7 - 暂存寄存器。 Scratch Register.

编写串口驱动,代码见关键代码部分, [点击跳转](# 3.1-串口驱动的实现)

设置日志输出格式,代码见关键代码部分, [点击跳转](# 3.2-日志输出)

思考题

1.在 pkg/kernelCargo.toml 中,指定了依赖中 boot 包为 default-features = false,这是为了避免什么问题?请结合 pkg/bootCargo.toml 谈谈你的理解。

pkg/bootCargo.toml 中有如下内容

1
2
3
[features]
boot = ["uefi/alloc", "uefi-services"]
default = ["boot"]

当在 pkg/kernelCargo.toml 指定依赖中 boot 包为 default-features = false, 上面的特性会被禁用

根据uefi-services的官方文档, It includes a panic handler, a logger, and a global allocator.

设置不包含它,可以避免自己定义的panic handler, logger 与之发生冲突

另外,其使用的logger crate会使用到std crate, 与#![no_std] 冲突


2.在 pkg/boot/src/main.rs 中参考相关代码,聊聊 max_phys_addr 是如何计算的,为什么要这么做?

相关代码如下

1
2
3
4
5
6
7
8
9
10
11
let mmap = system_table
.boot_services()
.memory_map(mmap_storage)
.expect("Failed to get memory map");

let max_phys_addr = mmap
.entries()
.map(|m| m.phys_start + m.page_count * 0x1000)
.max()
.unwrap()
.max(0x1_0000_0000); // include IOAPIC MMIO area

这段代码是在操作系统启动过程中,用于获取内存映射信息的

  • 首先,从 system_table 中获取引导服务(boot_services())。

  • 然后,调用 memory_map(mmap_storage) 来获取内存映射信息。这个函数返回一个内存映射的结构体,其中包含了系统中不同内存区域的信息。

  • 接下来,对内存映射的每个条目进行处理。对于每个条目m,计算了物理地址的上限(max_phys_addr)。具体计算如下:

    • m.phys_start 表示该内存区域的起始物理地址。

    • m.page_count 表示该区域占用的页数(每页大小为 4KB,所以乘以 0x1000)。

    • 将这两者相加,得到了该内存区域的结束物理地址。

  • 接着,使用 .max() 函数找到了所有内存区域中的最大物理地址。这个值将作为系统的最大物理地址。

  • 最后,使用 .max(0x1_0000_0000) 来确保最大物理地址至少是 0x1_0000_0000(即 4GB)。这是因为在某些系统中,IOAPIC(输入输出高级可编程中断控制器)的内存映射区域位于这个地址之上。


3.串口驱动是在进入内核后启用的,那么在进入内核之前,显示的内容是如何输出的?

在进入内核之前, 由引导加载程序,即UEFI负责实现串口驱动


4.在 QEMU 中,我们通过指定 -nographic 参数来禁用图形界面,这样 QEMU 会默认将串口输出重定向到主机的标准输出。

  • 假如我们将 Makefile 中取消该选项,QEMU 的输出窗口会发生什么变化?请观察指令 make run QEMU_OUTPUT= 的输出,结合截图分析对应现象。

现象:会弹出QEMU窗口, 输出在窗口中,但是进入内核后就停止了输出

image-20240317162208179

  • 在移除 -nographic 的情况下,如何依然将串口重定向到主机的标准输入输出?请尝试自行构造命令行参数,并查阅 QEMU 的文档,进行实验。

执行下面的命令

1
/usr/bin/qemu-system-x86_64 -bios assets/OVMF.fd -net none -serial stdio -m 96M -drive format=raw,file=fat:rw:esp

其中关键是-serial stdio, 将串口重定向到主机的标准输入输出

效果如下

image-20240317164129468

可以在主机的控制台看到内核的输出

加分项

1.😋 线控寄存器的每一比特都有特定的含义,尝试使用 bitflags 宏来定义这些标志位,并在 uart16550 驱动中使用它们。

参考文档https://www.lammertbies.nl/comm/info/serial-uart, 可以定义如下的bitflags宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bitflags! {
pub struct LCR: u8 {
const five_data_bits = 0b00000000;
const six_data_bits = 0b00000001;
const seven_data_bits = 0b00000010;
const eight_data_bits = 0b00000011;
const one_stop_bit = 0b00000000;
const no_parity = 0b00000000;
const odd_parity = 0b00000100;
const even_parity = 0b00011000;
const high_parity = 0b00101000;
const low_parity = 0b00111000;
const DLAB0 = 0b00000000;
const DLAB1 = 0b10000000;
}

}

uart16550 驱动中使用它们,代码如下

1
2
self.line_control.write(LCR::DLAB1.bits());
self.interrupt_enable.write(LCR::eight_data_bits.bits() | LCR::no_parity.bits() | LCR::one_stop_bit.bits());

2.😋 尝试在进入内核并初始化串口驱动后,使用 escape sequence 来清屏,并编辑 get_ascii_header() 中的字符串常量,输出你的学号信息。

pkg/kernel/src/drivers/serial.rs添加如下,用于清屏

1
println!("\x1b[2J");

pkg/kernel/src/utils/mod.rs修改如下

1
2
3
4
5
6
7
8
9
10
11
12
    concat!(
r"
__ __ __ _____ ____ _____
\ \/ /___ _/ /_/ ___/___ ____ / __ \/ ___/
\ / __ `/ __/\__ \/ _ \/ __ \/ / / /\__ \
/ / /_/ / /_ ___/ / __/ / / / /_/ /___/ /
/_/\__,_/\__//____/\___/_/ /_/\____//____/

v
CJL-22330004",
env!("CARGO_PKG_VERSION")
)

效果如图

image-20240319205557667

清屏是成功的,而且学号信息打印了出来


3.🤔 尝试添加字符串型启动配置变量 log_level,并修改 logger 的初始化函数,使得内核能够根据启动参数进行日志输出。

UEFI读取数字部分的参考文献:https://stackoverflow.com/questions/73113685/how-do-i-read-user-input-in-uefi-rs-uefi-rust-wrapper

代码详见关键代码部分, [点击跳转](# 加分项3)

UEFI执行过程中会停止, 要求用户输入一个数字表示log等级

image-20240322231046731

如果输入3,代表info,结果如下, info可以正常输出

image-20240322231151162

如果输入1, 代表Error,结果如下, info不能输出

image-20240322231255371


4.🤔尝试使用调试器,在内核初始化之后(ysos::init 调用结束后)下断点,查看、记录并解释如下的信息:

  • 内核的栈指针、栈帧指针、指令指针等寄存器的值。

image-20240319212313247

如图,分别输出了栈指针指向的内存地址及所在地址的数据, 栈帧指针指向的内存地址及所在地址的数据, 以及指令指针寄存器的值

  • 内核的代码段、数据段、BSS 段等在内存中的位置。

执行readelf -Sl esp/KERNEL.ELF ,结果如下图

image-20240322220335445


5.🤔 “开发者是愿意用安全换取灵活的”,所以,我要把代码加载到栈上去,可当我妄图在栈上执行代码的时候,却得到了 Segment fault,你能解决这个问题吗?

如下图, 将代码加载到了栈上, 然后执行

正常编译的结果会导致segment fault,但是如果在编译时加上-z execstack, 允许程序在栈上执行, 就不会导致segment fault, 还能正常输出

image-20240322214909968

分别用两种编译方法, 使用readelf查看Program Headers的权限,如下图

image-20240322215524353

只有第二张图编译的结果可以在栈上执行代码, 可以看出其GNU_STACK是具有执行权限的

3. 关键代码

2.1-加载相关文件

加载配置文件

pkg/boot/src/main.rs

1
2
3
4
5
6
7
8
9
// 1. Load config
let config = {
/* FIXME: Load config file */
//打开并加载config文件
let mut file = open_file(bs, CONFIG_PATH);
let buf = load_file(bs, &mut file);
//从config文件内容加载Config结构体
crate::config::Config::parse(buf)
};

加载内核 ELF

pkg/boot/src/main.rs

1
2
3
4
5
6
7
8
9
10
11
// 2. Load ELF files
let elf = {
/* FIXME: Load kernel elf file */
//内核存储地址可以从config结构体读出
let KERNEL_PATH = config.kernel_path;
//读取内核文件
let mut file = open_file(bs, KERNEL_PATH);
let buf = load_file(bs, &mut file);
//新建ElfFile结构体
ElfFile::new(buf).unwrap()
};

2.2-更新控制寄存器

pkg/boot/src/main.rs

1
2
3
4
5
6
7
// FIXME: root page table is readonly, disable write protect (Cr0)
unsafe {
//WRITE_PROTECT: When set, it is not possible to write to read-only pages from ring 0.
//fn remove(&mut self, other: Self)
//self & !other
Cr0::update(|f| f.remove(Cr0Flags::WRITE_PROTECT));
}

2.3-映射内核文件

pkg/boot/src/main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// FIXME: map physical memory to specific virtual address offset
let mut frame_allocator = UEFIFrameAllocator(bs);
map_physical_memory(config.physical_memory_offset, max_phys_addr, &mut page_table, &mut frame_allocator);
// FIXME: load and map the kernel elf file
load_elf(&elf, config.physical_memory_offset, &mut page_table, &mut frame_allocator).unwrap();
// FIXME: map kernel stack
let _page_range = map_range(
config.kernel_stack_address,
match config.kernel_stack_auto_grow{
0 => config.kernel_stack_size,
_ => config.kernel_stack_auto_grow / 4096,
},
&mut page_table, &mut frame_allocator).unwrap();
// FIXME: recover write protect (Cr0)
unsafe {
//WRITE_PROTECT: When set, it is not possible to write to read-only pages from ring 0.
//fn insert(&mut self, other: Self)
//The bitwise or (|) of the bits in two flags values.
Cr0::update(|f| f.insert(Cr0Flags::WRITE_PROTECT));
}

补全load_segment函数: 根据段是否可读,可写,可执行的要求设置page_table_flags

pkg/elf/src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
// FIXME: handle page table flags with segment flags
//unimplemented!("Handle page table flags with segment flags!");
if segment.flags().is_read(){
page_table_flags |= PageTableFlags::USER_ACCESSIBLE;
}
if segment.flags().is_write(){
page_table_flags |= PageTableFlags::WRITABLE;
}
if !segment.flags().is_execute(){
page_table_flags |= PageTableFlags::NO_EXECUTE;
}

3.1-串口驱动的实现

pkg/kernel/src/drivers/uart16550.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
use core::fmt;
use x86_64::instructions::port::{Port, PortReadOnly, PortWriteOnly};
/// A port-mapped UART 16550 serial interface.
//Fix: struct
pub struct SerialPort{
data: Port<u8>,
interrupt_enable: PortWriteOnly<u8>,
interrupt_identification: PortWriteOnly<u8>,
line_control: PortWriteOnly<u8>,
modem_control: PortWriteOnly<u8>,
line_status: PortReadOnly<u8>,
}

impl SerialPort {
pub const fn new(port: u16) -> Self {
//FIX: init
Self{
data: Port::new(port + 0),
interrupt_enable: PortWriteOnly::new(port + 1),
interrupt_identification: PortWriteOnly::new(port + 2),
line_control: PortWriteOnly::new(port + 3),
modem_control: PortWriteOnly::new(port + 4),
line_status: PortReadOnly::new(port + 5),
}
}

/// Initializes the serial port.
pub fn init(&mut self) {//这里原来是不可变借用,改成了可变借用
// FIXME: Initialize the serial port
unsafe{
// Disable all interrupts
self.interrupt_enable.write(0x00);
// Enable DLAB (set baud rate divisor)
self.line_control.write(0x80);
// Set divisor to 3 (lo byte) 38400 baud
self.data.write(0x03);
// Set divisor to 3 (hi byte) 38400 baud
self.interrupt_enable.write(0x00);
// 8 bits, no parity, one stop bit
self.line_control.write(0x03);
// Enable FIFO, clear them, with 14-byte threshold
self.interrupt_identification.write(0xc7);
// IRQs enabled, RTS/DSR set
self.modem_control.write(0x0B);
// Set in loopback mode, test the serial chip
self.modem_control.write(0x1e);
// Test serial chip (send byte 0xAE and check if serial returns same byte)
self.data.write(0xae);
// Check if serial is faulty (i.e: not same byte as sent)
if self.data.read()!= 0xae{
panic!("serial is faulty");
}
// If serial is not faulty set it in normal operation mode
// (not-loopback with IRQs enabled and OUT#1 and OUT#2 bits enabled)
self.modem_control.write(0x0f);

}
}

/// Sends a byte on the serial port.
pub fn send(&mut self, data: u8) {
// FIXME: Send a byte on the serial port
unsafe{
while self.line_status.read() & 0x20 ==0{}
self.data.write(data);
}
}

/// Receives a byte on the serial port no wait.
pub fn receive(&mut self) -> Option<u8> {
// FIXME: Receive a byte on the serial port no wait
unsafe{
if self.line_status.read() & 1 !=0{
Some(self.data.read())
}else{
None
}

}
}
}

impl fmt::Write for SerialPort {
fn write_str(&mut self, s: &str) -> fmt::Result {
for byte in s.bytes() {
self.send(byte);
}
Ok(())
}
}

3.2-日志输出

pkg/kernel/src/utils/logger.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
use log::{Metadata, Record, Level, LevelFilter};

pub fn init() {
static LOGGER: Logger = Logger;
log::set_logger(&LOGGER).unwrap();

// FIXME: Configure the logger
//设置最高输出等级为Trace,以便查看所有log的效果
log::set_max_level(LevelFilter::Trace);

info!("Logger Initialized.");
}

struct Logger;

impl log::Log for Logger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}

fn log(&self, record: &Record) {
// FIXME: Implement the logger with serial output
if self.enabled(record.metadata()) {
// println!("{} - {}", record.level(), record.args());
match record.level() {
Level::Error => println!(
"\x1b[31;1;4m[X] Error\x1b[0m - \x1b[31mfrom {}, at line {}, {}\x1b[0m",
record.file_static().unwrap(),
record.line().unwrap(),
record.args(),
),
Level::Warn => println!(
"\x1b[33;1;4m[!] Warning\x1b[0m- \x1b[33mfrom {}, at line {}, {}\x1b[0m",
record.file_static().unwrap(),
record.line().unwrap(),
record.args(),
),
Level::Info => println!(
"\x1b[34;1;4m[+] Info\x1b[0m - \x1b[34mfrom {}, at line {}, {}\x1b[0m",
record.file_static().unwrap(),
record.line().unwrap(),
record.args(),
),
Level::Debug => println!(
"\x1b[36;1;4m[#] Debug\x1b[0m - \x1b[36mfrom {}, at line {}, {}\x1b[0m",
record.file_static().unwrap(),
record.line().unwrap(),
record.args(),
),
Level::Trace => println!(
"\x1b[32;1;4m[%] Trace\x1b[0m - \x1b[32mfrom {}, at line {}, {}\x1b[0m",
record.file_static().unwrap(),
record.line().unwrap(),
record.args(),
),
}
}
}

fn flush(&self) {}
}

加分项3

pkg/boot/src/main.rs: 这段代码在UEFI启动阶段要求用户输入一个数字1-5, 表示log_level, 结果储存在u8变量level中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//let log_level: u8 = 0;
info!("Please input a char from 1 to 5, which represents the log level from Error to Trace:");
let mut exit_flag = false;
let mut level:u8 = 5;
while !exit_flag {
let key = system_table.stdin().read_key().unwrap();
match key {
Some(k) => {
match k {
uefi::proto::console::text::Key::Printable(p) => {
if p == Char16::try_from(49u16).unwrap() {
level = 1;
exit_flag = true;
} else if p == Char16::try_from(50u16).unwrap() {
level = 2;
exit_flag = true;
} else if p == Char16::try_from(51u16).unwrap() {
level = 3;
exit_flag = true;
} else if p == Char16::try_from(52u16).unwrap() {
level = 4;
exit_flag = true;
} else if p == Char16::try_from(53u16).unwrap() {
level = 5;
exit_flag = true;
}
}
uefi::proto::console::text::Key::Special(s) => {
if s == ScanCode::ESCAPE {
exit_flag = true;
}
}
};
},
None => {}
};
};
info!("Your input log level is {}", level);

结尾: 将level的地址传递给jump_to_entry函数

1
2
3
unsafe {
jump_to_entry(&bootinfo, stacktop, &level);
}

pkg/boot/src/lib.rs: 修改jump_to_entry函数, 将level的地址作为第二个参数传给内核(放在rsi寄存器中)

1
2
3
4
5
pub unsafe fn jump_to_entry(bootinfo: *const BootInfo, stacktop: u64, log_level: *const u8) -> ! {
assert!(ENTRY != 0, "ENTRY is not set");
asm!("mov rsp, {}; call {}", in(reg) stacktop, in(reg) ENTRY, in("rdi") bootinfo, in("rsi") log_level);
unreachable!()
}

然后修改entry_point宏, 允许内核main函数接收第二个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
#[macro_export]
macro_rules! entry_point {
($path:path) => {
#[export_name = "_start"]
pub extern "C" fn __impl_start(boot_info: &'static $crate::BootInfo, log:&'static u8) -> ! {
// validate the signature of the program entry point
let f: fn(&'static $crate::BootInfo,&'static u8) -> ! = $path;

f(boot_info,log)
}
};
}

pkg/kernel/src/main.rs修改kernel_main函数, 允许接收第二个参数,并传给ysos::init

1
2
3
pub fn kernel_main(boot_info: &'static boot::BootInfo, log_level: &'static u8) -> ! {
info!("Log level: {}", log_level);
ysos::init(boot_info, *log_level);

pkg/kernel/src/lib.rs

1
2
3
pub fn init(_boot_info: &'static BootInfo, log_level: u8) {
drivers::serial::init(); // init serial output
logger::init(log_level); // init logger system

pkg/kernel/src/utils/logger.rs根据数字设置不同的log等级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub fn init(log_level: u8) {
static LOGGER: Logger = Logger;
log::set_logger(&LOGGER).unwrap();

// FIXME: Configure the logger
//设置最高输出等级为Trace,以便查看所有log的效果
match log_level {
1 => log::set_max_level(LevelFilter::Error),
2 => log::set_max_level(LevelFilter::Warn),
3 => log::set_max_level(LevelFilter::Info),
4 => log::set_max_level(LevelFilter::Debug),
5 => log::set_max_level(LevelFilter::Trace),
_ => log::set_max_level(LevelFilter::Trace),
}

info!("Logger Initialized.");
}

4. 实验结果

如图,内核成功启动,并且输出了log

image-20240317143715599

5. 总结

学习了如何编写boot引导启动操作系统内核, 并且学会了编写串口驱动, 对rust项目开发更加熟悉


YSOS-rust lab1
https://blog.algorithmpark.xyz/2024/03/22/YSOS/lab1/index/
作者
CJL
发布于
2024年3月22日
更新于
2024年5月14日
许可协议