Entering Protected Mode
"With great power comes great responsibility." - Voltaire / Spider-Man
As you may recall from previous chapters, our BIOS only loads the first sector to RAM, which leaves about just shy of 512 bytes1.
After we read from disk, it will enable us to write much more code, because we will not be limited to 512 bytes.
But just before we do that, we don't want to limit ourselves only to 16bit instructions.
For that we need to enter protected mode which will allow us to unlock some cpu features such as 32bit instructions.
Entering protected mode requires us to initialize the global descriptor table which is a CPU structure that will be discussed in depth below, and toggling the protected mode bit in cr0
The Global Descriptor Table
All the information about the global descriptor table is taken from both the Intel Manual Volume 3A section 3.4.5, and the great osdev website
This is a structure that is specific to the x86 cpu family, and it contains information about the different segments. In general, segments are used to divide memory into logical parts, and to translate addresses as we seen in real mode.
Address translation with the GDT will not be wildely used in this chapter, because it will not be used throughout the OS and memory paging, which will be explained in the next chapter will be used. For now, think of a memory segment as a fixed size blob of contiguous physical memory
In protected mode, the common way to organize memory is using these segments. Because segments registers2 can only hold one number,
they can't hold enough information for us, and that is where the global descriptor table comes in place.
The global descriptor table is an array of structures that include information about a segment,
when we want to use our custom segment, we load it's offset on the GDT to the segment register.
For example, we can create a segment for user data at index one of our table.
This segment will not hold important data for the system, and will not contain code that can be executed,
if we want to load it into the ds we will set it to the offset of the structure in the table.
Each entry is 8 bytes long, index one will be at an offset of 8, which means we will set ds=8
Instead of just revealing you the structure that is used for each segment, I want you to pause and ponder about what each segment should include.
Remember that some instructions assume segments, like mov, jmp etc. and we want segments for the kernel, users, data and code
When I asked myself this question, I came up with the following ideas:
- What is the initial address of the segment. i.e the start address in memory where the segment starts.
- What is the end address of the segment. i.e the end address in memory where the segment ends.
- What the segment includes. i.e data segment, code segment etc.
- What is the privilege level of the segment. i.e can anyone access it or only the kernel
- For a data segment, Is the data read only, or may I modify it?
- For a code segment, Can I execute it, or not yet.
If you gussed something that is similar to this, you are mostly correct!
Our entry will look like this:
But what are these fields?
- Base: this is a 32-bit value, which is split on the entire entry and it represents the address of where the segment begins.
- Limit: this is a 20-bit value, which is split on the entire entry, and it represents the size of the segment.
- Access Byte: flags that are relevant to the memory range of the segment, like the access privileges of this segment.
- Flags: general flags that are relevant for the entry fields.
All of these fields will become a struct and together they represent a single entry on our GDT.
Both the AccessByte and the LimitFlags and more structures throughout the book, are using one bit flags, which represents some inner settings to the CPU.
Although setting one bit flag is easy, and can be done with 1 << bit_number to set the nth bit, we would like abstractions such as set_<flag_name>, which are more readable and less prone to errors.
But, if we would do that to every flag, it will be A LOT of boiler plate code.
For this reason, Rust provides us with an amazing macro system
If you read through some previous version of this book, you may have seen the explanation of the flag! proc-macro, which was used like this:
impl AccessByte { flag!(readable, 1); }This macro was used to define those exactly 1 bit flags. But as it will turn out, this is not enough, and more functionality will be needed.
The problem that this macro had, is that the struct the these functions were defined on, didn't understand that it was a structure that contains bit flags, but it was rather a struct that wraps an integer type, and it has functions that is defined on it to turn specific bits. At first glance this seems almost the same. But, because the macro doesn't get as input all the information on the flags, but rather 'per flag' input, it cannot implement the Debug trait automatically when we want to print and look on the flags.
More problems that are I was having, but are not a direct outcome of the initial design, is that flags sometimes contain more than 1 bit, and may contain n bits, also, certain n bit flags may have a specific set of values that are valid, and we may want to name them in an enum
The current design of this macros, looks like this:
#[bitfields]
pub struct AccessByte {
#[flag(r)]
accessed: B1,
readable_writable: B1,
direction_conforming: B1,
executable: B1,
#[flag(flag_type = SegmentDescriptorType)]
segment_type: B1,
#[flag(flag_type = ProtectionLevel)]
dpl: B2,
present: B1,
}
As you can see, we have the macro attribute at the top of our struct, which is called bitfields.
-
Each field in this struct, is a flag, and as you can see, the highlighter is smart and can expand our macro, so the color of the field is the same as functions.
-
The type of each field represents the flag width in bits. B1 is one bit and B20 is 20 bits.
-
Some flags can have their own attribute, which may contain r and w, which creates only read function, or write function (defaults to both)
-
Flags may also contain types, which are mostly enums that contains the valid values, or even all the values but gives them a readable name.
-
While this macro seems complex, it will just create the functions that will help us to set flags in a convenient way.
To see what this macro generated, we can use the amazing
cargo-expandtool created byDavid TolnayFor example, the expansion of the call above
#![feature(const_trait_impl)] use super::enums::{ProtectionLevel, SegmentDescriptorType}; pub struct AccessByte(u8); #[automatically_derived] impl ::core::marker::Copy for AccessByte {} #[automatically_derived] #[doc(hidden)] unsafe impl ::core::clone::TrivialClone for AccessByte {} #[automatically_derived] impl ::core::clone::Clone for AccessByte { #[inline] fn clone(&self) -> AccessByte { let _: ::core::clone::AssertParamIsClone<u8>; *self } } impl AccessByte { #[inline] pub fn new() -> Self { Self(0) } #[inline] fn is_accessed(&self) -> bool { unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); val & (1 << 0usize) != 0 } } #[inline] fn is_readable_writable(&self) -> bool { unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); val & (1 << 1usize) != 0 } } #[inline] fn set_readable_writable(&mut self, v: bool) { if false { if !((u8::try_from(v) Result.ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 1usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value: {0:?} is too large for this bitfield", v, )); } } } unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); let cleared: u8 = val & !(((1 << 1usize) - 1) << 1usize); let new: u8 = cleared | ((u8::try_from(v).unwrap() as u8) << 1usize); core::ptr::write_volatile(dst: addr, src: new); } } fn set_readable_writable #[inline] const fn readable_writable(mut self) -> Self { self.0 |= (1 << 1usize); self } #[inline] fn is_direction_conforming(&self) -> bool { unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); val & (1 << 2usize) != 0 } } #[inline] fn set_direction_conforming(&mut self, v: bool) { if false { if !((u8::try_from(v) Result .ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 1usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value: {0:?} is too large for this bitfield", v, )); } } } unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); let cleared: u8 = val & !(((1 << 1usize) - 1) << 2usize); let new: u8 = cleared | ((u8::try_from(v).unwrap() as u8) << 2usize); core::ptr::write_volatile(dst: addr, src: new); } } fn set_direction_conforming #[inline] const fn direction_conforming(mut self) -> Self { self.0 |= (1 << 2usize); self } #[inline] fn is_executable(&self) -> bool { unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); val & (1 << 3usize) != 0 } } #[inline] fn set_executable(&mut self, v: bool) { if false { if !((u8::try_from(v) Result .ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 1usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value: {0:?} is too large for this bitfield", v, )); } } } unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); let cleared: u8 = val & !(((1 << 1usize) - 1) << 3usize); let new: u8 = cleared | ((u8::try_from(v).unwrap() as u8) << 3usize); core::ptr::write_volatile(dst: addr, src: new); } } fn set_executable #[inline] const fn executable(mut self) -> Self { self.0 |= (1 << 3usize); self } #[inline] fn get_segment_type(&self) -> SegmentDescriptorType { unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); SegmentDescriptorType::try_from( ((val >> 4usize) & ((1 << 1usize) - 1)) as u8, ) Result > .expect( "Cannot convert bit representation into the given type", ) } } #[inline] fn set_segment_type(&mut self, v: bool) { if false { if !((u8::try_from(v) Result.ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 1usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value: {0:?} is too large for this bitfield", v, )); } } } unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); let cleared: u8 = val & !(((1 << 1usize) - 1) << 4usize); let new: u8 = cleared | ((u8::try_from(v).unwrap() as u8) << 4usize); core::ptr::write_volatile(dst: addr, src: new); } } fn set_segment_type #[inline] const fn segment_type(mut self, v: SegmentDescriptorType) -> Self { if false { if !((u8::try_from(v) Result .ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 1usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value is too large for this bitfield" )); } } } self.0 |= ((u8::try_from(v) Result .ok() Option .expect("Can't convery value 'v' into the struct type") as u8) << 4usize); self } const fn segment_type #[inline] fn get_dpl(&self) -> ProtectionLevel { unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); ProtectionLevel::try_from( ((val >> 5usize) & ((1 << 2usize) - 1)) as u8, ) Result > .expect( "Cannot convert bit representation into the given type", ) } } #[inline] fn set_dpl(&mut self, v: ProtectionLevel) { if false { if !((u8::try_from(v) Result.ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 2usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value: {0:?} is too large for this bitfield", v, )); } } } unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); let cleared: u8 = val & !(((1 << 2usize) - 1) << 5usize); let new: u8 = cleared | ((u8::try_from(v).unwrap() as u8) << 5usize); core::ptr::write_volatile(dst: addr, src: new); } } fn set_dpl #[inline] const fn dpl(mut self, v: ProtectionLevel) -> Self { if false { if !((u8::try_from(v) Result .ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 2usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value is too large for this bitfield" )); } } } self.0 |= ((u8::try_from(v) Result .ok() Option .expect("Can't convery value 'v' into the struct type") as u8) << 5usize); self } const fn dpl #[inline] fn is_present(&self) -> bool { unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); val & (1 << 7usize) != 0 } } #[inline] fn set_present(&mut self, v: bool) { if false { if !((u8::try_from(v) Result .ok() Option .expect("Can't convery value 'v' into the struct type") as u8) < (1 << 1usize) as u8) { { ::core::panicking::panic_fmt(format_args!( "Value: {0:?} is too large for this bitfield", v, )); } } } unsafe { let addr: *mut u8 = self as *const _= AccessByte as *mut u8; let val: u8 = core::ptr::read_volatile(src: addr); let cleared: u8 = val & !(((1 << 1usize) - 1) << 7usize); let new: u8 = cleared | ((u8::try_from(v).unwrap() as u8) << 7usize); core::ptr::write_volatile(dst: addr, src: new); } } fn set_present #[inline] const fn present(mut self) -> Self { self.0 |= (1 << 7usize); self } } impl AccessByte impl const Default for AccessByte { fn default() -> Self { Self(0) } } impl const From<u8> for AccessByte { fn from(value: u8) -> Self { AccessByte(value) } } impl const From<AccessByte> for u8 { fn from(value: AccessByte) -> u8 { value.0 } } impl core::fmt::Debug for AccessByte { fn fmt(&self, f: &mut core::fmt::Formatter<'a:'_>) -> core::fmt::Result { f.debug_struct(name: "AccessByte") DebugStruct<'_, '_> .field(name: "accessed", value: &u8::try_from(self.is_accessed())) &mut DebugStruct<'_, '_> .field( name: "readable_writable", value: &u8::try_from(self.is_readable_writable()), ) &mut DebugStruct<'_, '_> .field( name: "direction_conforming", value: &u8::try_from(self.is_direction_conforming()), ) &mut DebugStruct<'_, '_> .field(name: "executable", value: &u8::try_from(self.is_executable())) &mut DebugStruct<'_, '_> .field( name: "segment_type", value: &SegmentDescriptorType::try_from(self.get_segment_type()), ) &mut DebugStruct<'_, '_> .field(name: "dpl", value: &ProtectionLevel::try_from(self.get_dpl())) &mut DebugStruct<'_, '_> .field(name: "present", value: &u8::try_from(self.is_present())) &mut DebugStruct<'_, '_> .finish() } fn fmt } impl Debug for AccessByte
If this macro seems really cool and complicated, that's great! because it will be fully explained and implemented in later chpaters.
We will also define an enum that will include the protection level and the system segment type, so it would be more clear
#[repr(u8)]
#[derive(
Debug, Clone, Copy, ConstTryFromPrimitive, ConstIntoPrimitive,
)]
#[num_enum(error_type(name = ConversionError<u8>, constructor = ConversionError::CantConvertFrom))]
pub enum ProtectionLevel {
Ring0 = 0,
Ring1 = 1,
Ring2 = 2,
Ring3 = 3,
}
#[repr(u8)]
#[derive(
Copy, Clone, Debug, ConstTryFromPrimitive, ConstIntoPrimitive,
)]
#[num_enum(error_type(name = ConversionError<u8>, constructor = ConversionError::CantConvertFrom))]
pub enum SegmentDescriptorType {
System = 0,
CodeOrData = 1,
}
Now, just before creating a new function to our entry, we don't want each time to specify the base in three parts and the limit in two parts, instead we want the new function to abstract it from us.
#[repr(C, packed)]
struct GlobalDescriptorTableEntry32 {
limit_low: u16,
base_low: u16,
base_mid: u8,
access_byte: AccessByte,
limit_flags: LimitFlags,
base_high: u8,
}
impl GlobalDescriptorTableEntry32 {
/// Create a new entry
///
/// # Parameters
///
/// - `base`: The base address of the segment
/// - `limit`: The size of the segment
/// - `access_byte`: The type and access privileges of the entry
/// - `flags`: Configuration flags of the entry
// ANCHOR: gdt_entry32_new
pub const fn new(
base: u32,
limit: u32,
access_byte: AccessByte,
flags: LimitFlags,
) -> GlobalDescriptorTableEntry32 {
// Split base into the appropriate parts
let base_low: u16 = (base & 0xffff) as u16;
let base_mid: u8 = ((base >> 0x10) & 0xff) as u8;
let base_high: u8 = ((base >> 0x18) & 0xff) as u8;
// Split limit into the appropriate parts
let limit_low: u16 = (limit & 0xffff) as u16;
let limit_high: u8 = ((limit >> 0x10) & 0xf) as u8;
// Combine the part of the limit size with the flags
let limit_flags: u8 = flags.0 | limit_high;
GlobalDescriptorTableEntry32 {
limit_low,
base_low,
base_mid,
access_byte,
limit_flags: LimitFlags(limit_flags),
base_high,
}
} const fn new
} impl GlobalDescriptorTableEntry32
Jumping to the next stage!
Now, after understanding the global descriptor table, we want to jump to the next stage. This will require us to create and load a temporary global descriptor table.
Each table must have at least three entries, an initial null entry that is filled with zeros, which is always required as the first entry, a data entry for the data segment so we can read and write to memory, and code entry so we can execute code.
Together it will all look like this:
/// Initial temporary GDT
#[repr(C, packed)]
pub struct GlobalDescriptorTableProtected {
null: GlobalDescriptorTableEntry32,
code: GlobalDescriptorTableEntry32,
data: GlobalDescriptorTableEntry32,
}
impl GlobalDescriptorTableProtected {
/// Creates default global descriptor table for
/// protected mode
// ANCHOR: gdt_default
pub const fn default() -> Self {
Self {
null: GlobalDescriptorTableEntry32::default(),
code: GlobalDescriptorTableEntry32::new(
base: 0,
limit: 0xfffff,
access_byte: AccessByte::default() AccessByte
.present() AccessByte
.dpl(ProtectionLevel::Ring0) AccessByte
.segment_type(SegmentDescriptorType::CodeOrData) AccessByte
.executable() AccessByte
.readable_writable(),
flags: LimitFlags::default().granularity().protected(),
),
data: GlobalDescriptorTableEntry32::new(
base: 0,
limit: 0xfffff,
access_byte: AccessByte::default() AccessByte
.present() AccessByte
.dpl(ProtectionLevel::Ring0) AccessByte
.segment_type(SegmentDescriptorType::CodeOrData) AccessByte
.readable_writable(),
flags: LimitFlags::default().granularity().protected(),
),
}
} const fn default
} impl GlobalDescriptorTableProtected
If you noticed, all of the functions that we defined so far are marked with const this is useful because we can create our global descriptor table as a static variable, which will be in the binary.
This is useful because it will make our initialization of the global descriptor table to be in compile time.
So, the only thing left to do is to load the global descriptor table. This can be done with the lgdt instruction which loads the Global Descriptor Table Register with our table. This is a hidden register that includes information about our global descriptor table, like it's size and address in memory.
We will create a load function that will create this register structure, and will load it to the cpu.
#[repr(C, packed)]
pub struct GlobalDescriptorTableRegister {
pub limit: u16,
pub base: usize,
}
impl GlobalDescriptorTableProtected {
/// Load the GDT with the `lgdt` instruction
///
/// # Safety
/// This function doesn't check if a GDT already exists, and just
/// overrides it.
// ANCHOR: gdt_load
pub unsafe fn load(&'static self) {
let gdtr: GlobalDescriptorTableRegister = {
GlobalDescriptorTableRegister {
limit: (size_of::<Self>() - 1) as u16,
base: self as *const _= GlobalDescriptorTableProtected as usize,
}
};
unsafe {
instructions::lgdt(&gdtr);
}
}
} impl GlobalDescriptorTableProtected
Now, to apply all of the created functionality, enable protected mode, and to jump to the next stage, we need to add the following code to our entry function.
But just before that, when we jump to the next stage, we need to specify the offset in the GDT of the relevant section we want to jump to, which will load the cs segment register with that value. In that case it is the kernel_code section, which will allow us to run code on ring0. For an easy way to specify the section, we will create an enum.
Notice that this also contains segments of other GDT that we will use in the future
#[repr(u16)]
#[derive(
Copy, Clone, Debug, ConstTryFromPrimitive, ConstIntoPrimitive,
)]
#[num_enum(error_type(name = ConversionError<u16>, constructor = ConversionError::CantConvertFrom))]
pub enum Sections {
Null = 0x0,
KernelCode = 0x8,
KernelData = 0x10,
UserCode = 0x18,
UserData = 0x20,
TaskStateSegment = 0x28,
}
static GLOBAL_DESCRIPTOR_TABLE: GlobalDescriptorTableProtected =
GlobalDescriptorTableProtected::default();
unsafe fn enter_protected_mode() {
// Load Global Descriptor Table
unsafe { GLOBAL_DESCRIPTOR_TABLE.load() };
// Set the Protected Mode bit and enter Protected Mode
asm!(
"mov eax, cr0",
"or eax, 1",
"mov cr0, eax",
options(readonly, nostack, preserves_flags)
);
// Jump to the next stage
// The 'ljmp' instruction is required to because it updates the cpu
// segment to the new ones from our GDT.
//
// The segment is the offset in the GDT.
// (KernelCode = 0x8 which is the code segment)
asm!(
"ljmp ${segment}, ${next_stage_address}",
segment = const Sections::KernelCode as u8,
next_stage_address = const SECOND_STAGE_OFFSET,
options(att_syntax)
);
} unsafe fn enter_protected_mode
- Load the global descriptor table