精华内容
下载资源
问答
  • 微机原理-汇编语言系统中断实验-实验报告,有代码,有注释,完全看得懂的那种
  • 实验的目的在于深刻理解(类)中断原理和机制,掌握 CPU 访问设备控制器的方法,掌握 x86 体系结构的(类)中断机制和规范,实现时钟中断服务和部分异常处理等。 二、实验过程&错误 内容(一):实现 ...

    一、实验目的

    中断、异常和陷阱指令(合称类中断)是操作系统的基石,现代操作系统就是由(类)中断驱动的。本实验的目的在于深刻理解(类)中断的原理和机制,掌握 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 exception
        x86_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 exception
        x86_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 newline
                    0x20..=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 overflow
        stack_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 exception
        x86_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 newline
                    0x20..=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中断将与"一般保护故障"的处理器中断重叠。由于处理器保留了中断0x00x1F,我们将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实验报告,总的来说,很自豪,因为自己找到了一个我以前认为无解的问题的解,真的是自己摸索出来的。

    展开全文
  • 计算机组成原理实验七:中断实验

    千次阅读 2019-04-24 09:02:36
    加深理解计算机系统中断的工作原理及处理过程。 学习和掌握中断产生、响应、处理等技术。 掌握中断服务子程序的编写要点,进行一次硬、软件的综合调试。 二、实验设备与器材      ~~~~~&...

    一、实验目的

    1. 加深理解计算机系统中断的工作原理及处理过程。
    2. 学习和掌握中断产生、响应、处理等技术。
    3. 掌握中断服务子程序的编写要点,进行一次硬、软件的综合调试。

    二、实验设备与器材

         ~~~~~ TEC-XP+教学实验系统和仿真终端软件PCEC。

    三、实验说明和原理

    1. 要求中断隐指令中执行关中断功能,如果用户中断服务程序允许被中断,必须 在中断程序中执行EI开中断命令。
    2. 教学机的中断系统共支持三级中断,由三个无锁按键确定 从右到左依次为一、二、三级中断,对应的P1、P0的编码分别是01、10、11,优先级也依次升高,这决定 了它们的中断向量,为XXX4、XXX8、XXXC。可以看到,每级中断实际可用空间只有四个字节,故这个空间一般只存放一条转移指令,而真正的用户中断服务程序则存放在转移指令所指向的地址。
    3. 用户需扩展中断隐指令、开中断指令、产中断指令、中断返回指令及其节拍。

    四、实验内容

    1. 扩展中断隐指令,为中断隐指令分配节拍,中断隐指令用到12个节拍,为了和一般指令相区别,应将其节拍T3设计为1.
    2. 扩展开中断指令EI、关中断指令DI、中断返回指令IRET。
    3. 确定中断向量地址。中断微量的高12位由开关确定为(0001001000000)。三级中断对应的中断微量为2404H、2408H、240CH。当有中断请求且被响应后,将执行存放在该中断的中断微量所指向的内存区的指令。
    4. 真写中断微量表。在上述2404H、2408H、240CH地址写入三条JR转移指令,JR指令的OFFSSET是-128~127之间,但在PCEC16中输入时,用户不需要计算偏移量,直接输入要转向的绝对地址即可。
    5. 编写中断服务程序。中断服务程序可以放在中断微量表之后,中断服务程序可以实现在程序正常运行时在计算机屏幕上显示与优先级相对应的不同字符。
    6. 写主程序。可编写一死循环,要求先开中断。

    五、实验步骤与中断程序

    截图结果如下:
    在这里插入图片描述
    在这里插入图片描述

    六、实验心得

        ~~~~ 通过此次试验,我加深理解计算机系统中断的工作原理及处理过程,学习和掌握了中断产生、响应、处理等技术,掌握中断服务子程序的编写要点,进行一次硬、软件的综合调试。
    由于本次实验课前,计算机组成原理课堂上进行了关于中断机制,中断向量表的学习,因此在本次实验课上得心应手。
    本次除了敲上课本所附代码外,自行设计了一个中断程序,虽然过程中遇到些许麻烦,不过最终得以解决,过程中,对中断的产生,响应以及处理机制加深了了解和熟悉。
    在学习中断时,应学会主动制造错误,如除法溢出等出发中断,然后逐步了解其响应过程,以及通过中断向量表调用内存中自己所写入的程序代码,从而加深对中断整个过程的了解。
    从单片机中的学习,清楚明白到中断机制的存在为底层编程予以极大方便和灵活性,因此熟悉中断机制是我们深入学习嵌入式系统的前提。

    展开全文
  • 实验目的 通过本实验掌握软中断的基本原理掌握中断信号的使用进程的创建以及系统计时器的使用 2.实验内容上交的实验2统一取名为test2) 由父进程创建两个子进程通过终端输入Crtl+\组合键向父进程发送SIGQUIT软中断...
  • 一个主PIC和一个从PIC连接到主系统的一条中断线上,一共15个端口。 pics的默认配置不可用,因为它向CPU发送0-15范围内的中断向量号。这些数字已经被CPU异常占用。为了解决这个重叠问题,我们需要将PIC中断重新映射到...

    中断控制器

    中断提供了一种从附加硬件设备通知CPU的方法。这个英特尔8259是1976年推出的可编程中断控制器(PIC)。8259具有8条中断线和几条用于与CPU通信的线路。一个主PIC和一个从PIC连接到主系统的一条中断线上,一共15个端口。

    pics的默认配置不可用,因为它向CPU发送0-15范围内的中断向量号。这些数字已经被CPU异常占用。为了解决这个重叠问题,我们需要将PIC中断重新映射到不同的数字。只要不与异常重叠,实际范围就无关紧要,但通常选择的范围是32-47,因为这是32个例外插槽之后的第一个空闲数字。

    配置是通过向pics的命令和数据端口写入特殊值来实现的。幸运的是,已经有一个箱子叫做pic8259_simple,所以我们不需要自己编写初始化序列。
    我们将以下内容添加到我们的项目中:

    # in Cargo.toml
    
    [dependencies]
    pic8259_simple = "0.2.0"
    

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

    // in src/interrupts.rs
    
    use pic8259_simple::ChainedPics;
    use spin;
    
    pub const PIC_1_OFFSET: u8 = 32;
    pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;
    
    pub static PICS: spin::Mutex<ChainedPics> =
        spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });
    

    我们使用initialize函数来执行PIC初始化。

    // in src/lib.rs
    
    pub fn init() {
        gdt::init();
        interrupts::init_idt();
        unsafe { interrupts::PICS.lock().initialize() }; // new
    }
    

    cargo xrun 一切正常

    启动中断

    因为在CPU配置中禁用了中断,所以我们来开启中断

    // in src/lib.rs
    
    pub fn init() {
        gdt::init();
        interrupts::init_idt();
        unsafe { interrupts::PICS.lock().initialize() };
        x86_64::instructions::interrupts::enable();     // new
    }
    

    这个interrupts::enable的功能x86_64机箱执行特殊sti指令(“设置中断”)以启用外部中断。
    当我们尝试cargo xrun现在,我们看到出现了双重故障:
    在这里插入图片描述
    造成这种双重故障的原因是硬件定时器在默认情况下是启用的,所以一旦启用中断,我们就开始接收定时器中断。因为我们还没有为它定义一个处理程序函数,所以会调用我们的双故障处理程序。

    处理定时器中断

    计时器使用主PIC的第0行,这意味着它作为中断32到达CPU(0+偏移量32)。而不是硬编码索引32,我们将其存储在InterruptIndex枚举:

    现在,我们可以为计时器中断添加一个处理程序函数:

    // in src/interrupts.rs
    
    use crate::print;
    
    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[InterruptIndex::Timer.as_usize()]
                .set_handler_fn(timer_interrupt_handler); // new
    
            idt
        };
    }
    
    extern "x86-interrupt" fn timer_interrupt_handler(
        _stack_frame: &mut InterruptStackFrame)
    {
        print!(".");
    }
    

    注意要将main.rs文件中的stack_overflow注释掉,如图所示
    在这里插入图片描述
    在定时器中断处理程序中,我们在屏幕上打印一个点。由于计时器中断是定期发生的,我们希望看到每个计时器滴答上出现一个点。然而,当我们运行它时,我们看到只有一个点被打印出来:

    在这里插入图片描述
    中断结束
    上图所示只打印了一个点的原因是没有设置中断信号,PIC认为我们还在处理第一个定时器中断,一直等待中断结束的信号

    为了发送EOI(中断结束信号),我们使用我们的静电PICS再次构造:

    // in src/interrupts.rs
    
    extern "x86-interrupt" fn timer_interrupt_handler(
        _stack_frame: &mut InterruptStackFrame)
    {
        print!(".");
    
        unsafe {
            PICS.lock()
                .notify_end_of_interrupt(InterruptIndex::Timer.as_u8());
        }
    }
    

    这个notify_end_of_interrupt计算出主要的或次要的PIC是否发送了中断,然后使用command和data将EOI信号发送给相应控制器的端口。

    当我们现在执行cargo xrun我们看到屏幕上周期性地出现点:在这里插入图片描述

    死锁

    现在内核中有一种并发形式:计时器中断是异步发生的,因此它们可以中断我们的_start随时起作用。幸运的是,Rust的所有权系统防止了编译时与并发相关的许多类型的错误。
    一个值得注意的例外是死锁。如果线程试图获取永远不会释放的锁,则会发生死锁。因此,线程无限期地挂起。

    例如

    // in src/vga_buffer.rs
    
    []
    
    #[doc(hidden)]
    pub fn _print(args: fmt::Arguments) {
        use core::fmt::Write;
        WRITER.lock().write_fmt(args).unwrap();
    }
    

    这个代码不用添加,已经存在
    它锁定WRITER,电话write_fmt并在函数结束时隐式地解锁。现在,假设一个中断发生在WRITER被锁定,中断处理程序也尝试打印一些内容。
    这个WRITER被锁定,因此中断处理程序等待直到空闲。但这从未发生因为_start函数只在中断处理程序返回后继续运行(打印内容)。因此,整个系统挂起。
    触发死锁
    我们可以很容易地在内核中触发这样的死锁,方法是在循环的末尾打印一些东西。

    // in src/main.rs
    
    #[no_mangle]
    pub extern "C" fn _start() -> ! {
        println!("Hello World{}", "!");
        lyh_os::init();
        println!("It did not crash!");
        loop {
            use lyh_os::print;
            print!("-");        // new
        }
    }
    

    当我们在QEMU中运行它时
    在这里插入图片描述
    我们看到只有有限数量的连字符被打印出来,直到第一个定时器中断发生。然后系统挂起,因为计时器中断处理程序在试图打印点时会死锁。这就是我们在上面的输出中没有看到点的原因。
    避免死锁
    为了避免这种死锁,我们可以禁用中断,只要Mutex被锁定:

    // in src/vga_buffer.rs
    
    /// 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;
        use x86_64::instructions::interrupts;   // new
    
        interrupts::without_interrupts(|| {     // new
            WRITER.lock().write_fmt(args).unwrap();
        });
    }
    

    这个without_interrupts函数采用封闭并在没有中断的环境中执行。我们使用它来确保只要Mutex被锁上了。当我们现在运行我们的内核时,我们看到它在不挂起的情况下继续运行。总的来说就是禁用中断。

    我们可以将相同的更改应用于我们的串行打印功能,以确保它不会出现死锁:

    // in src/serial.rs
    
    #[doc(hidden)]
    pub fn _print(args: ::core::fmt::Arguments) {
        use core::fmt::Write;
        use x86_64::instructions::interrupts;       // new
    
        interrupts::without_interrupts(|| {         // new
            SERIAL1
                .lock()
                .write_fmt(args)
                .expect("Printing to serial failed");
        });
    }
    

    禁用中断不应该是一个通用的解决方案,问题是它增加了最坏的中断延迟

    竞争条件

    如果你跑cargo xtest你可能会看到test_println_output测试失败:
    在这里插入图片描述
    原因是测试和计时器处理程序之间的竞争条件。
    测试看起来是这样的:

    // in src/vga_buffer.rs
    
    #[test_case]
    fn 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);
        }
    }
    

    发生竞争条件是因为计时器中断处理程序可能在println以及屏幕字符的读取之间运行。

    要解决这个问题,我们执行了以下更改。
    1、将lock()写入完整的测试中,用允许打印到已锁定的写入器的writeln代替println
    2、避免死锁,测试期间禁用中断
    3、定时器中断可能在测试之前运行,所以在打印字符串s前打印一额外行

    // in src/vga_buffer.rs
    
    #[test_case]
    fn test_println_output() {
        use core::fmt::Write;
        use x86_64::instructions::interrupts;
    
        let s = "Some test string that fits on a single line";
        interrupts::without_interrupts(|| {
            let mut writer = WRITER.lock();
            writeln!(writer, "\n{}", s).expect("writeln failed");
            for (i, c) in s.chars().enumerate() {
                let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read();
                assert_eq!(char::from(screen_char.ascii_character), c);
            }
        });
    }
    

    HLT指令

    因为我们使用了简单的空循环语句,使得CPU不停的工作,非常的低效率
    所以我们让CPU进入休息的状态,知道下一个中断到达

    // in src/lib.rs
    
    pub fn hlt_loop() -> ! {
        loop {
            x86_64::instructions::hlt();
        }
    }
    

    我们现在可以用这个hlt_loop而不是无尽的循环_start和panic职能:

    // in src/main.rs
    
    #[no_mangle]
    pub extern "C" fn _start() -> ! {
        println!("Hello World{}", "!");
        lyh_os::init();
        println!("It did not crash!");
        lyh_os::hlt_loop();            // new
    }
    
    
    #[cfg(not(test))]
    #[panic_handler]
    fn panic(info: &PanicInfo) -> ! {
        println!("{}", info);
        lyh_os::hlt_loop();            // new
    }
    

    让我们更新我们的lib.rs

    // in src/lib.rs
    
    /// Entry point for `cargo test`
    #[cfg(test)]
    #[no_mangle]
    pub extern "C" fn _start() -> ! {
        init();
        test_main();
        hlt_loop();         // new
    }
    
    pub fn test_panic_handler(info: &PanicInfo) -> ! {
        serial_println!("[failed]\n");
        serial_println!("Error: {}\n", info);
        exit_qemu(QemuExitCode::Failed);
        hlt_loop();         // new
    }
    

    当我们现在在QEMU中运行内核时,我们看到CPU的使用率要低得多。

    键盘输入

    让我们为键盘中断添加一个处理程序函数。它非常类似于我们如何定义定时器中断的处理程序,它只是使用了一个不同的中断号:

    // in src/interrupts.rs
    
    #[derive(Debug, Clone, Copy)]
    #[repr(u8)]
    pub enum InterruptIndex {
        Timer = PIC_1_OFFSET,
        Keyboard, // new
    }
    
    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[InterruptIndex::Timer.as_usize()]
                .set_handler_fn(timer_interrupt_handler);
            // new
            idt[InterruptIndex::Keyboard.as_usize()]
                .set_handler_fn(keyboard_interrupt_handler);
    
            idt
        };
    }
    
    extern "x86-interrupt" fn keyboard_interrupt_handler(
        _stack_frame: &mut InterruptStackFrame)
    {
        print!("k");
    
        unsafe {
            PICS.lock()
                .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
        }
    }
    

    在中断处理程序中,我们打印一个k并将中断信号的结束发送给中断控制器。
    读取扫描代码
    找出哪一个按下键后,需要查询键盘控制器。我们通过从ps/2控制器的数据端口读取数据来做到这一点,这是I/O端口带号0x60:

    // in src/interrupts.rs
    
    extern "x86-interrupt" fn keyboard_interrupt_handler(
        _stack_frame: &mut InterruptStackFrame)
    {
        use x86_64::instructions::port::Port;
    
        let mut port = Port::new(0x60);
        let scancode: u8 = unsafe { port.read() };
        print!("{}", scancode);
    
        unsafe {
            PICS.lock()
                .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
        }
    }
    

    我们使用Port类型x86_64从键盘的数据端口读取字节(扫描码)。
    当我们按下键盘
    在这里插入图片描述
    解读扫描码
    要将扫描代码转换为键,可以使用Match语句:

    // in src/interrupts.rs
    
    extern "x86-interrupt" fn keyboard_interrupt_handler(
        _stack_frame: &mut InterruptStackFrame)
    {
        use x86_64::instructions::port::Port;
    
        let mut port = Port::new(0x60);
        let scancode: u8 = unsafe { port.read() };
    
        // new
        let key = match scancode {
            0x02 => Some('1'),
            0x03 => Some('2'),
            0x04 => Some('3'),
            0x05 => Some('4'),
            0x06 => Some('5'),
            0x07 => Some('6'),
            0x08 => Some('7'),
            0x09 => Some('8'),
            0x0a => Some('9'),
            0x0b => Some('0'),
            _ => None,
        };
        if let Some(key) = key {
            print!("{}", key);
        }
    
        unsafe {
            PICS.lock()
                .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
        }
    }
    

    上面的代码翻译数字键0-9的按键,忽略所有其他键。
    当我们按下0-9键,就会显示相应的数字
    在这里插入图片描述
    翻译其他键的方式也是一样的。幸运的是有一个名为pc-keyboard库用于翻译扫描代码集1和2的扫描代码,因此我们不需要自己实现这一点。要使用这个库,我们将它添加到Cargo.toml并将其导入我们的lib.rs:

    # in Cargo.toml
    
    [dependencies]
    pc-keyboard = "0.5.0"
    

    现在我们可以用这个库重写我们的keyboard_interrupt_handler:

    // in/src/interrupts.rs
    
    extern "x86-interrupt" fn keyboard_interrupt_handler(
        _stack_frame: &mut InterruptStackFrame)
    {
        use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};
        use spin::Mutex;
        use x86_64::instructions::port::Port;
    
        lazy_static! {
            static ref KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> =
                Mutex::new(Keyboard::new(layouts::Us104Key, ScancodeSet1,
                    HandleControl::Ignore)
                );
        }
    
        let mut keyboard = KEYBOARD.lock();
        let mut port = Port::new(0x60);
    
        let scancode: u8 = unsafe { port.read() };
        if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
            if let Some(key) = keyboard.process_keyevent(key_event) {
                match key {
                    DecodedKey::Unicode(character) => print!("{}", character),
                    DecodedKey::RawKey(key) => print!("{:?}", key),
                }
            }
        }
    
        unsafe {
            PICS.lock()
                .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
        }
    }
    

    在每次中断时,我们锁定互斥锁,从键盘控制器读取扫描代码,并将其传递给add_byte方法,该方法将扫描代码转换为Option。这个KeyEvent包含导致事件的键以及它是新闻事件还是发布事件。

    要解释这个关键事件,我们将它传递给process_keyevent方法,如果可能,将键事件转换为字符。例如,翻译A小写的键a字符或大写A字符,取决于是否按下Shift键。

    使用这个修改后的中断处理程序,我们现在可以编写文本:
    在这里插入图片描述
    大写也是可以的。
    操作系统概念1~5次实验报告:https://download.csdn.net/download/weixin_43979304/15321050?spm=1001.2014.3001.5503

    展开全文
  • 掌握PC机中断处理系统的基本原理,学会编写中断服务程序。 实验内容 连接好线路后,直接用手动产生单脉冲作为中断请求信号要求每按一次开关产生一次中断,在屏幕上显示一次“IRQ10”,中断10次后退出程序。 源程序 ...

    IRQ10的中断程序

    实验目的

    掌握PC机中断处理系统的基本原理,学会编写中断服务程序。

    实验内容

    连接好线路后,直接用手动产生单脉冲作为中断请求信号要求每按一次开关产生一次中断,在屏幕上显示一次“IRQ10”,中断10次后退出程序。

    源程序

    DATA SEGMENT
    MESS DB 'IRQ10!',0DH,0AH,'$'
    DATA ENDS
    CODE SEGMENT
    ASSUME CS: CODE, DS:DATA
    START:
        MOV AX, CS
        MOV DS, AX
        MOV DX, OFFSET INT10
        MOV AX,2572H
       ; INT 21H                        ;设置IRQ2的中断向量
       ; IN AL,21H                      ;读中断屏蔽控制器
        ;AND AL,0FBH
       ; OUT 21H, AL                    ;开放IRQ2中断,IMR
        
        INT 21H  
        IN AL,0A1H                     
        AND AL,0FBH
        OUT 0A1H, AL                   ;开放IRO10中断
        
        MOV CX,10                      ;记中断循环次数为10次
        STI
    LL: 
        JMP LL                         ;等待脉冲
    INT10:                              ;中断服务程序
        MOV AX, DATA
        MOV DS, AX
        MOV DX, OFFSET MESS 
        MOV AH,09                      ;显示每次中断提示信息
        INT 21H
        MOV AL,20H
        OUT 20H, AL                    ;发出EOI结束命令
        OUT 0A0H,AL 
        LOOP NEXT
    ;    IN AL,21H
    ;    OR AL,04H                      ;关闭IRQ2中断
    ;    OUT 21H, AL
        
        IN AL,0A1H                     ;关闭IRQ10中断
        OR AL,04H
        OUT 0A1H,AL
        
        STI                            ;置中断标志位
        MOV AH,4CH                     ;返回DOS
        INT 21H
    NEXT: IRET
        CODE ENDS
    END START
    

    实验结果

    编译以及运行成功后,手动产生单脉冲,每按下一次按键,就在屏幕上显示“IR10”,中断10次,即按下10次按键后,程序结束。

    IRQ3和IRQ10的中断嵌套程序

    实验内容

    手动产生单脉冲作为中断请求信号,进行IRQ10或者IRQ3中断操作,在屏幕显示中断信息时,按下另一个脉冲按钮,进行中断,屏幕上中断信息会被打断,进而显示另一个中断类型的中断信息。

    源代码

    DATA SEGMENT
    MESS DB 'IRQ10!',0DH,0AH,'$'
    MEES DB 'IRQ3!',0DH,0AH,'$'
    DATA ENDS
    CODE SEGMENT
    ASSUME CS: CODE, DS:DATA
    .386
    START:
        MOV AX, CS
        MOV DS, AX
        MOV DX, OFFSET INT10
        MOV AX,2572H
        INT 21H   ;设置IRQ10的中断向量
    
        MOV DX, OFFSET INT3
        MOV AX,250BH
        INT 21H   ;设置IRQ10的中断向量
      
        IN AL,21H   ;读中断屏蔽控制器
        AND AL,0F3H   ;开放IRQ2 IRQ3中断 
        OUT 21H, AL
    
        IN AL,0A1H   ;读中断屏蔽控制器
        AND AL,0FBH   ;开放IRQ10中断
        OUT 0A1H, AL
       ; MOV CX,10  ;记中断循环次数为10次
        STI
        
    LL:  JMP LL
    INT10:        ;中断服务程序
      PUSHAD
      PUSHFD
      
        MOV CX,10  ;记中断循环次数为10次
        MOV AX, DATA
        MOV DS, AX
        CC:   MOV DX, OFFSET MESS 
        MOV AH,09    ;显示每次中断提示信息
        INT 21H
        CALL DELAY
        LOOP CC
           MOV AL,20H
        OUT 20H, AL   ;发出EOI结束命令
        MOV AL,0A0H
        OUT 0A0H, AL   ;发出EOI结束命令
     
       IN AL,21H
       OR AL,04H    ;关闭IRQ2中断
       OUT 21H, AL
        OUT 0A1H,AL
        POPFD
        POPAD
    
        STI      ;置中断标志位
        IRET
        MOV AH,4CH    ;返回DOS
        INT 21H
      
    
    INT3:        ;中断服务程序
     PUSHAD
      PUSHFD
    MOV CX,10
        MOV AX, DATA
        MOV DS, AX
     bb:   MOV DX, OFFSET MEES 
        MOV AH,09    ;显示每次中断提示信息
        INT 21H
        CALL DELAY
        LOOP BB
        
        MOV AL,20H
        OUT 20H, AL   ;发出EOI结束命令
       
       IN AL,21H
       OR AL,08H    ;关闭IRQ3中断
       OUT 21H, AL
       POPFD
        POPAD
        STI      ;置中断标志位
          IRET
        MOV AH,4CH    ;返回DOS
        INT 21H
    
    
    DELAY  PROC  NEAR         ;延时子程序
    PUSH BX
    PUSH CX
            MOV BX,100H
    lll:   MOV CX,0
    llLL:    LOOP llLL
            DEC BX
            JNE lll
            POP CX
            POP BX
            RET
    DELAY ENDP
     
        CODE ENDS
        END START
    
    
    展开全文

空空如也

空空如也

1 2 3 4 5 ... 18
收藏数 343
精华内容 137
关键字:

中断系统实验原理