一、实验目的

中断、异常和陷阱指令(合称类中断)是操作系统的基石,现代操作系统就是由(类)中断驱动的。本实验的目的在于深刻理解(类)中断的原理和机制,掌握 CPU 访问设备控制器的方法,掌握 x86 体系结构的(类)中断机制和规范,实现时钟中断服务和部分异常处理等。

二、实验过程&错误

内容(一):实现 Breakpoint 异常的处理

步骤1:新建一个os并复制文件main.rs和vga_buffer.rs

步骤2:新建lib.rs文件并输入pub mod interrupts;

步骤3:新建interrupts.rs文件并输入如下代码

我们将首先在src/interrupts.rs中创建一个新的中断模块,该模块首先创建一个init_idtfunction,该函数创建一个新的InterruptDescriptorTable。
步骤4:创建简单的断点处理函数。在interrupts.rs文件中覆盖为如下代码

现在我们可以添加处理程序函数了。我们首先为断点异常添加一个处理程序。断点异常是测试异常处理的完美异常。它的唯一目的是在执行断点指令int 3时暂停程序。
断点异常通常用于调试器:当用户设置断点时,调试器用int 3指令覆盖相应的指令,以便CPU在到达该行时抛出断点异常。当用户想要继续该程序时,调试器再次用原始指令替换int 3指令,并继续该程序。
对于我们的用例,我们不需要覆盖任何指令。相反,我们只希望在执行断点指令时打印一条消息,然后继续该程序。
步骤5:加载IDT,将如下代码写入到interrupts.rs文件中原本init_idt函数的位置

为了CPU使用我们的新中断描述符表,我们需要使用lidT指令加载它。x86_64的中断描述结构为该结构提供了加载方法功能。
因此,加载方法需要一个“静态”,这是一个对程序的完整运行时有效的引用。原因是CPU将在每次中断上访问此表,直到我们加载不同的IDT。因此,使用比“静态”更短的生存期会导致使用后的错误。
事实上,这正是在这里发生的事。我们的IDT是在堆栈上创建的,因此它仅在init函数内部有效。然后,栈存储器用于其它功能,因此CPU将随机堆栈存储器解释为IDT。
静态MUTS很容易出现数据争用,因此我们需要每个访问上的一个不安全的块。
步骤6:缓慢的静态处理,将interrupts文件修改如下

步骤7:运行,将lib.rs文件和main.rs文件修改如下

问题7-1:编译出错,一直有一个错误说找不到std

但问题是我已经在main中禁用了std标准库,为什么没有用呢?经过加上–verbose查看后发现详细原因如下:

但问题在哪里一直没有解决,后来我决定,
解决方法7-1:直接将原来的第二次试验的基础上增加新的文件,而不是新建文件。
但还是不行

由此考虑不是在于我们的coml文件或者main文件的问题,而是在我们的lib文件和interrupts文件中有对std标准库函数的引用导致这种问题。发现了我们在interrupts文件中使用了println!函数,而这时std标准库中的函数,我们想要使用我们自己写的println函数,
解决方法7-2:将interrupts文件修改如下:

可以看到,我一方面是禁用了std标准库,另一方面引用了我自己写的vga_buffer库。但发现还是不行:

显示是没有办法找到vga_buffer库
解决方法7-3:尝试更新rust,输入rustup update

还是没有办法引用std标准库

真的不知道为什么。。。
解决方法7-4:将coml文件修改如下:

问题7-2:std库的错误解决了,但出现了一个新的问题:

在interrupts文件里,没有办法使用println宏
解决方法7-5:在主函数中加入这么一句,就可以使用文件夹中我们所写的宏

注意,不能直接pub mod vga_buffer,这样会引起很神奇的错误。
问题7-3:找不到我们定义的宏

解决方法7-6:这是因为我们在所有的文件中都没有引入vga_buffer这个我们自己写的库,所以没有办法调用我们写的println宏,这里需要将lib文件修改如下:

问题7-4:我们在调用x86-interrupt的abi的时候可能会发生奇怪的变化

解决方法7-7:这个与x86的中断机制有关系,这里我还没有找到原因,我们需要在lib文件里加上这么一条语句

此时,我们就解决了这个问题,但很正常的,我们又出现了三个问题。。。
问题7-5:似乎是我们需要写一个处理异常的代码

解决方法7-8:那我们就写一个

现象7-1:编译成功!yeah!

有一个警告,但一般来讲不用管他,我们开始cargo bootimage
现象7-2:成功!我们成功地解决了std库的问题并建立了我们的操作系统!

现象7-3:现在,我们开始运行

啊,太完美了,我都要哭出来了,实验内容一的基础部分成功了!
步骤8:进行测试,首先将lib文件进行修改,增加如下代码

记住,这个_start函数将会在运行cargo xtest的时候使用,因为Rust测试lib.rs完全独立于main.rs。在运行测试之前,我们需要在这里调用init来设置IDT。现在,我们可以创建一个test_interpoint_Exception测试:
步骤9:创建一个测试,将interrupts文件增加如下代码:

除了通过串口打印状态消息外,测试还调用int 3函数来触发断点异常。通过检查之后是否继续执行,我们验证我们的断点处理程序是否正常工作。我们可以通过运行Cargo xtest(所有测试)或Cargo xtest-lib(仅测试lib.rs及其模块)来尝试这个新测试。应该在输出中看到test_interpoint_Exception.[ok]。
步骤10,:使用cargo xtest进行全测试,在命令行中输入cargo xtest

问题10-1:说我们的test_case是在一个不稳定库里面调用的,我们的自定义测试框架不稳定。

他建议我们增加一条语句#![feature(custom_test_frameworks)],那我们就试一下。
解决方法10-1:在main文件和lib文件里增加#![feature(custom_test_frameworks)]语句

问题10-2:问题解决了,但我们又多了一个问题,我们找不到test

这是因为我们上一节没有进行,上一节中写了一个serial文件,包含了今天用到的serial_print和serial_println两个宏,我们需要学习上一节的知识,并将serial文件补全
解决方法10-2:编写serial文件并调用

但并没有完全解决,因为我们也要对vga_buffer文件进行修改,同时需要增加很多东西,包括一个名叫test文件夹和里面两个basic_boot.rs和should_panic.rs的文件。

最终的最终,得到的文件是这样的:

main文件:

#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo4_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo4_os::println;
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {println!("Hello World{}", "!");junmo4_os::init();// invoke a breakpoint exceptionx86_64::instructions::interrupts::int3();#[cfg(test)]test_main();println!("It did not crash!");loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {println!("{}", info);loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {junmo4_os::test_panic_handler(info)
}

lib文件:

