mirror of
https://github.com/phil-opp/blog_os.git
synced 2025-12-16 14:27:49 +00:00
526: Rewrite breakpoint test r=phil-opp a=phil-opp The current test only tests the x86_64 crate, not any property of our kernel. This was reported in https://github.com/phil-opp/blog_os/issues/450#issuecomment-455771614. 529: Fixing typos in paging introduction r=phil-opp a=kolemannix Love this series and thought I'd contribute in whatever way I could. Thanks for the great work. Looking forward to the next post! Co-authored-by: Philipp Oppermann <dev@phil-opp.com> Co-authored-by: Koleman Nix <kolemannix@gmail.com>
This commit is contained in:
@@ -404,65 +404,42 @@ Let's create an integration test that ensures that the above continues to work.
|
||||
```rust
|
||||
// in src/bin/test-exception-breakpoint.rs
|
||||
|
||||
[…]
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
#![no_std]
|
||||
#![cfg_attr(not(test), no_main)]
|
||||
#![cfg_attr(test, allow(dead_code, unused_macros, unused_imports))]
|
||||
|
||||
static BREAKPOINT_HANDLER_CALLED: AtomicUsize = AtomicUsize::new(0);
|
||||
use core::panic::PanicInfo;
|
||||
use blog_os::{exit_qemu, serial_println};
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
init_test_idt();
|
||||
blog_os::interrupts::init_idt();
|
||||
|
||||
// invoke a breakpoint exception
|
||||
x86_64::instructions::int3();
|
||||
|
||||
match BREAKPOINT_HANDLER_CALLED.load(Ordering::SeqCst) {
|
||||
1 => serial_println!("ok"),
|
||||
0 => {
|
||||
serial_println!("failed");
|
||||
serial_println!("Breakpoint handler was not called.");
|
||||
}
|
||||
other => {
|
||||
serial_println!("failed");
|
||||
serial_println!("Breakpoint handler was called {} times", other);
|
||||
}
|
||||
}
|
||||
serial_println!("ok");
|
||||
|
||||
unsafe { exit_qemu(); }
|
||||
loop {}
|
||||
}
|
||||
|
||||
|
||||
lazy_static! {
|
||||
static ref TEST_IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
idt
|
||||
};
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
serial_println!("failed");
|
||||
|
||||
pub fn init_test_idt() {
|
||||
TEST_IDT.load();
|
||||
}
|
||||
serial_println!("{}", info);
|
||||
|
||||
extern "x86-interrupt" fn breakpoint_handler(
|
||||
_stack_frame: &mut ExceptionStackFrame)
|
||||
{
|
||||
BREAKPOINT_HANDLER_CALLED.fetch_add(1, Ordering::SeqCst);
|
||||
unsafe { exit_qemu(); }
|
||||
loop {}
|
||||
}
|
||||
|
||||
[…]
|
||||
```
|
||||
|
||||
For space reasons we don't show the full content here. You can find the full file [on Github](https://github.com/phil-opp/blog_os/blob/master/src/bin/test-exception-breakpoint.rs).
|
||||
It is similar to our `main.rs`, but instead of printing "It did not crash!" to the VGA buffer, it prints "ok" to the serial output and calls `exit_qemu`. This allows the `bootimage` tool to detect that our code successfully continued after invoking the `int3` instruction. If our `panic_handler` is called, we instead print `failed` to signalize the failure to `bootimage`.
|
||||
|
||||
It is similar to our `main.rs`, but uses a custom IDT called `TEST_IDT` and different `_start` and `breakpoint_handler` functions. The most interesting part is the `BREAKPOINT_HANDLER_CALLED` static. It is an [`AtomicUsize`], an integer type that can be safely concurrently modified because all of its operations are atomic. We increment it when the `breakpoint_handler` is called and verify in our `_start` function that the handler was called exactly once.
|
||||
|
||||
[`AtomicUsize`]: https://doc.rust-lang.org/core/sync/atomic/struct.AtomicUsize.html
|
||||
|
||||
The [`Ordering`] parameter specifies the desired guarantees of the atomic operations. The `SeqCst` ordering means “sequential consistent” and gives the strongest guarantees. It is a good default, because weaker orderings can have undesired effects.
|
||||
|
||||
[`Ordering`]: https://doc.rust-lang.org/core/sync/atomic/enum.Ordering.html
|
||||
You can try this new test by running `bootimage test`.
|
||||
|
||||
### Fixing `cargo test` on Windows
|
||||
|
||||
|
||||
@@ -169,9 +169,9 @@ With these indices, we can now walk the page table hierarchy to determine the ma
|
||||
|
||||

