!!! NOTE !!!
This chapter was rushed due to time constraints. Call over the instructor if you want to properly learn about this topic.
Shared Memory
This chapter covers interacting with shared memory from protection domains written in Rust. Navigate to and run the example:
cd workspaces/microkit/shared-memory
make simulate
The example system description specifies two protection domains which share two memory regions:
<system>
<memory_region name="region_a" size="0x1_000" />
<memory_region name="region_b" size="0x1_000" />
<protection_domain name="client" priority="100">
<program_image path="microkit-shared-memory-client.elf" />
<map mr="region_a" vaddr="0x2_000_000" perms="rw" cached="true" setvar_vaddr="region_a_vaddr" />
<map mr="region_b" vaddr="0x2_400_000" perms="rw" cached="true" setvar_vaddr="region_b_vaddr" />
</protection_domain>
<protection_domain name="server" priority="200" pp="true">
<program_image path="microkit-shared-memory-server.elf" />
<map mr="region_a" vaddr="0x2_000_000" perms="r" cached="true" setvar_vaddr="region_a_vaddr" />
<map mr="region_b" vaddr="0x2_400_000" perms="r" cached="true" setvar_vaddr="region_b_vaddr" />
</protection_domain>
<channel>
<end pd="client" id="13" />
<end pd="server" id="37" />
</channel>
</system>
The Microkit tool will inject memory region virtual addresses into protection domain images according to the setvar_vaddr
attribute values.
For example, the virtual address of the mapping of region_a
into the client
protection domain will be injected into the microkit-shared-memory-client.elf
image at the location specified by then region_a_vaddr
symbol.
In the case of Rust, declaring a symbol that the Microkit tool can patch requires a bit more intentionality than in the C case.
The sel4_microkit::var!
macro is provided to declare such symbols.
The var!
macro's implementation is just a few lines of code.
We want to express this symbol as a global variable that does not change at runtime, but which cannot be assumed to have the value we assign it at compile time, and which must not be optimized away.
The near-trivial
sel4_immutable_cell::ImmutableCell
type encapsulates this pattern.
The #[no_mangle]
attribute instructs the compiler to use the name of the variable as the name of the symbol.
This is the default in C, but not Rust.
We direct the compiler to put this symbol in the .data
section with #[link_section = ".data"]
to ensure that space is allocated for it in the ELF file itself, not just the program image it describes.
So far, the example protection domains just store pointers to the shared memory regions in their handler state:
#[protection_domain]
fn init() -> impl Handler {
debug_println!("client: initializing");
let region_a = *var!(region_a_vaddr: usize = 0);
let region_b = *var!(region_b_vaddr: usize = 0);
debug_println!("client: region_a = {region_a:#x?}");
debug_println!("client: region_b = {region_b:#x?}");
HandlerImpl { region_a, region_b }
}
struct HandlerImpl {
region_a: usize,
region_b: usize,
}
impl Handler for HandlerImpl {
type Error = Infallible;
}
#[protection_domain]
fn init() -> impl Handler {
debug_println!("server: initializing");
let region_a = *var!(region_a_vaddr: usize = 0);
let region_b = *var!(region_b_vaddr: usize = 0);
debug_println!("server: region_a = {region_a:#x?}");
debug_println!("server: region_b = {region_b:#x?}");
HandlerImpl { region_a, region_b }
}
struct HandlerImpl {
region_a: usize,
region_b: usize,
}
impl Handler for HandlerImpl {
type Error = Infallible;
}
Step 11.A
Let's assign types to these shared memory regions. We can define our types in a crate that both the client and server can use:
use zerocopy::{AsBytes, FromBytes, FromZeroes};
pub const REGION_A_SIZE: usize = 1337;
#[repr(C)]
#[derive(AsBytes, FromBytes, FromZeroes)]
pub struct RegionB {
pub field_1: u64,
pub foo: [u16; 16],
}
Suppose region_a: [u8; REGION_A_SIZE]
and region_b: RegionB
.
You could just turn the virtual addresses we get in our var!
symbols into pointers and start interacting with the shared memory regions with unsafe
ptr::*
operations, but we can leverage the Rust type system to come up with a solution that only requires unsafe
at initialization time.
Step 11.B
The under-documented (for now)
sel4-externally-shared
provides a way for you to declare a memory region's type and bounds, along with the memory access operations that can safely be used on it, so that you can access it without unsafe
code.
That initial declaration is, however, unsafe
.
For now, it is a fork of the volatile
crate, generalized to enable the use of memory access operations beyond just the ptr::read_volatile
, ptr::write_volatile
, and friends supported by that crate.
The
sel4_externally_shared::ExternallySharedRef
type alias is the now-abstract
sel4_externally_shared::VolatileRef
type instantiated with memory access operations suitable for memory that is shared with another protection domain.
The
sel4_microkit::memory_region_symbol!
macro is like the sel4_microkit::var!
macro, except specialized for shared memory region virtual address symbols.
For one, the underlying symbol is always of type usize
and the macro returns a value of type NonNull<_>
.
memory_region_symbol!
has a few additional features.
For example, memory_region_symbol!(foo: *mut [u8] n = BAR)
returns a NonNull<[u8]>
with a runtime slice length of BAR
.
See this step's diff for how to put this all together.