#![no_std]
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![feature(abi_x86_interrupt)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub mod interrupts;
pub mod vga_buffer;
pub mod serial;
pub fn init() {interrupts::init_idt();
}
pub fn test_runner(tests: &[&dyn Fn()]) {serial_println!("Running {} tests", tests.len());for test in tests {test();}exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {serial_println!("[failed]\n");serial_println!("Error: {}\n", info);exit_qemu(QemuExitCode::Failed);loop {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {Success = 0x10,Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {use x86_64::instructions::port::Port;unsafe {let mut port = Port::new(0xf4);port.write(exit_code as u32);}
}
/// Entry point for `cargo xtest`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {init();test_main();loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {test_panic_handler(info)
}interrupts文件:
#![cfg(not(windows))]
use crate::println;
use lazy_static::lazy_static;
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
lazy_static! {static ref IDT: InterruptDescriptorTable = {let mut idt = InterruptDescriptorTable::new();idt.breakpoint.set_handler_fn(breakpoint_handler);idt};
}
pub fn init_idt() {IDT.load();
}
extern "x86-interrupt" fn breakpoint_handler(stack_frame: &mut InterruptStackFrame) {println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}
#[cfg(test)]
use crate::{serial_print, serial_println};
#[test_case]
fn test_breakpoint_exception() {serial_print!("test_breakpoint_exception...");// invoke a breakpoint exceptionx86_64::instructions::interrupts::int3();serial_println!("[ok]");
}

toml文件:

[package]
name = "junmo4_os"
version = "0.1.0"
authors = ["junmo"]
edition = "2018"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[dependencies]
bootloader = "0.6.0"
volatile = "0.2.3"
spin = "0.4.9"
x86_64 = "0.7.5"#注意注意!!
uart_16550 = "0.2.0"#注意注意!!
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]

vga_buffer文件:

use core::fmt;
use lazy_static::lazy_static;
use spin::Mutex;
use volatile::Volatile;
#[cfg(test)]
use crate::{serial_print, serial_println};
lazy_static! {pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {column_position: 0,color_code: ColorCode::new(Color::Yellow, Color::Black),buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },});
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {Black = 0,Blue = 1,Green = 2,Cyan = 3,Red = 4,Magenta = 5,Brown = 6,LightGray = 7,DarkGray = 8,LightBlue = 9,LightGreen = 10,LightCyan = 11,LightRed = 12,Pink = 13,Yellow = 14,White = 15,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {fn new(foreground: Color, background: Color) -> ColorCode {ColorCode((background as u8) << 4 | (foreground as u8))}
}
/// A screen character in the VGA text buffer, consisting of an ASCII character and a `ColorCode`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {ascii_character: u8,color_code: ColorCode,
}
/// The height of the text buffer (normally 25 lines).
const BUFFER_HEIGHT: usize = 25;
/// The width of the text buffer (normally 80 columns).
const BUFFER_WIDTH: usize = 80;
/// A structure representing the VGA text buffer.
#[repr(transparent)]
struct Buffer {chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
/// A writer type that allows writing ASCII bytes and strings to an underlying `Buffer`.
///
/// Wraps lines at `BUFFER_WIDTH`. Supports newline characters and implements the
/// `core::fmt::Write` trait.
pub struct Writer {column_position: usize,color_code: ColorCode,buffer: &'static mut Buffer,
}
impl Writer {/// Writes an ASCII byte to the buffer.////// Wraps lines at `BUFFER_WIDTH`. Supports the `\n` newline character.pub fn write_byte(&mut self, byte: u8) {match byte {b'\n' => self.new_line(),byte => {if self.column_position >= BUFFER_WIDTH {self.new_line();}let row = BUFFER_HEIGHT - 1;let col = self.column_position;let color_code = self.color_code;self.buffer.chars[row][col].write(ScreenChar {ascii_character: byte,color_code,});self.column_position += 1;}}}/// Writes the given ASCII string to the buffer.////// Wraps lines at `BUFFER_WIDTH`. Supports the `\n` newline character. Does **not**/// support strings with non-ASCII characters, since they can't be printed in the VGA text/// mode.fn write_string(&mut self, s: &str) {for byte in s.bytes() {match byte {// printable ASCII byte or newline0x20..=0x7e | b'\n' => self.write_byte(byte),// not part of printable ASCII range_ => self.write_byte(0xfe),}}}/// Shifts all lines one line up and clears the last row.fn new_line(&mut self) {for row in 1..BUFFER_HEIGHT {for col in 0..BUFFER_WIDTH {let character = self.buffer.chars[row][col].read();self.buffer.chars[row - 1][col].write(character);}}self.clear_row(BUFFER_HEIGHT - 1);self.column_position = 0;}/// Clears a row by overwriting it with blank characters.fn clear_row(&mut self, row: usize) {let blank = ScreenChar {ascii_character: b' ',color_code: self.color_code,};for col in 0..BUFFER_WIDTH {self.buffer.chars[row][col].write(blank);}}
}
impl fmt::Write for Writer {fn write_str(&mut self, s: &str) -> fmt::Result {self.write_string(s);Ok(())}
}
/// Like the `print!` macro in the standard library, but prints to the VGA text buffer.
#[macro_export]
macro_rules! print {($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
/// Like the `println!` macro in the standard library, but prints to the VGA text buffer.
#[macro_export]
macro_rules! println {() => ($crate::print!("\n"));($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
/// Prints the given formatted string to the VGA text buffer through the global `WRITER` instance.
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {use core::fmt::Write;WRITER.lock().write_fmt(args).unwrap();
}
#[test_case]
fn test_println_simple() {serial_print!("test_println... ");println!("test_println_simple output");serial_println!("[ok]");
}
#[test_case]
fn test_println_many() {serial_print!("test_println_many... ");for _ in 0..200 {println!("test_println_many output");}serial_println!("[ok]");
}
#[test_case]
fn test_println_output() {serial_print!("test_println_output... ");let s = "Some test string that fits on a single line";println!("{}", s);for (i, c) in s.chars().enumerate() {let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();assert_eq!(char::from(screen_char.ascii_character), c);}serial_println!("[ok]");
}

serial文件:

use lazy_static::lazy_static;
use spin::Mutex;
use uart_16550::SerialPort;
lazy_static! {pub static ref SERIAL1: Mutex<SerialPort> = {let mut serial_port = unsafe { SerialPort::new(0x3F8) };serial_port.init();Mutex::new(serial_port)};
}
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {use core::fmt::Write;SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {($($arg:tt)*) => {$crate::serial::_print(format_args!($($arg)*));};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {() => ($crate::serial_print!("\n"));($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(concat!($fmt, "\n"), $($arg)*));
}

basic_boot文件:

#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo4_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo4_os::{println, serial_print, serial_println};
use core::panic::PanicInfo;
#[no_mangle] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {test_main();loop {}
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {junmo4_os::test_panic_handler(info)
}
#[test_case]
fn test_println() {serial_print!("test_println... ");println!("test_println output");serial_println!("[ok]");
}

should_panic文件:

#![no_std]
#![no_main]
use junmo4_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {should_fail();serial_println!("[test did not panic]");exit_qemu(QemuExitCode::Failed);loop {}
}
fn should_fail() {serial_print!("should_fail... ");assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {serial_println!("[ok]");exit_qemu(QemuExitCode::Success);loop {}
}

现象10-1:运行结果是这样的。

同时,cargo xrun是可以正常运行的

内容(二):双重故障

简而言之,双重错误是一种特殊的异常,它在CPU无法调用异常处理程序时发生。例如,它在触发页面错误但在中断描述符表(IDT)中没有注册页面错误处理程序时发生。因此,它类似于编程语言中的全部块,但有例外,例如catch(…)C ++或catch(Exception e)Java或C#。
双重故障的行为类似于正常异常。它具有向量号8,我们可以在IDT中为其定义常规处理函数。提供双重故障处理程序确实很重要,因为如果未处理双重故障,则会发生致命的三次故障。无法捕获三重故障,大多数硬件都会做出系统复位反应。
步骤1:触发双重故障。让我们通过触发一个我们没有定义处理程序函数的异常来引发双重错误,我们需要在main文件中修改为如下代码:

我们用不安全的方式写到无效的地址0xdeadbeef。虚拟地址未映射到页表中的物理地址,因此会发生页错误。我们还没有在IDT中注册页面错误处理程序,因此会出现双重错误。当我们现在启动内核时,我们看到它进入了一个无休止的引导循环。启动循环的原因如下:
1、CPU试图写入0xdeadbeef,这会导致页面错误。
2、CPU查看IDT中相应的条目,并看到当前位没有被设置。因此,它不能调用页面错误处理程序,出现双重故障。
3、CPU查看双故障处理程序的IDT条目,但是这个条目也是不存在的。因此,出现三重故障。
4、三重故障是致命的。QEMU像大多数真实的硬件一样对它作出反应,并发出系统重置。
因此,为了防止这种三重故障,我们需要为页面错误提供一个处理程序函数,或者提供一个双故障处理程序。我们希望在所有情况下避免三重故障,因此让我们从一个双故障处理程序开始,该处理程序被所有未处理的异常类型调用。
现象1-1:不断频闪

步骤2:双故障处理器。双故障是带有错误代码的正常异常,因此我们可以指定类似于断点处理程序的处理程序函数。将interrupts中的代码修改如下:

我们的处理程序打印一条简短的错误消息并转储异常堆栈帧。双故障处理程序的错误代码总是为零,因此没有理由打印它。当我们现在启动内核时,我们应该看到双故障处理程序被调用。
现象2-1:双故障处理程序被调用

发生的事情:
CPU执行时试图写入0xdeadbeef,这会导致页面错误。和以前一样,CPU在ID T中查看相应的条目,并看到现在的位没有设置。因此,发生了双重故障。
CPU跳转到现在的双故障处理程序。三重故障(和引导环)不再发生,因为CPU现在可以调用双故障处理程序。
步骤3:处理内核堆栈溢出。
保护页是堆栈底部的特殊内存页,可以检测堆栈溢出。该页没有映射到任何物理框架,因此访问它将导致页面错误,而不是破坏其他内存。引导加载程序为内核堆栈设置一个保护页,因此堆栈溢出会导致页面错误。当发生页面错误时,CPU在IDT中查找页面故障处理程序,并尝试将异常堆栈帧推到堆栈上。
但是,当前堆栈指针仍然指向不存在的保护页。因此,出现第二个页面错误,这会导致双重错误(根据上表)。因此CPU现在试图调用双故障处理程序。
但是,在出现双故障时,CPU也试图推送异常堆栈帧。堆栈指针仍然指向保护页,因此出现第三页错误,这将导致三重故障和系统重新启动。
因此,在这种情况下,我们当前的双故障处理程序无法避免三重故障。
例如,我们将main文件修改如下,将会很轻易地触发内核堆栈溢出:

现象3-1:仍旧是频闪

因此,我们需要确保堆栈在发生双重故障异常时总是有效的。
步骤4:设计开关栈
当出现异常时,x86_64体系结构能够切换到预定义的好堆栈。此开关发生在硬件级别,因此可以在CPU推送异常堆栈帧之前执行。切换机制被实现为中断堆栈表(IST)。IST是一个由7个指针组成的表,指向已知的好堆栈。我们需要创建一个TSS。我们将要新建一个GDT模块,为此需要将lib代码增加一条pub mod gdt指令,并新建一个gdt.rs文件,在其中输入如下代码:

我们使用lazy_static,因为Rust的const评估器还不够强大,无法在编译时进行初始化。我们定义第0个IST条目是双故障堆栈(任何其他IST索引也可以工作)。然后,我们将双故障堆栈的顶部地址写入第0项。我们写顶部地址是因为x86上的堆栈向下增长,即。从高地址到低地址。
我们还没有实现内存管理,所以我们没有一个适当的方法来分配一个新的堆栈。相反,我们现在使用静态mut数组作为堆栈存储。不安全是必需的,因为编译器不能保证在访问可变静态时的种族自由。重要的是,它是静态的mut,而不是不可变的静态,因为否则引导加载程序将将其映射到只读页面。我们将在稍后的帖子中用适当的堆栈分配来取代它,那么在这个地方就不再需要不安全的东西了。
请注意,此双故障堆栈没有保护页,以防止堆栈溢出。这意味着我们不应该在双故障处理程序中做任何堆栈密集的事情,因为堆栈溢出可能会损坏堆栈下面的内存。

步骤5:装载TSS:
装载TSS现在我们创建了一个新的TSS,我们需要一种方法来告诉CPU它应该使用它。不幸的是,这有点麻烦,因为TSS使用分段系统(由于历史原因)。我们不必直接加载表,而是将新的段描述符添加到全局描述符表(GDT)中。然后,我们可以用相应的GDT索引来加载我们的TSS调用LTR指令。(这是我们命名模块GDT的原因。)。
全局描述符表全局描述符表(GDT)是在分页之前使用的用于内存分割的残留的标准。在64位模式下,它仍然需要用于各种事情,如内核/用户模式配置或TSS加载。GDT是一个包含程序段的结构。在分页成为标准之前,在旧的体系结构上使用它来相互隔离程序。虽然在64位模式下不再支持分段,但GDT仍然存在.它主要用于两个方面:在内核空间和用户空间之间切换和加载TSS结构。
让我们创建一个静态GDT,其中包含一个用于TSS静态的段,即在gdt文件里输入如下代码:

我们用代码段和TSS段创建了一个新的GDT。
步骤6:加载GDT
要加载我们的GDT,我们创建了一个新的GDT::init函数,我们从init函数调用,在gdt文件中加入如下代码:

在lib文件中加入如下代码:

现在我们的GDT已经加载(因为_start函数调用init),但是我们仍然可以看到堆栈溢出的引导循环。

步骤7:调用
现在的问题是GDT段还没有活动,因为段和TSS寄存器仍然包含来自旧GDT的值。我们还需要修改双故障IDT条目,以便它使用新堆栈。
总之,我们需要做以下工作:
1、重新加载代码段寄存器:我们更改了GDT,所以我们应该重新加载cs,代码段寄存器。这是必需的,因为旧的段选择器现在可以指向不同的GDT描述符(例如,TSS描述符)。
2、加载TSS:我们加载了一个包含TSS选择器的GDT,但是我们仍然需要告诉CPU它应该使用那个TSS。
3、更新IDT条目:一旦加载了TSS,CPU就可以访问有效的中断堆栈表(IST)。然后,我们可以告诉CPU,它应该通过修改我们的双故障IDT条目使用我们的新的双故障堆栈。
对于前两个步骤,我们需要访问gdt::init函数中的code_selector和tss_selector变量。我们可以通过一个新的选择器结构使它们成为静态的一部分,我们需要在gdt文件中修改并加入如下代码:

现在,我们可以使用选择器重新加载CS段寄存器,并加载我们的TSS,在gdt文件中修改为如下代码:

我们使用set_cs重新加载代码段寄存器,并使用LOAD_TSS加载TSS。函数被标记为不安全,因此我们需要一个不安全的块来调用它们。原因是可能通过加载无效的选择器破坏内存安全。现在我们已经加载了一个有效的TSS和中断堆栈表,我们可以在IDT中为我们的双故障处理程序设置堆栈索引,我们需要在interrupts文件中修改如下代码;

SET_STACK_INDEX方法是不安全的,因为调用方必须确保所使用的索引是有效的,而不是已经用于另一个异常.现在,每当出现双故障时,CPU都应该切换到双故障堆栈。因此,我们能够捕获所有双故障,包括内核堆栈溢出,此时使用cargo xrun应该是可以得到正确的结果:
问题7-1:我们没有定义globaldescripptortable,所以在使用的时候会出错

解决方法7-1:和tss的一样,我们需要在头部加上一行如下的代码:

问题7-2:我们在使用gdt的时候发生了重复定义

解决方法7-2:将其中一个多余的删除
现象7-1:正常运行!实验成功!

这意味着我们成功解决了三重错误的情况。
最终的文件是这样的:
main文件:

#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo5_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo5_os::println;
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {println!("Hello World{}", "!");junmo5_os::init();fn stack_overflow() {stack_overflow(); // for each recursion, the return address is pushed}// trigger a stack overflowstack_overflow();// as before#[cfg(test)]test_main();println!("It did not crash!");loop {}
}
/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {println!("{}", info);loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {junmo5_os::test_panic_handler(info)
}

gdt文件:

use x86_64::VirtAddr;
use x86_64::structures::tss::TaskStateSegment;
use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};
use lazy_static::lazy_static;
pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;
lazy_static! {static ref TSS: TaskStateSegment = {let mut tss = TaskStateSegment::new();tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {const STACK_SIZE: usize = 4096;static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];let stack_start = VirtAddr::from_ptr(unsafe { &STACK });let stack_end = stack_start + STACK_SIZE;stack_end};tss};
}
//use x86_64::structures::gdt::SegmentSelector;
lazy_static! {static ref GDT: (GlobalDescriptorTable, Selectors) = {let mut gdt = GlobalDescriptorTable::new();let code_selector = gdt.add_entry(Descriptor::kernel_code_segment());let tss_selector = gdt.add_entry(Descriptor::tss_segment(&TSS));(gdt, Selectors { code_selector, tss_selector })};
}
struct Selectors {code_selector: SegmentSelector,tss_selector: SegmentSelector,
}
pub fn init() {use x86_64::instructions::segmentation::set_cs;use x86_64::instructions::tables::load_tss;GDT.0.load();unsafe {set_cs(GDT.1.code_selector);load_tss(GDT.1.tss_selector);}
}

interrupts文件:

#![cfg(not(windows))]
use crate::println;
use lazy_static::lazy_static;
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::gdt;
lazy_static! {static ref IDT: InterruptDescriptorTable = {let mut idt = InterruptDescriptorTable::new();idt.breakpoint.set_handler_fn(breakpoint_handler);unsafe {idt.double_fault.set_handler_fn(double_fault_handler).set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new}idt};
}
// new
extern "x86-interrupt" fn double_fault_handler(stack_frame: &mut InterruptStackFrame, _error_code: u64)
{panic!("EXCEPTION: DOUBLE FAULT\n{:#?}", stack_frame);
}
extern "x86-interrupt" fn breakpoint_handler(stack_frame: &mut InterruptStackFrame) {println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
}
pub fn init_idt() {IDT.load();
}
#[cfg(test)]
use crate::{serial_print, serial_println};
#[test_case]
fn test_breakpoint_exception() {serial_print!("test_breakpoint_exception...");// invoke a breakpoint exceptionx86_64::instructions::interrupts::int3();serial_println!("[ok]");
}
lib文件:
#![no_std]
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![feature(abi_x86_interrupt)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"]
use core::panic::PanicInfo;
pub mod interrupts;
pub mod vga_buffer;
pub mod serial;
pub mod gdt;
pub fn init() {gdt::init();interrupts::init_idt();
}
pub fn test_runner(tests: &[&dyn Fn()]) {serial_println!("Running {} tests", tests.len());for test in tests {test();}exit_qemu(QemuExitCode::Success);
}
pub fn test_panic_handler(info: &PanicInfo) -> ! {serial_println!("[failed]\n");serial_println!("Error: {}\n", info);exit_qemu(QemuExitCode::Failed);loop {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {Success = 0x10,Failed = 0x11,
}
pub fn exit_qemu(exit_code: QemuExitCode) {use x86_64::instructions::port::Port;unsafe {let mut port = Port::new(0xf4);port.write(exit_code as u32);}
}
/// Entry point for `cargo xtest`
#[cfg(test)]
#[no_mangle]
pub extern "C" fn _start() -> ! {init();test_main();loop {}
}
#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {test_panic_handler(info)
}

serial文件:

use lazy_static::lazy_static;
use spin::Mutex;
use uart_16550::SerialPort;
lazy_static! {pub static ref SERIAL1: Mutex<SerialPort> = {let mut serial_port = unsafe { SerialPort::new(0x3F8) };serial_port.init();Mutex::new(serial_port)};
}
#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {use core::fmt::Write;SERIAL1.lock().write_fmt(args).expect("Printing to serial failed");
}
/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {($($arg:tt)*) => {$crate::serial::_print(format_args!($($arg)*));};
}
/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {() => ($crate::serial_print!("\n"));($fmt:expr) => ($crate::serial_print!(concat!($fmt, "\n")));($fmt:expr, $($arg:tt)*) => ($crate::serial_print!(concat!($fmt, "\n"), $($arg)*));
}

vga_buffer文件

use core::fmt;
use lazy_static::lazy_static;
use spin::Mutex;
use volatile::Volatile;
#[cfg(test)]
use crate::{serial_print, serial_println};
lazy_static! {pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {column_position: 0,color_code: ColorCode::new(Color::Yellow, Color::Black),buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },});
}
/// The standard color palette in VGA text mode.
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {Black = 0,Blue = 1,Green = 2,Cyan = 3,Red = 4,Magenta = 5,Brown = 6,LightGray = 7,DarkGray = 8,LightBlue = 9,LightGreen = 10,LightCyan = 11,LightRed = 12,Pink = 13,Yellow = 14,White = 15,
}
/// A combination of a foreground and a background color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {/// Create a new `ColorCode` with the given foreground and background colors.fn new(foreground: Color, background: Color) -> ColorCode {ColorCode((background as u8) << 4 | (foreground as u8))}
}
/// A screen character in the VGA text buffer, consisting of an ASCII character and a `ColorCode`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {ascii_character: u8,color_code: ColorCode,
}/// The height of the text buffer (normally 25 lines).
const BUFFER_HEIGHT: usize = 25;
/// The width of the text buffer (normally 80 columns).
const BUFFER_WIDTH: usize = 80;
/// A structure representing the VGA text buffer.
#[repr(transparent)]
struct Buffer {chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
/// A writer type that allows writing ASCII bytes and strings to an underlying `Buffer`.
///
/// Wraps lines at `BUFFER_WIDTH`. Supports newline characters and implements the
/// `core::fmt::Write` trait.
pub struct Writer {column_position: usize,color_code: ColorCode,buffer: &'static mut Buffer,
}
impl Writer {/// Writes an ASCII byte to the buffer.////// Wraps lines at `BUFFER_WIDTH`. Supports the `\n` newline character.pub fn write_byte(&mut self, byte: u8) {match byte {b'\n' => self.new_line(),byte => {if self.column_position >= BUFFER_WIDTH {self.new_line();}let row = BUFFER_HEIGHT - 1;let col = self.column_position;let color_code = self.color_code;self.buffer.chars[row][col].write(ScreenChar {ascii_character: byte,color_code,});self.column_position += 1;}}}fn write_string(&mut self, s: &str) {for byte in s.bytes() {match byte {// printable ASCII byte or newline0x20..=0x7e | b'\n' => self.write_byte(byte),// not part of printable ASCII range_ => self.write_byte(0xfe),}}}/// Shifts all lines one line up and clears the last row.fn new_line(&mut self) {for row in 1..BUFFER_HEIGHT {for col in 0..BUFFER_WIDTH {let character = self.buffer.chars[row][col].read();self.buffer.chars[row - 1][col].write(character);}}self.clear_row(BUFFER_HEIGHT - 1);self.column_position = 0;}/// Clears a row by overwriting it with blank characters.fn clear_row(&mut self, row: usize) {let blank = ScreenChar {ascii_character: b' ',color_code: self.color_code,};for col in 0..BUFFER_WIDTH {self.buffer.chars[row][col].write(blank);}}
}
impl fmt::Write for Writer {fn write_str(&mut self, s: &str) -> fmt::Result {self.write_string(s);Ok(())}
}
/// Like the `print!` macro in the standard library, but prints to the VGA text buffer.
#[macro_export]
macro_rules! print {($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
/// Like the `println!` macro in the standard library, but prints to the VGA text buffer.
#[macro_export]
macro_rules! println {() => ($crate::print!("\n"));($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
/// Prints the given formatted string to the VGA text buffer through the global `WRITER` instance.
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {use core::fmt::Write;WRITER.lock().write_fmt(args).unwrap();
}
#[test_case]
fn test_println_simple() {serial_print!("test_println... ");println!("test_println_simple output");serial_println!("[ok]");
}
#[test_case]
fn test_println_many() {serial_print!("test_println_many... ");/*for _ in 0..200 {println!("test_println_many output");}*/serial_println!("[ok]");
}
#[test_case]
fn test_println_output() {serial_print!("test_println_output... ");let s = "Some test string that fits on a single line";println!("{}", s);for (i, c) in s.chars().enumerate() {let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();assert_eq!(char::from(screen_char.ascii_character), c);}serial_println!("[ok]");
}

basic_boot文件:

#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(junmo5_os::test_runner)]
#![reexport_test_harness_main = "test_main"]
use junmo5_os::{println, serial_print, serial_println};
use core::panic::PanicInfo;
#[no_mangle] // don't mangle the name of this function
pub extern "C" fn _start() -> ! {test_main();loop {}
}
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {junmo5_os::test_panic_handler(info)
}
#[test_case]
fn test_println() {serial_print!("test_println... ");println!("test_println output");serial_println!("[ok]");
}

should_panic文件:

#![no_std]
#![no_main]
use junmo5_os::{exit_qemu, serial_print, serial_println, QemuExitCode};
use core::panic::PanicInfo;
#[no_mangle]
pub extern "C" fn _start() -> ! {should_fail();serial_println!("[test did not panic]");exit_qemu(QemuExitCode::Failed);loop {}
}
fn should_fail() {serial_print!("should_fail... ");assert_eq!(0, 1);
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {serial_println!("[ok]");exit_qemu(QemuExitCode::Success);loop {}
}

这个时候,请注意,我们已经没有办法用cargo xtest进行测试了

步骤8:堆栈溢出测试
为了测试我们的新GDT模块并确保在堆栈溢出时正确调用了双重故障处理程序,我们可以添加一个集成测试。其思想是在测试函数中触发双故障,并验证是否调用了双故障处理程序。让我们从最小骨架开始:
首先,在tests文件夹中新建一个stack_overflow.rs,并加入如下代码

就像我们的panic处理器测试一样,测试将在没有测试工具的情况下运行。原因是我们不能在双重故障后继续执行,所以多个测试是没有意义的。为了禁用测试的测试线束,我们将以下内容添加到我们的Cargo.toml中:

现在,cargo xtest --test stack_overflow应该成功编译。当然,测试失败了,因为未实现的宏观panic。
现象8-1:当前运行时错误的

步骤9:实施启动
_start函数的实现如下所示,我们将stack_overflow文件修改如下:

我们调用我们的gdt::init函数来初始化一个新的gdt。我们没有调用interrupts::init_idt函数,而是调用稍后将解释的init_test_idt函数。原因是我们希望注册一个自定义的双故障处理程序,它执行EXIT_QEMU(QemuExitCode::Success),而不是panick。
堆栈_溢出函数与main.rs中的函数相同。此外,我们还添加了allow(unconditional_recursion)属性,以沉默该函数无休止地递归的警告。
步骤10:测试IDT
如上所述,该测试需要它自己的IDT和一个定制的双故障处理程序。实施方式如下,我们将stack_overflow文件进行如下修改:

该实现与interrupts文件中的正常IDT非常相似,在正常IDT中,我们将堆栈索引设置为双故障处理程序的IST,以便切换到单独的堆栈。INIT_TEST_IDT函数通过load函数方法在CPU上加载IDT。

步骤11:双故障处理器
唯一的缺失是我们的双故障处理程序。将stack_overflow文件进行如下修改

当调用双故障处理程序时,我们使用一个成功的退出代码退出QEMU,该代码将测试标记为已通过。由于集成测试是完全独立的可执行文件,所以我们需要在测试文件的顶部再次设置#![Feature(ABI_x86_interrupt)]属性。
现在,我们可以通过cargo xtest --test stack_overflow(或者cargo xtest来运行所有测试)来运行我们的测试。正如所料,我们看到堆栈溢出…。[OK]控制台中的输出。尝试注释出SET_STACK_INDEX行:它会导致测试失败。
现象11-1:测试成功:

现象11-2:测试失败:

内容(三):阅读 pic8259_simple 库的源码,并在报告中详细报告如何初始化 8259 中断控制器。

代码及分析如下:

//支持8259可编程中断控制器,可处理基本I/O中断。在多核模式下,我们需要用APIC接口来替换它。//
//这里的基本思想是,我们有两个PIC芯片,PIC 1和pic2,而pic2是用来在PIC 1上中断2的。你可以在http://wiki.osdev.org/PIC找到整个叙述。基本上,我们非常复杂的现代芯片组正在从事80年代早期的角色,我们的目标是做到最低限度地得到合理的中断。//
//我们需要在此做的最重要的事情是为我们的两个PICS中的每一个设置基本"偏移",因为默认情况下,PIC1的偏移量为0x8,这意味着来自PIC1的I/O中断将与"一般保护故障"的处理器中断重叠。由于处理器保留了中断0x0至0x1F,我们将PIC1中断移动到0x20-0x27,而PIC2中断将中断至0x28-0x2F。如果我们想写一个DOS仿真器,我们可能需要选择不同的基础中断,因为DOS使用的中断0x21用于系统调用。//
#![feature(const_fn)]
#![no_std]
extern crate cpuio;
/// 命令已发送以开始PIC初始化。
const CMD_INIT: u8 = 0x11;
/// 命令发送以确认中断。
const CMD_END_OF_INTERRUPT: u8 = 0x20;
// 我们想要运行我们的照片的模式。
const MODE_8086: u8 = 0x01;
//单个PIC芯片。这不是导出的,因为我们总是通过下面的“PICS”访问它。
struct Pic {/// 将中断映射到的基本偏移量。offset: u8,/// 我们发送命令的处理器I/O端口。command: cpuio::UnsafePort<u8>,///我们发送和接收数据的处理器I/O端口。data: cpuio::UnsafePort<u8>,
}
impl Pic {/// 我们是否改变了处理指定的中断?(每个PIC处理8个中断。)fn handles_interrupt(&self, interupt_id: u8) -> bool {self.offset <= interupt_id && interupt_id < self.offset + 8}///通知我们一个中断已经被处理,我们准备好了更多。unsafe fn end_of_interrupt(&mut self) {self.command.write(CMD_END_OF_INTERRUPT);}
}
/// 一对连锁PIC控制器。这是x86上的标准设置。
pub struct ChainedPics {pics: [Pic; 2],
}
impl ChainedPics {/// 为标准PIC1和PIC2控制器创建一个新接口,指定所需的中断偏移。pub const unsafe fn new(offset1: u8, offset2: u8) -> ChainedPics {ChainedPics {pics: [Pic {offset: offset1,command: cpuio::UnsafePort::new(0x20),data: cpuio::UnsafePort::new(0x21),},Pic {offset: offset2,command: cpuio::UnsafePort::new(0xA0),data: cpuio::UnsafePort::new(0xA1),},]}}/// 初始化我们的PICS。我们同时将它们初始化在一起,因为它是传统的这样做的,并且因为I/O操作可能不是在较旧的处理器上是瞬时的。pub unsafe fn initialize(&mut self) {// 我们需要在PIC之间添加一个延迟,特别是在较老的主板上。但我们不一定有任何类型的计时器,因为它们中的大多数都需要中断。各种较老版本的Linux和其他PC操作系统都通过将垃圾数据写入端口0x80来解决这一问题,据称,这需要足够长的时间使所有的东西都能在大多数硬件上正常工作。在这里,“等待”是一个结束。//let mut wait_port: cpuio::Port<u8> = cpuio::Port::new(0x80);let mut wait = || { wait_port.write(0) };// 保存我们原来的中断屏蔽,因为我太懒了,无法找出合理的值。当我们做完的时候,我们会恢复这些。//let saved_mask1 = self.pics[0].data.read();let saved_mask2 = self.pics[1].data.read();// 告诉每个PIC,我们将在它的数据端口上发送一个三字节的初始化序列。self.pics[0].command.write(CMD_INIT);wait();self.pics[1].command.write(CMD_INIT);wait();// 字节1:设置我们的基本偏移self.pics[0].data.write(self.pics[0].offset);wait();self.pics[1].data.write(self.pics[1].offset);wait();// 字节2:配置PIC1和PIC2之间的链接。self.pics[0].data.write(4);wait();self.pics[1].data.write(2);wait();// 字节3:设置我们的模式。self.pics[0].data.write(MODE_8086);wait();self.pics[1].data.write(MODE_8086);wait();// 恢复我们保存的数据。self.pics[0].data.write(saved_mask1);self.pics[1].data.write(saved_mask2);}/// 我们能处理这个中断吗?pub fn handles_interrupt(&self, interrupt_id: u8) -> bool {self.pics.iter().any(|p| p.handles_interrupt(interrupt_id))}/// 找到(如果有)我们链中的PICS哪一个需要知道该中断。这很棘手,因为来自pics[1]的所有中断都通过pics[0]链接。pub unsafe fn notify_end_of_interrupt(&mut self, interrupt_id: u8) {if self.handles_interrupt(interrupt_id) {if self.pics[1].handles_interrupt(interrupt_id) {self.pics[1].end_of_interrupt();}self.pics[0].end_of_interrupt();}}
}

内容(四):实现时钟中断的处理

在硬件中断的帖子中,我们设置了可编程中断控制器,以正确地将硬件中断转发到CPU。为了处理这些中断,我们将新条目添加到中断描述符表中,就像我们为异常处理程序一样。我们将学习如何获取定期的计时器中断以及如何从键盘获取输入。
中断提供了一种从附加硬件设备通知CPU的方法。因此,与其让内核定期检查键盘中的新字符(一个称为轮询的进程),键盘还可以将每个按键通知内核。这样做的效率要高得多,因为内核只需要在发生了什么事情时才采取行动。我们无法将所有硬件设备直接连接到CPU。相反,通过一个单独的中断控制器从所有设备聚合中断,然后通知CPU:
大多数中断控制器是可编程的,这意味着它们支持不同优先级的中断。例如,这允许定时器中断比键盘中断更高的优先级,以确保精确的时间保持。与异常不同,硬件中断异步发生。这意味着它们完全独立于所执行的代码并且可以在任何时间发生。因此,我们在内核中突然有一个并发性的形式,所有潜在并发相关的错误。
8259 PIC:Intel 8259是1976年推出的可编程中断控制器(PIC)。长期以来,它已被较新的APIC所取代,但出于向后兼容性的原因,它的接口仍然在当前系统上得到支持。与APIC相比,8259 PIC的建立要容易得多,因此我们将在以后的文章中切换到APIC之前,使用它来介绍中断。8259具有8条中断线和几条用于与CPU通信的线路。当时的典型系统配备了两个8259个PIC的实例,一个初级PIC和一个二级PIC连接到主系统的一条中断线上。
15行的大部分具有固定的映射,例如,次级PIC的线4被分配给鼠标。每个控制器可以通过两个I/O端口、一个“命令”端口和一个“资料”端口来配置。对于主控制器,这些端口是0x20(命令)和0x21(数据)。对于辅助控制器,它们为0xA0(命令)和0xA1(数据)。
步骤1:重新映射。pics的默认配置不可用,因为它向CPU发送范围0-15中的中断向量编号。这些数字已被CPU异常占用,例如8号对应于双故障。
要解决此重叠问题,我们需要将PIC中断重新映射到不同的数字。实际范围无关紧要,只要它不与异常重叠。我们这里选择范围32-47,因为这些是在32个例外时隙之后的第一组自由数字。该配置通过将特殊值写入PICS的命令和数据端口而发生。幸运的是,已经有一个名为PIC8259_SIMPLE的机箱,因此我们不需要自己编写初始化序列。要将机箱添加为依赖项,我们将以下项目添加到项目中,我们需要在toml文件中加入如下代码:

然后设置主/副PIC布局的链式PICS结构,这里需要将interrupts文件加入如下代码:

我们将pics的偏移设置为上面提到的32-47范围。通过在Mutex中封装chainedpics结构,我们可以获得安全的可变访问(通过锁定方法),我们在下一步中需要这样做。chainedpics::新函数不安全,因为错误的偏移可能会导致未定义的行为。我们现在可以在init函数中初始化8259PIC,这里需要将lib文件修改如下:

我们使用Initialize函数执行PIC初始化。与chainedpics::new函数一样,此函数也不安全,因为如果PIC配置不当,则会导致未定义的行为。如果一切顺利,我们应该在执行cargo Xrun时看到"it did not crash"消息。
问题1-1:编译不成功

解决方法1-1:找一个有网络的地方。。。注意,校园网不可以,一定要用别的或者流量
现象1-1:运行成功
步骤2:启动中断。到目前为止,还没有发生任何事情,因为在CPU配置中仍然禁用了中断。这意味着CPU根本不听中断控制器的声音,所以没有中断可以到达CPU。让我们改变这一点,我们将lib文件修改成如下样子:

但发现有双重故障:

造成这种双重故障的原因是硬件定时器(确切地说是Intel 8253)默认启用,所以我们一启用中断就开始接收定时器中断。因为我们还没有为它定义一个处理程序函数,所以会调用我们的双故障处理程序。
步骤3:处理定时器中断。正如我们从上图看到的,计时器使用主PIC的第0行。这意味着它到达CPU作为中断32(0+偏移32)。代替硬编码索引32,我们将其存储在中断索引ENUM中,将interrupts文件增加如下代码:

enum是一个类似C-like enum,因此我们可以直接为每个变量指定索引。repr(U8)属性指定每个变量都表示为U8。我们将在未来为其他中断添加更多的变体。现在,我们可以为计时器中断添加一个处理程序函数,继续对interrupts文件进行修改和添加如下代码:

计时器_中断_处理程序具有与异常处理程序相同的签名,因为CPU对异常和外部中断的响应是相同的(唯一的区别是某些异常会推送错误代码)。InterruptDescriptorTable结构实现了IndexMut特性,因此我们可以通过数组索引语法访问单个条目。在定时器中断处理程序中,我们在屏幕上打印一个点。由于计时器中断是定期发生的,我们希望看到每个计时器滴答上出现一个点。然而,当我们运行它时,我们看到只有一个点被打印出来:

注意,这里需要对main文件进行修改,去掉原本的stack_overflow函数才能得到结果。

步骤4:结束中断。原因是PIC期望从我们的中断处理程序得到一个明确的“中断结束”(EOI)信号。此信号告诉控制器中断已被处理,系统已准备好接收下一个中断。因此PIC认为我们还在忙着处理第一个定时器中断,在发送下一个信号之前耐心地等待EOI信号。要发送EOI,我们再次使用静态PICS结构,对interrupts文件进行如下修改:

notifyendofinterrup确定主PIC还是次PIC发送中断,然后使用命令和数据端口向各个控制器发送EOI信号。如果辅助PIC发送中断,则需要通知两个PIC,因为辅助PIC连接到主PIC的输入行。我们需要小心使用正确的中断矢量号,否则我们可能会意外删除一个重要的未发送中断或导致系统挂起。这是函数不安全的原因。当我们现在执行cargo xrun时,我们会看到屏幕上定期出现的点:

我们使用的硬件定时器称为可编程间隔定时器,简称PIT。如名称所示,可以在两个中断之间配置间隔。我们不会在这里详细讨论,因为我们将很快切换到APIC定时器,但是OSDevwiki有一篇关于配置PIT的广泛文章。
步骤5:死锁。
我们的内核中现在有一个并发的形式:定时器中断是异步发生的,因此它们可以随时中断我们的启动功能。幸运的是,rust的所有权系统在编译时防止了许多类型的并发相关的错误。一个值得注意的例外是死锁。如果线程试图获取永远不会变为空闲的锁,则会发生死锁。从而该线程无限期地悬挂。我们可以在内核中引发死锁。记住,我们的println宏调用vga_buffer::_print函数,它使用spinlock锁定全局写入程序。如图:

它锁定写入器,在其上调用WITH_FMT,并在函数结束时隐式地解锁它。现在,假设在写入器被锁定时发生中断,并且中断处理程序也试图打印一些内容:

WRITER被锁定,因此中断处理程序等待直到它变为空闲。但这永远不会发生,因为start函数只在中断处理程序返回后继续运行。因此,整个系统将挂起。
步骤6:引发死锁:
通过在我们的_start函数末尾打印循环中的东西,我们可以很容易地在内核中引发这种死锁,将main文件修改如下:

运行结果是这样的:

我们看到只有有限数量的连字符被打印出来,直到第一个定时器中断发生。然后系统挂起,因为计时器中断处理程序在试图打印点时会死锁。这就是我们在上面的输出中没有看到点的原因。由于计时器中断是异步发生的,因此每次运行时连字符的实际数量都会有所不同。这种不确定性使得与并发相关的bug很难调试。
步骤7:解决死锁:
为了避免死锁,只要互斥信号量被锁定,我们就可以禁用中断。比方说讲vga_buffer文件修改如下:

without_interrupts 函数采用闭包,并在没有中断的环境中执行。我们使用它来确保只要Mutex被锁定,就不会发生中断。当我们现在运行我们的内核时,我们看到它在不挂起的情况下继续运行。(我们仍然没有注意到任何点,但这是因为它们滚动得太快了。尝试放慢打印速度,例如在循环中放置一个for_in 0…10000{}。)
我们可以将相同的更改应用于我们的串行打印功能,以确保它不会出现死锁。将serial文件修改如下;

请注意,禁用中断不应该是一个通用的解决方案。问题是它增加了最坏的中断延迟,即系统对中断作出反应的时间。因此,中断应该只在很短的时间内被禁用。
步骤8:修正文件。
如果我们运行Cargo xtest,我们可能会看到test_println_Output测试失败。注意注意,这里是“可能”,我做了三次才失败了一次!!

原因是测试和计时器处理程序之间的竞争条件。
测试将一个字符串打印到VGA缓冲区,然后通过手动迭代缓冲区_chars数组来检查输出。出现争用情况是因为计时器中断处理程序可能在println和读取屏幕字符之间运行。请注意,这并不是一个危险的数据竞争,Rust在编译时完全阻止了这一点。有关详细信息,请参阅Rustonom图标。要解决这个问题,我们需要在测试的整个时间内锁定编写器,这样计时器处理程序就不能写一个。在中间的屏幕上。我们需要对vga_buffer文件进行修改:

我们进行了以下更改:
1、我们通过显式地使用lock()方法将写入器锁定在完整的测试中。代替println,我们使用可允许打印到已锁定的写入器的wertelnMarco。
2、为避免另一个死锁,我们禁用测试持续时间的中断。否则,当写入器仍处于锁定状态时,测试可能会中断。
3、由于定时器中断处理程序仍然可以在测试之前运行,所以我们在打印字符串S之前打印一条附加的新行N。这样,我们可以避免当计时器处理程序已经打印过一些时的测试失败。
随着上述变化,cargo Xtest现在确定性地再次成功。这是一个非常无害的比赛条件,只造成测试失败。正如您所想象的那样,由于它们的非确定性特性,其他竞争条件可能更难以调试。幸运的是,rust会阻止我们的数据竞争,这是最严重的竞争条件,因为它们会导致各种未定义的行为,包括系统崩溃和内存损坏。
步骤9:hit指令。
直到现在,我们使用了一个简单的空循环语句在我们的_start和panic函数的结尾。这会导致CPU无休止地旋转,从而按预期工作。但这也是非常低效率的,因为CPU继续全速运行,即使没有工作可做。在运行内核时,您可以在任务管理器中看到这个问题:QEMU进程始终需要接近100%的CPU。我们真正想做的是停止CPU,直到下一个中断到达。这允许CPU进入睡眠状态,在这种状态下消耗的能量要少得多。hlt指令正是这样做的。让我们使用这个指令创建一个节能的无休止循环,我们需要对lib文件进行修改。

instructions::hlt函数只是围绕程序集指令的一个薄包装器。它是安全的,因为它不可能损害内存安全。我们现在可以使用这个hlt_loop,而不是在我们的_start和panic函数中使用没完没了的循环,即将main文件修改如下:

我们也需要对我们lib文件进行更新。

我们也可以用这个函数对interrupts文件进行更新

现在当我们运行的时候,可以看到cpu的利用率降低了很多

步骤10:键盘输入。
现在,我们能够处理来自外部设备的中断,我们终于能够增加对键盘输入的支持。这将允许我们第一次与内核交互。请注意,我们只介绍如何处理这里的PS/2键盘,而不是USB键盘。然而,主板模拟USB键盘作为PS/2设备来支持较早的软件,因此我们可以安全地忽略USB键盘,直到我们的内核有USB支持。
与硬件计时器一样,默认情况下,键盘控制器已启用。因此,当您按键时,键盘控制器向PIC发送中断,PIC将其转发到CPU。CPU查找IDT中的处理程序函数,但相应的条目为空。因此发生双故障。因此,让我们为键盘中断添加处理程序函数。它非常类似于我们为定时器中断定义了处理程序,它只是使用了一个不同的中断号。将interrupts文件修改如下:

正如我们从上图看到的,键盘使用主PIC的第1行。这意味着它作为中断33到达cpu(1偏移量32)。我们将此索引作为新的键盘变量添加到InterruptIndex enum中。我们不需要显式地指定值,因为它默认为前一个值加一个,也就是33。在中断处理程序中,我们打印一个k,并将中断信号的结束发送给中断控制器。
我们现在看到,当我们按下一个键时,屏幕上会出现一个k。然而,这只适用于我们按下的第一个键,即使我们继续按下键,屏幕上也不会出现更多的k键。这是因为键盘控制器不会发送另一个中断,直到我们阅读了所谓的按下键的扫描代码。

步骤11:读取扫描代码。
为了找出哪个键被按下,我们需要查询键盘控制器。我们通过从PS/2控制器的数据端口读取数据来做到这一点,该数据端口是编号为0x60的I/O端口,将interrupts文件进行修改:

我们使用x86_64库的端口类型从键盘的数据端口读取字节。这个字节被称为扫描代码,是一个表示按键/发布的数字。我们还没有对扫描代码做任何事情,只需将其打印到屏幕上:

相邻的键具有相邻的扫描码,并且按下一个键导致不同于释放它的不同的扫描码。但是,我们如何将扫描代码准确地转换为实际的关键动作?
步骤12:解释扫描代码。
扫描码和键之间的映射有三种不同的标准,即所谓的扫描码集.这三者都回到了早期IBM计算机的键盘上:IBMXT、IBM 3270 PC和IBMAT。幸运的是,后来的计算机没有继续定义新的扫描代码集的趋势,而是模仿了现有的集并对它们进行了扩展。今天,大多数键盘可以配置为模仿这三组中的任何一组。
默认情况下,PS/2键盘模拟扫描代码集1(“XT”)。在这个集合中,扫描代码字节的下面7位定义键,最重要的位定义是按下(“0”)还是发布(“1”)。没有出现在原始IBMXT键盘上的键(如键盘上的Enter键)依次生成两个扫描代码:一个0xe0转义字节,然后一个代表该键的字节。
要将扫描代码转换为键,可以使用Match语句,这里对interrupts文件进行修改。

上述代码转换数字键0-9的按键并忽略所有其它键。它使用匹配语句为每个扫描代码分配一个字符或一个字符。然后,它使用iflet来解构可选的密钥。通过在图形中使用相同的变量名称关键字,我们会遮蔽前面的声明,这是一种常见的模式,用于在rust中破坏选项类型。

将其他键转换为相同的方式。幸运的是,有一个名为PC-keyboard的库,用于翻译ScanCode集1和2的ScanCode,因此我们不必自己实施。要使用库,我们将其添加到我们的Cargo.toml中,并将其导入到我们的lib.rs:中,这里需要对toml文件进行修改。

现在我们可以使用这个库重写键盘_中断处理程序,即将interrupts文件修改如下:

我们使用lazy_static宏创建受互斥体保护的静态键盘对象。在每个中断上,我们锁定互斥体,从键盘控制器读取ScanCode,并将其传递到add_byte方法,该方法将ScanCode转换为选项。KeyEvent包含导致该事件的密钥以及该事件是否为引发或释放事件。
要解释此关键事件,我们将其传递到Process_KeyEvent方法,如果可能,该方法将密钥事件转换为字符。例如,根据是否按下Shift键,将A键的按事件转换为小写A字符或大写A字符。现在我们可以输入字符了:

注意,一定要找一个网络好的地方,而且不能是校园网。

三、实验重难点

本次实验与上次实验之间缺少了一节,所以会遇到我一开始所遇到的那个错误,那个应该是我所能遇到的最严重的也最难以解决的错误。在微信群里问了同学,都是卡在那里了,而且更严重的是,网上只有前三节的汉化翻译和过程,后续根本没有中文的。但是,在我最终将他所缺少的那一节(链接在此:https://os.phil-opp.com/testing/)做完之后,我才发现怎么在原有的实验2的基础上进行修改,让我们可以绕过std标准库而继续使用我们的库来写一个专属于我们的操作系统,具体如下。
首先,我们需要在所有的文件(至少是main文件和interrupts文件)中使用#![no_std]指令禁用掉我们的std标准库,但这样还是不彻底的,我们需要对coml文件进行修改,确保里面有features=[“spin_no_std”]指令,这是为了彻底禁用std标准库;x86_64=”0.7.5”指令,这是为了能够调用x86_64库,避免出现找不到x86_64…json的错误
最终得到的coml文件如下:

到此为止,如果你之前的代码是按照网站上来的话,std标准库的错误解决了,但出现了一个新的问题,我们没有办法调用我们写的vga_buffer库里面的println宏:

如果直接使用pub mod vga_buffer或者mod vga_buffer的话,将会出现如下的错误:

这是因为我们是一个多文件程序,在使用其他文件的时候有严格的规定,我们需要在主函数中加入这么一句,声明我们下面中的println宏使用的是junmo4_os这个文件夹中的宏,也就是使用文件夹中我们所写的宏

但问题还没有解决,因为编译器找不到我们定义的宏

这是因为我们在所有的文件中都没有引入vga_buffer这个我们自己写的库,所以没有办法调用我们写的println宏,这里需要在lib文件中引用我们的vga_buffer文件,这个时候才是可以的,不会出现上文的错误,因为他知道去哪里找vga_buffer,而不是漫无目的地寻找:

但是,我们在调用x86-interrupt的abi的时候可能会发生奇怪的变化

这个与x86的中断机制有关系,应该是我们还没有声明或者说代码找不到这个x86-interrupt abi库,所以我们需要在lib文件里加上这么一条语句,将这个库引入进来。

到此为止,如果之前所有的代码都是正常的,和网页上的一模一样,那么恭喜你,你将会得到这么一个结果。

IT did not crash 他没有崩溃,现在你可以崩溃了。

四、实验心得体会

这次实验我做了两个周,从实验二验收之前开始,一直到第九周期中考试才算整体完成并写完了实验报告。实验报告的完成时间已经到11月6号中午三点了。
有意思的是,这个实验本身的难度并不大,后面的内容二、和四基本上按照网页上的步骤来就可以了,三的话需要自己的英文水平足够扎实,能够翻译就可以,但是,但是,这个实验的难点就在于他缺少了一节,一节十分重要的内容,教我们如何进行测试,如何用cargo xtest进行中断检测,由于缺少了这一环,我们没有办法从实验二的基础上做出实验三,大多数人都是卡在了这里,而我已经在本实验报告的第三部分详细的说明了如何解决“无法使用std标准库”的问题。
这次实验步骤比较明确,但细节知识点极多,很有挑战性。这个实验报告是我写的最长的一篇,甚至超过了当初的cpu实验报告,总的来说,很自豪,因为自己找到了一个我以前认为无解的问题的解,真的是自己摸索出来的。

操作系统原理实验(3):操作系统的基石中断与异常相关推荐

  1. Linux操作系统原理与应用05:中断和异常

    目录 1. 中断概述 1.1 什么是中断 1.2 为什么引入中断 1.3 中断的分类 1.4 CPU什么时候响应中断 2. 中断控制器简介 2.1 中断的C/S模型结构 2.2 作为中介的中断控制器 ...

  2. 操作系统原理实验-进程同步

    操作系统原理实验报告 实验题目 实验二进程同步 实验二.进程同步 1.1 实验目的 现代操作系统的核心是多道程序设计.多处理器和分布式处理器,这些方案和操作系统设计技术的基础都是并发.当多个进程并发执 ...

  3. ZUCC_操作系统原理实验_Lab9进程的通信消息队列

    lab9进程的通信–消息队列 一.两个进程并发执行,通过消息队列,分别进行消息的发送和接收 1.代码: //接受消息 #include<stdio.h> #include<stdli ...

  4. ZUCC_操作系统原理实验_实验九 消息队列

    操作系统原理实验报告 课程名称 操作系统原理实验 实验项目名称 实验九 消息队列 实验目的 了解 Linux 系统的进程间通信机构 (IPC): 理解Linux 关于消息队列的概念: 掌握 Linux ...

  5. 操作系统原理 实验1、2

    操作系统原理 实验1.2 1.高响应比作业调度 代码示例 #include<malloc.h> #include<stdio.h> #include<string.h&g ...

  6. 操作系统原理实验二(三)

    继续完成操作系统原理的实验 4.5(实验目的:熟悉Window线程创建过程)在windows环境下,利用高级语言编程环境(限定为VS环境或VC环境)调用CreateThread函数实现"并发 ...

  7. 操作系统原理实验(四)深渊:竞争条件与死锁(硬件中断)

    硬件中断 中断控制器 启动中断 处理定时器中断 死锁 竞争条件 HLT指令 键盘输入 中断控制器 中断提供了一种从附加硬件设备通知CPU的方法.这个英特尔8259是1976年推出的可编程中断控制器(P ...

  8. 操作系统原理实验(5):内存管理

    一.实验目的 分页内存管理是内存管理的基本方法之一.本实验的目的在于全面理解分页式内存管理的基本方法以及访问页表,完成地址转换等的方法. 二.实验过程&错误 内容(一):设计不同的方式引发页错 ...

  9. 【操作系统】实验 模拟操作系统的存储系统

    模拟分页式存储管理中硬件的地址转换和产生缺页中断 (1)分页式虚拟存储系统是把作业信息的副本存放在磁盘上,当作业被选中时,可把作业的开始几页先装入主存且启动执行.为此,在为作业建立页表时,应说明哪些页 ...

最新文章

  1. R语言ggplot2可视化强制设置x轴、y轴坐标的起始点为0或者其他实战
  2. CentOS PPTP ×××
  3. android开发字体样式,Android开发中修改程序字体的样式
  4. 21天学通python电子版-小数据池,深浅拷贝,集合+菜中菜
  5. 使用websockets,后台实时发数据,前台实时接受数据,并集成到Django
  6. 关于写文章的一点经验
  7. html文件中script标签放在哪里?
  8. Azure 命令行工具大混战,都是什么,该选哪个?
  9. 前端学习(1530):钩子函数--代码演示(面试重点)
  10. (转)分布式锁的几种使用方式(redis、zookeeper、数据库)
  11. Leaving Auction CF 749D
  12. go语言学习--string、int、int64互相转换,字符串的截取,数组和字符串的转换
  13. python 注销一大段_Django框架用户注销功能实现方法分析
  14. 句句真研—每日长难句打卡Day16
  15. Windows命令行netsh winsock reset解决网络连接问题
  16. win10资源管理器打开缓慢,自动搜索
  17. SuperMap iDesktopX 数据迁移
  18. 层次分析与一致性检验
  19. 14届数独-真题标准数独-Day 5-20220120
  20. java求1到100素数的和_求解JAVA题:求出1~100中所有质数?,求一段java代码,题目是求1到100之间所有质数和?...

热门文章

  1. 基于android端计步器软件的尝试
  2. 奇思妙想 CSS 文字动画
  3. 调用微信的收货地址和我的地址功能页面。
  4. 宝宝 天天加班, 有意义吗
  5. oracle 插入表数据时,自动生成ID
  6. 【图文教程】Shell基础知识
  7. 为何64位的.NET程序不能申请超过2G的空间
  8. html5 特效 banner,精品配饰活动banner html5特效制作教程
  9. 台式机开机黑屏一直闪小横杠,键盘灯鼠标均正常,主板亮
  10. 手机的模拟,有耗电和充电方法, 有电量的属性