|
||||
|
||||
The permissions for the page in the level 1 table are `r`, which means read-only. The hardware enforces this permissions and would throw an exception if we tried to write to that page. Permissions in higher level pages restrict the possible permissions in lower level, so if we set the level 3 entry to read-only, no pages that use this entry can be writable, even if lower levels specify read/write permissions.
|
||||
The permissions for the page in the level 1 table are `r`, which means read-only. The hardware enforces these permissions and would throw an exception if we tried to write to that page. Permissions in higher level pages restrict the possible permissions in lower level, so if we set the level 3 entry to read-only, no pages that use this entry can be writable, even if lower levels specify read/write permissions.
|
||||
|
||||
It's important to note that even through this example used only a single instance of each table, there are typically multiple instances of each level in each address space. At maximum, there are:
|
||||
It's important to note that even though this example used only a single instance of each table, there are typically multiple instances of each level in each address space. At maximum, there are:
|
||||
|
||||
- one level 4 table,
|
||||
- 512 level 3 tables (because the level 4 table has 512 entries),
|
||||
@@ -213,7 +213,7 @@ We see that only bits 12–51 are used to store the physical frame address, the
|
||||
|
||||
Let's take a closer look at the available flags:
|
||||
|
||||
- The `present` flag differentiates mapped pages from unmapped ones. It can be used to temporary swap out pages to disk when main memory becomes full. When the page is accessed subsequently, a special exception called _page fault_ occurs, to which the operating system can react by reloading the missing page from disk and then continuing the program.
|
||||
- The `present` flag differentiates mapped pages from unmapped ones. It can be used to temporarily swap out pages to disk when main memory becomes full. When the page is accessed subsequently, a special exception called _page fault_ occurs, to which the operating system can react by reloading the missing page from disk and then continuing the program.
|
||||
- The `writable` and `no execute` flags control whether the contents of the page are writable or contain executable instructions respectively.
|
||||
- The `accessed` and `dirty` flags are automatically set by the CPU when a read or write to the page occurs. This information can be leveraged by the operating system e.g. to decide which pages to swap out or whether the page contents were modified since the last save to disk.
|
||||
- The `write through caching` and `disable cache` flags allow to control the caches for every page individually.
|
||||
|
||||
@@ -1,70 +1,31 @@
|
||||
#![feature(abi_x86_interrupt)]
|
||||
#![no_std]
|
||||
#![cfg_attr(not(test), no_main)]
|
||||
#![cfg_attr(test, allow(dead_code, unused_macros, unused_imports))]
|
||||
|
||||
use blog_os::{exit_qemu, serial_println};
|
||||
use core::panic::PanicInfo;
|
||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
static BREAKPOINT_HANDLER_CALLED: AtomicUsize = AtomicUsize::new(0);
|
||||
use blog_os::{exit_qemu, serial_println};
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn _start() -> ! {
|
||||
init_test_idt();
|
||||
blog_os::interrupts::init_idt();
|
||||
|
||||
// invoke a breakpoint exception
|
||||
x86_64::instructions::int3();
|
||||
|
||||
match BREAKPOINT_HANDLER_CALLED.load(Ordering::SeqCst) {
|
||||
1 => serial_println!("ok"),
|
||||
0 => {
|
||||
serial_println!("failed");
|
||||
serial_println!("Breakpoint handler was not called.");
|
||||
}
|
||||
other => {
|
||||
serial_println!("failed");
|
||||
serial_println!("Breakpoint handler was called {} times", other);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
exit_qemu();
|
||||
}
|
||||
serial_println!("ok");
|
||||
|
||||
unsafe { exit_qemu(); }
|
||||
loop {}
|
||||
}
|
||||
|
||||
/// This function is called on panic.
|
||||
|
||||
#[cfg(not(test))]
|
||||
#[panic_handler]
|
||||
fn panic(info: &PanicInfo) -> ! {
|
||||
serial_println!("failed");
|
||||
|
||||
serial_println!("{}", info);
|
||||
|
||||
unsafe {
|
||||
exit_qemu();
|
||||
}
|
||||
|
||||
unsafe { exit_qemu(); }
|
||||
loop {}
|
||||
}
|
||||
|
||||
use x86_64::structures::idt::{ExceptionStackFrame, InterruptDescriptorTable};
|
||||
|
||||
lazy_static! {
|
||||
static ref TEST_IDT: InterruptDescriptorTable = {
|
||||
let mut idt = InterruptDescriptorTable::new();
|
||||
idt.breakpoint.set_handler_fn(breakpoint_handler);
|
||||
idt
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init_test_idt() {
|
||||
TEST_IDT.load();
|
||||
}
|
||||
|
||||
extern "x86-interrupt" fn breakpoint_handler(_stack_frame: &mut ExceptionStackFrame) {
|
||||
BREAKPOINT_HANDLER_CALLED.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user