Skip to content

Commit

Permalink
wip: lab 2
Browse files Browse the repository at this point in the history
  • Loading branch information
GZTimeWalker committed Jan 16, 2024
1 parent 0689790 commit e6a2d12
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 5 deletions.
1 change: 0 additions & 1 deletion docs/labs/0x01/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ unsafe {

### 调试内核


依据[调试教程](../../wiki/debug.md)的相关内容,搭建基于命令行的 GDB 调试环境。

作为实验的推荐调试环境,你需要配置好 `gef` 插件以进行更加灵活的二进制调试。同时利用 VSCode 进行调试也是一个不错的选择,鼓励你进行尝试,它将会作为实验的加分项目之一。
Expand Down
105 changes: 101 additions & 4 deletions docs/labs/0x02/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,63 @@

**3. 不要直接复制粘贴命令执行**

## 合并实验代码

!!! tip "如何使用本次参考代码"

为了帮助大家进行项目代码的结构组织,本次实验给出的参考代码中包含了**完整的文件结构**。
本次给出的参考代码为**增量补充**,即在上一次实验的基础上进行修改和补充。因此,你需要将本次参考代码与上一次实验的代码进行合并。

文件的目录与上一次实验相同,因此你可以直接将本次参考代码的 `src` 目录下的文件复制到上一次实验的目录结构下,覆盖同名文件。

合并后的代码并不能直接运行,你需要基于合并后的代码、按照如下文档进行修改补充,才能逐步实现本次实验的功能。

`pkg/kernel/src/memory` 文件夹中,增量代码补充包含了如下的模块:

- `address.rs`:定义了物理地址到虚拟地址的转换函数,这一模块接受启动结构体提供的物理地址偏移,从而对物理地址进行转换。此部分内容在 lab 1 中已经有所涉及,你可以参考[完整的物理地址映射](https://os.phil-opp.com/paging-implementation/#map-the-complete-physical-memory)进行深入了解。
- `frames.rs`:利用 bootloader 传入的内存布局进行物理内存帧分配,实现 x86_64 的 `FrameAllocator` trait。**本次实验中不会涉及,后续实验中会用到。**
- `gdt.rs`:定义 TSS 和 GDT,为内核提供内存段描述符和任务状态段。
- `allocator.rs`:注册内核堆分配器,为内核堆分配提供能力。从而允许内核使用 `alloc` crate 进行需要动态内存分配的操作、使用其中定义的数据结构,如 `Vec``String``Box` 等。

!!! note "动态内存分配算法在这里不做要求,本次实验直接使用现有的库赋予内核堆分配能力。"

## GDT、TSS 与 IDT
`pkg/kernel/src/interrupt` 文件夹中,增量代码补充包含了如下的模块:

- `apic`:有关 XAPIC、IOAPIC 和 LAPIC 的定义和实现。
- `consts.rs`:有关于中断向量、IQR 的常量定义。
- `exceptions.rs`:包含了 CPU 异常的处理函数,并暴露 `register_idt` 用于注册 IDT。
- `mod.rs`:定义了 `init` 函数,用于初始化中断系统,加载 IDT。

## GDT 与 TSS

!!! note "请阅读 [x64 数据结构概述](../../wiki/structures.md) 部分,了解 x64 架构中的 GDT、TSS 和 IDT。"

在我们的操作系统中,GDT、TSS 和 IDT 均属于全局静态的数据结构,因此我们将它们定义为 `static` 类型,并使用 `lazy_static` 宏来实现懒加载,其本质上也是通过 `Once` 来保护全局对象,但是它的初始化函数无需参数传递,因此可以直接声明,无需手动调用 `call_once` 函数来传递不同的初始化参数。

```rust
lazy_static! {
static ref TSS: TaskStateSegment = {
let mut tss = TaskStateSegment::new();

// do something...

tss
};
}
```

你需要参考上下文,在 `src/memory/gdt.rs` 中补全 TSS 的中断栈表,为 Double Fault 和 Page Fault 准备独立的栈。

!!! question "实验任务"

补全上述代码任务,并进行下列尝试,并在报告中保留对应的触发方式及相关代码片段:

1. 尝试用你的方式触发 Triple Fault,开启 `intdbg` 对应的选项,在 QEMU 中查看调试信息,分析 Triple Fault 的发生过程。
2. 补全 Double Fault 的中断处理函数,观察 Double Fault 的发生过程。尝试通过调试器定位 Double Fault 发生时的栈是否符合预期。
3. 补全 Page Fault 的中断处理函数,通过访问非法地址触发 Page Fault,观察 Page Fault 的发生过程。分析 Cr2 寄存器的值,并尝试回答为什么 Page Fault 属于可恢复的异常。

## 注册中断处理程序

`src/interrupt/mod.rs` 中,参考如下代码,将中断描述符表(IDT)的注册工作委托给各个模块。IDT 只会被初始化一次,因此这里使用了 `lazy_static` 宏来实现懒加载,而不对它进行上锁或使用 Once 保护
`src/interrupt/mod.rs` 中,参考如下代码,将中断描述符表的注册委托给各个模块

```rust
// mod clock;
Expand Down Expand Up @@ -104,9 +152,58 @@ pub unsafe fn register_idt(idt: &mut InterruptDescriptorTable) {

!!! note "请阅读 [APIC 可编程中断控制器](../../wiki/apic.md) 部分,了解什么是 APIC 可编程中断控制器。"

根据上述文档,你需要在 `src/interrupt/apic/xapic.rs` 中补全 APIC 的初始化代码,以便在后续实验中使用 APIC 实现时钟中断和 I/O 设备中断。

## 时钟中断

## 内存管理(此处不作重点)
在顺利配置好 XAPIC 并初始化后,APIC 的中断就被成功启用了。为了响应时钟中断,我们需要为 IRQ0 Timer 设置中断处理程序。创建 `src/interrupt/clock.rs` 文件,参考如下代码,为 Timer 设置中断处理程序:

```rust
use core::sync::atomic::{AtomicU64, Ordering};
use super::consts::*;

pub unsafe fn register_idt(idt: &mut InterruptDescriptorTable) {
idt[(Interrupts::IrqBase as u8 + Irq::Timer as u8) as usize]
.set_handler_fn(clock_handler);
}

pub extern "x86-interrupt" fn clock_handler(_sf: InterruptStackFrame) {
x86_64::instructions::interrupts::without_interrupts(|| {
if inc_counter() % 0x10000 == 0 {
info!("Tick! @{}", read_counter());
}
super::ack();
});
}

static COUNTER: AtomicU64 = AtomicU64::new(0);

#[inline]
pub fn read_counter() -> u64 {
// FIXME: load counter value
}

#[inline]
pub fn inc_counter() -> u64 {
// FIXME: read counter value and increase it
}
```

仅仅开启 APIC 的中断并不能触发中断处理,这是因为 CPU 的中断并没有被启用。在 `src/lib.rs` 中,所有组件初始化完毕后,需要为 CPU 开启中断,你可以通过如下代码实现:

```rust
x86_64::instructions::interrupts::enable();
```

它使用 `sti` 汇编指令,将 `RFLAGS` 寄存器中的 `IF` 位置 1,从而开启中断。

!!! question "实验任务"

补全上述代码任务,并进行下列尝试:

1. 如何调节时钟中断的频率?请尝试修改你的代码,更改不同的频率,并观察 QEMU 中的输出。说明你修改了哪些代码,如果想要中断的频率减半,应该如何修改?
2. 考虑时钟中断进行进程调度的场景,时钟中断的频率应该如何设置?太快或太慢的频率会带来什么问题?请分别回答。
3. `without_interrupts` 函数的作用是什么?为什么需要使用它?它的实现原理是什么?

## 串口输入中断

Expand Down
6 changes: 6 additions & 0 deletions src/0x02/pkg/kernel/src/interrupt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,9 @@ pub fn enable_irq(irq: u8) {
let mut ioapic = unsafe { IoApic::new(physical_to_virtual(IOAPIC_ADDR)) };
ioapic.enable(irq, 0);
}

#[inline(always)]
pub fn ack() {
let mut lapic = unsafe { XApic::new(physical_to_virtual(LAPIC_ADDR)) };
lapic.eoi();
}

0 comments on commit e6a2d12

Please sign in to comment.