Heap
OS in Rust
Announcements
- Action Items:
- Welcome back.
- Be finishing up on page faults and tables.
Citations
Intro
Local and Static Variables
- We currently use two types of variables in our kernel:
- Local variables are stored on the call stack for the lifetime of the function.
staticvariables are stored at a fixed memory location for the complete lifetime of the program.
Local Variables
- The call stack is a (hardware level) stack ADT
- Supports
pushandpopoperations. - Handshake between the hardware and the compiler.
The Call Stack
- On each function entry, these are pushed by the compiler:
- The parameters,
- The return address, and
- The local variables of the called function
Visually
Description
- Consider the call stack after the
outerfunction called theinnerfunction. - The local variables of
outerare first. - On the
innercall, the parameter1and the return address were pushed. - Then
innerwhich pushed its local array.
Visually
Description
- After the
innerfunction returns:- Its part of the call stack is popped again and
- Only the local variables of
outerremain.
- Variables persist precisely as long as the function that creates them.
Rust implementation
- The Rust compiler enforces these lifetimes.
- It throws an error when we use a value for too long.
- For example, return a reference to a local variable:
Error
returns a reference to data owned by the current function
Use Case
- Maybe we want to do this.
- The most common case is returning a pair (or some other collection) of values.
- One option is the static variable.
Static Variables
- Static variables are stored at a fixed memory location separate from the stack.
- This memory location is assigned at compile time by the linker (remember those?) and encoded in the executable.
- Statics live for the complete runtime of the program, so they have the
'staticlifetime and can always be referenced from local variables.
Visually
Description
- When the
innerfunction returns, its part of the call stack is “destroyed”.- I wonder how it gets destroyed.
- I wonder what destroyed means.
- The static variables live in a separate memory range that is never destroyed.
- So the
&Z[1]reference is still valid after the return.
Code
- This resolves the earlier error.
Niceties
- Static variables’ location is known at compile time…
- So no reference is needed for accessing them.
- That is,
Zwas not defined withininner.
Problems
State of Play
- Local and static variables enable most use cases.
- However, they both have their limitations:
- Local variables cannot be returned.
- Static variables cost memory for the complete runtime of the program.
- They pose many safety problems.
Fixed Size
- I have historically motivated allocation with variable size types.
- Locals and statics are fixed size.
- Examples include arbitrary precision integers, Pythonic lists.
Dynamic Memory
- Compilers often support a memory region for storing variables called the heap.
- The heap supports dynamic memory allocation at runtime through two functions called
allocateanddeallocate.allocatereturns a free chunk of memory of the specified size that can be used to store a variable.- This variable then lives until it is freed by being passed to the
deallocatefunction.
Visually
Description
innerstoreszto heap.- Allocates to get a raw pointer
- Writes using
ptr::write - Returns via
offset
- The calling function then frees the memory via
deallocated.
Visually
Problem
z[1]slot is free again and can be reused- But
z[0]andz[2]are never freed! - This is a memory leak and is bad.
Other Problems
- Continuing to use a variable after calling
deallocateis a use-after-free vulnerability. - Freeing a variable twice, is a double-free vulnerability.
- It might free a different allocation.
- Can also (indirectly) lead to a use-after-free.
These Happen
- Writing memory safe code is hard.
- Here’s a 2019 Linux use-after-free.
- Absolute cowards, and also other people, use Java and Python to solve this problem.
Enter Ownership
- Rust allocation and deallocation is implicit using:
- Ownership, and
- Lifetimes.
- To manage memory manually, we used
Box
Boxing
- We recall
Box::new, which is the Rust allocator. - We introduce dropping to free, implemented in two parts.
- By the
Droptrait - By the
drop<T>(_x: T)function
- By the
- Drops also occur on out-of-scope.
Minimal Examples
- This works
Minimal Examples
- This fails.
Error
Implicit Drop
- This works
Implicit Drop
- This fails
Error
Shout out C++
This pattern has the strange name resource acquisition is initialization (or RAII for short). It originated in C++, where it is used to implement a similar abstraction type called
std::unique_ptr.
Breaking the Rules
- Persist a reference after a deallocation.
rustc complains
error[E0597]: `y[_]` does not live long enough
--> src/main.rs:4:9
|
2 | let x = {
| - borrow later stored here
3 | let y = Box::new(["Hello", "World"]);
| - binding `y` declared here
4 | &y[0] // No semi-colon.
| ^^^^^ borrowed value does not live long enough
5 | }; // Semi-colon.
| - `y[_]` dropped here while still borrowedLifetimes
- Rust attached a “lifetime” to each reference.
- Lifetimes can be specified as some set of scopes.
- Can be assigned variables and used within the type checker. Read more
- Lifetimes prevent use-after-free and go further to provide memory safety.
- All memory freed eventually, no unallocated memory accessed.
Interface
Caveats
- We avoid dynamic memory allocation thus far.
- Incurs overhead to performance.
- Incurs engineering overhead in design.
- Has a minor problem.
- Isn’t Turing complete.
- Can’t compute by definition.
Requirements
- Dynamic memory is required for variables that have
- a dynamic lifetime or
- a variable size.
Reference count
- An important dynamic lifetime is [
Rc]- “Reference Counted”
- Can be addressed by \(n\) rather than \(1\) aliases (references).
std::rc::Rc
- I haven’t really found a use for these but some student solutions have leveraged reference counts.
Vectors
- Remember these? They’re a good time.
- Strings too, of course.
- And the other collection types, like
BTreeMapwhich I used in code I actually wrote that I wanted to work once.
We Lack These
- Try it:
It Fails
Allocator
I Lied
- Surely that will work.
Custom Target
- We once again return to
.cargo/config.toml- Cargo: \(\exists\) people that hold it in some regard.
config
- It’s been a while, so refresher.
- Mine currently looks like this:
Modify build-std
- We add
allocwhere we addedcore - That’s it!
I Lied
- Someone actually has to write the allocator.
Requirements
- The
alloccrate requires a heap allocator- Which implements
allocateanddeallocate
- Which implements
- In Rust, described by
GlobalAlloc(referred to in error)
To set the heap allocator for the crate, the
#[global_allocator]attribute must be applied to astaticvariable that implements theGlobalAlloctrait.
GlobalAlloc
- Here it is:
alloc
- Takes a
Layoutinstance as an argument- Describes the desired size and alignment for allocated memory.
- Returns a raw pointer to the first byte of the allocated memory block.
- Instead of an explicit error value, the
allocmethod returns a null pointer to signal an allocation error. - No comment from me on that.
- Okay, I have one comment.
- Instead of an explicit error value, the
dealloc
- The
deallocmethod is the counterpart toalloc. - Frees a memory block after use.
- It receives two arguments:
- A pointer returned by
alloc - The
Layoutthat was used for the allocation.
- A pointer returned by
Bonus Methods
alloc_zeroedis equivalent to callingallocand then setting the allocated memory block to zero.reallocmethod allows to grow or shrink an allocation.- The default implementation allocates a new memory block with the desired size and copies over all the content from the previous allocation.
A DummyAllocator
- Create a simple dummy allocator.
- Create a new
allocatormodule:
Do nothing
- We return a null point on alloc and panic on dealloc.
Some notes
- We use a size zero struct.
- I used this previously in my implementation of printing.
- I love it when Rust forces me to implement a function as a method!
- This isn’t quite enough.
- We’ll get the same error.
- In theory, we should never hit that panic.
The Attribute
- We have satisfying, if useless, allocator.
- Informer compiler via
#[global_allocator] - It must be:
- A
staticwhich - Implements
GlobalAlloc
- A
This should work
- I at least got a “Hello, world!” after:
- Update
.config - Add
allocandallocator, both, tosrc/lib.rs - Define a
structwith some methods, and… - Declare a
static.
- Update
Kernel Heap
Boxing
- Let’s try boxing something up, like a delicious integer.
src/main.rs
- Rust in its infinite wisdom has blessed us with getting to navigate an obscure namespace.
Fixing
- We must:
- Include
extern crate alloc;in any file where we useBox. - Must refer to
Boxfrom withinallocusing whatever means we prefer.
- Include
- You can tell it’s “working” when you get the following panic:
So far…
Box::newcalls alloc.- Alloc returns.
Box::newnull-checks the return value.- It does not unpack an
Option
- It does not unpack an
Box::newpanics if it sees null.- We didn’t write any of this (except the
allocreturn), it’s all just language features.
Creating a Kernel Heap
- We must create a proper allocator
- But first we need to create a heap memory region
- Somewhere from which the allocator can allocate memory
- We need:
- A virtual memory range for the heap region
- A map from this region to physical frames
Virtual Region
- This part is easy.
- We can make our life moderately easier by picking recognizable values.
Naively
- If we simply map this region now, allocation will fail.
- There is no corresponding physical frame.
- We will initialize our heap, using the
Mapperwe previously implemented.- I didn’t find a graceful way to do this since I’m coerced into certain return types and into using certain methods.
Implementation
src/allocator.rs
pub fn init_heap(
mapper: &mut impl x86_64::structures::paging::Mapper<x86_64::structures::paging::Size4KiB>,
frame_allocator: &mut impl x86_64::structures::paging::FrameAllocator<
x86_64::structures::paging::Size4KiB,
>,
) -> Option<()> {
let page_range = {
let heap_start = x86_64::VirtAddr::new(HEAP_START as u64);
let heap_end = heap_start + HEAP_SIZE - 1u64;
let heap_start_page = x86_64::structures::paging::Page::containing_address(heap_start);
let heap_end_page = x86_64::structures::paging::Page::containing_address(heap_end);
x86_64::structures::paging::Page::range_inclusive(heap_start_page, heap_end_page)
};
let flags = x86_64::structures::paging::PageTableFlags::PRESENT
| x86_64::structures::paging::PageTableFlags::WRITABLE;
for page in page_range {
let frame = match frame_allocator.allocate_frame() {
Some(f) => f,
_ => return None,
};
unsafe {
match mapper.map_to(page, frame, flags, frame_allocator) {
Ok(m) => m.flush(),
_ => return None,
};
}
}
return Some(());
}Two Parts
- Pages: Create a page range using a little arithmetic technique I like to call substraction (also some addition in there!)
- Frames: Construct a map from pages to frames.
Pages
- Convert the
HEAP_STARTpointer to aVirtAddrtype. - Calculate the heap end address from it by adding the
HEAP_SIZE. - Subtract 1, obviously.
- Convert the addresses into
Pagetypes using thecontaining_addressfunction. - Create a page range from the start and end pages using the (provided)
Page::range_inclusivefunction.
Frames
- Prepare page flags for the pages in this region.
- For each page:
- Allocate a frame via
FrameAllocator::allocate_frame. - Set flags.
- Map the page to the frame with the flags.
- The
flushupdates the TLB.
- The
- Allocate a frame via
- I used options instead of results to reduce text volumes.
Onward
- Go ahead and
cargo rto make sure your code doesn’t explode, then let’s head over tosrc/main.rsto run this thing. - Basically, we are extending our initialization routine.
- I am finally, for the first time in my life, considering appreciating how long computers take to turn on.
- After some consideration, I’ve determined actual OS engineers should not use crates and instead optimize this code.
Run it
- At long last, initialize the kernel heap from
src/main.rs
src/main.rs
pub extern "C" fn _start(boot_info: &'static bootloader::BootInfo) -> ! {
osirs::init();
let offset = x86_64::VirtAddr::new(boot_info.physical_memory_offset);
let mut mapper = unsafe { osirs::memory::init(offset) };
let mut frame_allocator =
unsafe { osirs::memory::BootInfoFrameAllocator::init(&boot_info.memory_map) };
osirs::allocator::init_heap(&mut mapper, &mut frame_allocator).unwrap();
println!("Hello world{}", "!");