small thigns and framebuffer blog

This commit is contained in:
janis 2026-07-18 18:11:06 +02:00
parent 932d98af20
commit 5b2b881ced
Signed by: janis
SSH key fingerprint: SHA256:bB1qbbqmDXZNT0KKD5c2Dfjg53JGhj7B3CFcLIzSqq8
6 changed files with 379 additions and 34 deletions

View file

@ -405,3 +405,318 @@ fn _start() -> ! {
#+end_src #+end_src
* Getting the Framebuffer * Getting the Framebuffer
https://wiki.osdev.org/Limine_Bare_Bones#Compiling_the_kernel
https://github.com/Limine-Bootloader/limine-protocol/blob/trunk/PROTOCOL.md
The limine protocol, which weve chosen to use for our bootloader, allows our kernel to make requests at compile time, which the bootloader will then fulfil at boot time.
One of the requests we can make is for the Framebuffer with which we can draw pixels to the screen.
Limine requests generally have the following structure:
#+begin_src rust
#[repr(C)]
pub struct Request<T, U = ()> {
magic: [u64; 2],
id: [u64; 2],
revision: u64,
response: UnsafeCell<Option<NonNull<Response<T>>>>,
request: U,
}
impl<T, U> Request<T, U> {
pub const MAGIC: [u64; 2] = [0xc7b1dd30df4c8b88, 0x0a82e883a194f07b];
pub const fn new_raw(id: [u64; 2], revision: u64, request: U) -> Self {
Self {
magic: Self::MAGIC,
id,
revision,
response: UnsafeCell::new(None),
request,
}
}
pub fn response(&self) -> Option<&Response<T>> {
unsafe {
self.response
.get()
.cast::<Option<NonNull<Response<T>>>>()
.read_volatile()
}
.map(|ptr| unsafe { ptr.as_ref() })
}
}
#+end_src
A magic 128-bit number, which is the same for all requests, is followed by a 128-bit request ID which is unique to each request type. For the Framebuffer, the request ID, expressed as two 64-bit integers, is =[0x9d5827dcd881dd75, 0xa3148604f6fab11b]=.
What follows is a 64-bit revision number for the request, which allows single requests to change shape without having to change the revision of the entire protocol.
For the Framebuffer, there are currently two revisions, 0 and 1, but we are interested only in the payload of revision 1.
In the case that there are Framebuffers available, limine will write a pointer to the response structure into the =response= field of the request.
Some requests may require additional data to be read by the bootloader, which are appended to the end of the request structure, here represented by the generic type parameter =U=.
The response structure is very simple, containing only the revision number of the response the bootloader provided, followed by the request-dependent payload, which in the case of the Framebuffer is a count, and a pointer to an array of pointers to the available Framebuffers:
#+begin_src rust
#[repr(C)]
pub struct Response<T> {
revision: u64,
response: T,
}
#[repr(C)]
pub struct FramebufferResponse {
count: u64,
framebuffers: *const *const Framebuffer,
}
unsafe impl Send for FramebufferResponse {}
unsafe impl Sync for FramebufferResponse {}
impl FramebufferRequest {
pub const ID: [u64; 2] = [0x9d5827dcd881dd75, 0xa3148604f6fab11b];
pub const fn new() -> Self {
Self::new_raw(Self::ID, 0, ())
}
pub fn framebuffers<'a>(&self) -> &'a [&'a Framebuffer] {
// SAFETY: limine responses are guaranteed to be valid for the lifetime of the memory mapping.
self.response()
.map(|response| unsafe {
core::slice::from_raw_parts(
response
.response
.framebuffers
.cast::<&'a Framebuffer>(),
response.response.count as usize,
)
})
.unwrap_or(&[])
}
}
#+end_src
Limine requests, for any revision versions of the protocol greater than 1, must be placed inbetween two special magic markers in the mapping of the kernel binary.
This way, Limine will know where to find the requests and hopefully not accidentally mistake some other data in the kernel binary for a request.
#+begin_src rust
#[repr(transparent)]
pub struct RequestsStartMarker([u64; 4]);
pub const REQUESTS_START_MARKER: RequestsStartMarker = RequestsStartMarker([
0xf6b8f4b39de7d1ae,
0xfab91a6940fcb9cf,
0x785c6ed015d3e316,
0x181e920a7852b9d9,
]);
#[repr(transparent)]
pub struct RequestsEndMarker([u64; 2]);
pub const REQUESTS_END_MARKER: RequestsEndMarker =
RequestsEndMarker([0xadc0e0531bb10d03, 0x9572709f31764c62]);
#+end_src
Typically, items in Rust are placed into sections (e.g. =.text=, =.data=, =.rodata=, =.bss=) according to their type and usage.
Functions are placed in the =.text= section, which is usually marked as executable and read-only, meaning that, just like attempting to write to a =const= variable, attempting to write to a function will cause a fault.
Sometimes, one might still want to write to a function however, and then Rust offers the =link_section= attribute to place items into arbitrary sections.
A linker or other binary manipulation tools (found in the =binutils= collection of tools) can be used to inform the loader to mark the memory of that section as, for example, writable.
#+begin_src rust
#[used]
#[unsafe(link_section = ".limine_requests_start")]
static LIMINE_REQUESTS_START: RequestsStartMarker = REQUESTS_START_MARKER;
#[used]
#[unsafe(link_section = ".limine_requests")]
pub static FRAMEBUFFER_REQUEST: FramebufferRequest = FramebufferRequest::new();
#[used]
#[unsafe(link_section = ".limine_requests_end")]
static LIMINE_REQUESTS_END: RequestsEndMarker = REQUESTS_END_MARKER;
#+end_src
Since we dont reference the =LIMINE_REQUESTS_START= and =LIMINE_REQUESTS_END= markers anywhere in our code, we tell the compiler and clippy that they are nevertheless used with the =used= attribute.
Rust already knows to place our requests into read-write memory, however the order of items within a section cannot be controlled from within Rust, and so we will use a linker script to ensure that all requests are placed between the start and end markers.
The following script is mostly taken from the =osdev.org= wiki entry on the [[https://wiki.osdev.org/Limine_Bare_Bones#Compiling_the_kernel][Limine Bare Bones example]].
#+begin_src linkerscript
OUTPUT_FORMAT(elf64-x86-64)
ENTRY(_start)
PHDRS {
limine_requests PT_LOAD;
text PT_LOAD;
rodata PT_LOAD;
data PT_LOAD;
}
SECTIONS {
. = 0xffffffff80000000;
.limine_requests : {
KEEP(*(.limine_requests_start))
KEEP(*(.limine_requests))
KEEP(*(.limine_requests_end))
} :limine_requests
. = ALIGN(CONSTANT(MAXPAGESIZE));
.text : {
*(.text .text.*)
} :text
. = ALIGN(CONSTANT(MAXPAGESIZE));
.rodata : {
*(.rodata .rodata.*)
} :rodata
. = ALIGN(CONSTANT(MAXPAGESIZE));
.data : {
*(.data .data.*)
} :data
.bss : {
*(.bss .bss.*)
*(COMMON)
} :data
/DISCARD/ : {
*(.eh_frame*)
*(.note .note.*)
}
}
#+end_src
We take the opportunity to place our kernel explicitly into the higher-half of the address space, starting at =0xffffffff80000000=.
The most important bit with respect to the limine requests is the ordering of the =KEEP= directives in the =.limine_requests= section.
After that, we need to make sure that none of the sections with different permissions (read, write, execute) are placed in the same page.
In addition to the Framebuffer, another request we will make use of is the Base Revision request, which tells us whether the bootloader supports the revision of the protocol we are using.
#+begin_src rust
#[repr(C)]
pub struct BaseRevision(UnsafeCell<[u64; 3]>);
unsafe impl Send for BaseRevision {}
unsafe impl Sync for BaseRevision {}
impl BaseRevision {
pub const fn from_revision(revision: u64) -> Self {
Self(UnsafeCell::new([
0xf9562b2d5c95a6c8,
0x6a7b384944536bdc,
revision,
]))
}
pub fn is_supported(&self) -> bool {
unsafe { self.0.get().cast::<u64>().add(2).read_volatile() == 0 }
}
pub fn base_revision(&self) -> u64 {
unsafe { self.0.get().cast::<u64>().add(2).read_volatile() }
}
}
#+end_src
Even though we dont ever require mutable access to the internal array, we use an =UnsafeCell= to tell the compiler that the value of the array may have changed at any time and are not what it might expect them to be.
This makes the reads of the array, which require unsafe code to access the contents of the interior mutabily cell, perfectly safe as we dont violate Rusts aliasing rules.
Rust also assumes by default that anything containing an =UnsafeCell= is not safe to share between threads, and while our program is currently single-threaded, since we only ever read from the cell anyway, we can safely tell Rust our type is =Sync=.
This is actually required for the =BaseRevision= request to be used in a static variable, as Rust has no way of enforcing that all code that accesses the variable is running on the same thread.
The same is true also for the =Request= type, except that we have to bound the impl of =Sync= on =T= and =U= being =Sync= as well.
** Using the Framebuffer
http://www.petesqbsite.com/sections/tutorials/tuts/vbe3.pdf
https://www.ctyme.com/intr/rb-0274.htm#Table82
https://github.com/Limine-Bootloader/Limine/blob/v12.x/common/drivers/vbe.c
https://github.com/Limine-Bootloader/Limine/blob/v12.x/common/drivers/gop.c
Framebuffers have the following layout:
#+begin_src rust
#[repr(C)]
pub struct Framebuffer {
pub addr: *mut u8,
pub width: u64,
pub height: u64,
pub pitch: u64,
pub bpp: u16,
pub memory_model: u8,
pub red_mask_size: u8,
pub red_mask_shift: u8,
pub green_mask_size: u8,
pub green_mask_shift: u8,
pub blue_mask_size: u8,
pub blue_mask_shift: u8,
pub reserved: [u8; 7],
pub edid_size: u64,
pub edid: *mut u8,
}
#+end_src
The limine documentation on this type is somewhat sparse, but the fields are mostly self-explanatory.
Relevant to us are the =addr= field, and the fields describing how the (sub)pixels =addr= points to are laid out in memory.
=width= and =height= describe the dimensions of the framebuffer in pixels, =pitch= describes how many bytes are used for each row of pixels, and =bpp= describes how many bits are used for each pixel.
=bpp= can be assumed to be a multiple of 8. We will additionally assume that =bpp= is always at most 64.
=memory_model= is the [[https://www.ctyme.com/intr/rb-0274.htm#Table82][VESA SVGA memory model]] the framebuffer uses, and at least at the time of writing, the only value reported by limine is =6=.
The =mask_size= and =mask_shift= fields describe how the respective color channels are to be organised with a =bpp / 8= byte memory region.
To draw an rgb-colored pixel at a given position =(x, y)= then, we will define the following function:
#+begin_src rust
pub struct Color(pub [u8; 3]);
impl Framebuffer {
/// # Safety
/// the caller must ensure they have exclusive access to the region of the
/// framebuffer they are writing to.
pub unsafe fn draw_pixel(&self, x: u64, y: u64, color: Color) {
assert!(self.bpp <= 64);
let bytes_per_pixel = self.bpp.div_ceil(8) as usize;
let [r, g, b] = color.0;
let mask_color = |c: u8, bits: u8, shift: u8| {
let mask = (1 << bits) - 1;
(c as u64 & mask) << shift
};
let mut pixel = 0u64;
pixel |= mask_color(r, self.red_mask_size, self.red_mask_shift);
pixel |= mask_color(g, self.green_mask_size, self.green_mask_shift);
pixel |= mask_color(b, self.blue_mask_size, self.blue_mask_shift);
let pixel = pixel.to_ne_bytes();
let dst = unsafe {
self.addr
.add((y * self.pitch + x * bytes_per_pixel as u64) as usize)
};
unsafe { super::volatile_copy(pixel.as_ptr(), dst, bytes_per_pixel) }
}
}
#+end_src
The method is =unsafe=, since we cannot guarantee that the call site is the only code currently attempting to write to the framebuffer yet.
=volatile_copy= is a simple utility function mirroring =core::ptr::copy=, but performing a volatile write to the destination; this is necessary because =addr= might be device mapped memory.
Inside of our =_start= function, we can now bring all of this together to draw a pretty pattern to the screen:
#+begin_src rust
#[unsafe(no_mangle)]
fn _start() -> ! {
assert!(limine_requests::LIMINE_BASE_REVISION.is_supported());
let fb = limine_requests::FRAMEBUFFER_REQUEST
.framebuffers()
.first()
.expect("No framebuffer found");
for y in 0..fb.height {
for x in 0..fb.width {
let r = (x * 255 / fb.width) as u8;
let b = (y * 255 / fb.height) as u8;
// SAFETY: We have exclusive access to the framebuffer.
unsafe {
fb.draw_pixel(x, y, kernel::limine::Color([r, 0, b]));
}
}
}
kernel::x86_64::halt_loop()
}
#+end_src

View file

@ -12,8 +12,37 @@ pub unsafe fn volatile_copy<T: Sized>(src: *const T, dst: *mut T, count: usize)
for i in 0..count { for i in 0..count {
let src_ptr = src.add(i); let src_ptr = src.add(i);
let dst_ptr = dst.add(i); let dst_ptr = dst.add(i);
let val = src_ptr.read_volatile(); let val = src_ptr.read();
dst_ptr.write_volatile(val); dst_ptr.write_volatile(val);
} }
} }
} }
#[macro_export]
macro_rules! drop_guard {
($($stmts:stmt)*) => {
{
struct __DropGuard<F: FnOnce()>(::core::mem::ManuallyDrop<F>);
impl<F: FnOnce()> __DropGuard<F> {
#[allow(dead_code)]
fn forget(self) {
let mut this = ::core::mem::ManuallyDrop::new(self);
unsafe {
ManuallyDrop::drop(&mut this.0);
}
}
}
impl<F: FnOnce()> Drop for __DropGuard<F> {
fn drop(&mut self) {
unsafe { ::core::ptr::read(&*self.0)() }
}
}
__DropGuard(::core::mem::ManuallyDrop::new(|| {
$($stmts)*
}))
}
};
}

View file

@ -3,7 +3,6 @@ use core::{cell::UnsafeCell, ptr::NonNull};
#[repr(C)] #[repr(C)]
pub struct BaseRevision(UnsafeCell<[u64; 3]>); pub struct BaseRevision(UnsafeCell<[u64; 3]>);
unsafe impl Send for BaseRevision {}
unsafe impl Sync for BaseRevision {} unsafe impl Sync for BaseRevision {}
impl BaseRevision { impl BaseRevision {
@ -45,8 +44,7 @@ pub struct Request<T, U = ()> {
request: U, request: U,
} }
unsafe impl<T, U> Send for Request<T, U> {} unsafe impl<T: Sync, U: Sync> Sync for Request<T, U>{}
unsafe impl<T, U> Sync for Request<T, U> {}
#[repr(C)] #[repr(C)]
pub struct Response<T> { pub struct Response<T> {
@ -78,6 +76,7 @@ impl<T, U> Request<T, U> {
} }
} }
#[derive(Debug)]
#[repr(C)] #[repr(C)]
pub struct Framebuffer { pub struct Framebuffer {
pub addr: *mut u8, pub addr: *mut u8,
@ -100,7 +99,10 @@ pub struct Framebuffer {
pub struct Color(pub [u8; 3]); pub struct Color(pub [u8; 3]);
impl Framebuffer { impl Framebuffer {
pub fn draw_pixel(&self, x: u64, y: u64, color: Color) { /// # Safety
/// the caller must ensure they have exclusive access to the region of the
/// framebuffer they are writing to.
pub unsafe fn draw_pixel(&self, x: u64, y: u64, color: Color) {
assert!(self.bpp <= 64); assert!(self.bpp <= 64);
let bytes_per_pixel = self.bpp.div_ceil(8) as usize; let bytes_per_pixel = self.bpp.div_ceil(8) as usize;
@ -116,7 +118,7 @@ impl Framebuffer {
pixel |= mask_color(g, self.green_mask_size, self.green_mask_shift); pixel |= mask_color(g, self.green_mask_size, self.green_mask_shift);
pixel |= mask_color(b, self.blue_mask_size, self.blue_mask_shift); pixel |= mask_color(b, self.blue_mask_size, self.blue_mask_shift);
let pixel = pixel.to_le_bytes(); let pixel = pixel.to_ne_bytes();
let dst = unsafe { let dst = unsafe {
self.addr self.addr
@ -132,6 +134,10 @@ pub struct FramebufferResponse {
framebuffers: *const *const Framebuffer, framebuffers: *const *const Framebuffer,
} }
// SAFETY: We only ever read from the framebuffer array.
unsafe impl Send for FramebufferResponse {}
unsafe impl Sync for FramebufferResponse {}
pub type FramebufferRequest = Request<FramebufferResponse>; pub type FramebufferRequest = Request<FramebufferResponse>;
const impl Default for FramebufferRequest { const impl Default for FramebufferRequest {
@ -147,15 +153,12 @@ impl FramebufferRequest {
Self::new_raw(Self::ID, 0, ()) Self::new_raw(Self::ID, 0, ())
} }
pub fn framebuffers<'limine>(&self) -> &'limine [&'limine Framebuffer] { pub fn framebuffers<'a>(&self) -> &'a [&'a Framebuffer] {
// SAFETY: limine responses are guaranteed to be valid for the lifetime of the memory mapping. // SAFETY: limine responses are guaranteed to be valid for the lifetime of the memory mapping.
self.response() self.response()
.map(|response| unsafe { .map(|response| unsafe {
core::slice::from_raw_parts( core::slice::from_raw_parts(
response response.response.framebuffers.cast::<&'a Framebuffer>(),
.response
.framebuffers
.cast::<&'limine Framebuffer>(),
response.response.count as usize, response.response.count as usize,
) )
}) })

View file

@ -7,8 +7,9 @@ fn panic(_info: &core::panic::PanicInfo) -> ! {
kernel::x86_64::halt_loop() kernel::x86_64::halt_loop()
} }
/// Entry point for the kernel
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
fn _start() -> ! { extern "C" fn _start() -> ! {
kernel::serial_println!("Hello, world!"); kernel::serial_println!("Hello, world!");
assert!(limine_requests::LIMINE_BASE_REVISION.is_supported()); assert!(limine_requests::LIMINE_BASE_REVISION.is_supported());
@ -17,16 +18,16 @@ fn _start() -> ! {
.first() .first()
.expect("No framebuffer found"); .expect("No framebuffer found");
kernel::serial_println!("Framebuffer: {fb:#?}");
for y in 0..fb.height { for y in 0..fb.height {
for x in 0..fb.width { for x in 0..fb.width {
let mut bits = ((x * 255 / fb.width) as u32) << 8; let r = (x * 255 / fb.width) as u8;
bits ^= (y * 255 / fb.height) as u32; let b = (y * 255 / fb.height) as u8;
// SAFETY: We have exclusive access to the framebuffer.
fb.draw_pixel( unsafe {
x, fb.draw_pixel(x, y, kernel::limine::Color([r, 0, b]));
y, }
kernel::limine::Color(*bits.to_le_bytes().first_chunk().unwrap()),
);
} }
} }
@ -39,19 +40,19 @@ mod limine_requests {
RequestsEndMarker, RequestsStartMarker, RequestsEndMarker, RequestsStartMarker,
}; };
#[used]
#[unsafe(link_section = ".limine_requests_start")] #[unsafe(link_section = ".limine_requests_start")]
#[unsafe(no_mangle)]
static LIMINE_REQUESTS_START: RequestsStartMarker = REQUESTS_START_MARKER; static LIMINE_REQUESTS_START: RequestsStartMarker = REQUESTS_START_MARKER;
#[used]
#[unsafe(link_section = ".limine_requests")] #[unsafe(link_section = ".limine_requests")]
#[unsafe(no_mangle)]
pub static LIMINE_BASE_REVISION: BaseRevision = BaseRevision::from_revision(6); pub static LIMINE_BASE_REVISION: BaseRevision = BaseRevision::from_revision(6);
#[used]
#[unsafe(link_section = ".limine_requests")] #[unsafe(link_section = ".limine_requests")]
#[unsafe(no_mangle)]
pub static FRAMEBUFFER_REQUEST: FramebufferRequest = FramebufferRequest::new(); pub static FRAMEBUFFER_REQUEST: FramebufferRequest = FramebufferRequest::new();
#[used]
#[unsafe(link_section = ".limine_requests_end")] #[unsafe(link_section = ".limine_requests_end")]
#[unsafe(no_mangle)]
static LIMINE_REQUESTS_END: RequestsEndMarker = REQUESTS_END_MARKER; static LIMINE_REQUESTS_END: RequestsEndMarker = REQUESTS_END_MARKER;
} }

View file

@ -750,6 +750,9 @@ pub mod uart_16550 {
self.uart.send_bytes_exact(b"\r\n"); self.uart.send_bytes_exact(b"\r\n");
} }
8 | 0x7f => { 8 | 0x7f => {
// Backspace or Delete: send backspace, space,
// backspace to erase the character on the
// terminal
self.uart.send_bytes_exact(&[8, b' ', 8]); self.uart.send_bytes_exact(&[8, b' ', 8]);
} }
_ => {} _ => {}

View file

@ -192,15 +192,9 @@ mod once {
// even though we don't have unwinding, for // even though we don't have unwinding, for
// completeness sake we'll poison the lock if // completeness sake we'll poison the lock if
// the closure panics. // the closure panics.
struct Guard<'a>(&'a AtomicU8); let guard = crate::drop_guard! {
self.state.store(PANICKED, Ordering::Release)
impl Drop for Guard<'_> { };
fn drop(&mut self) {
self.0.store(PANICKED, Ordering::Release);
}
}
let guard = Guard(&self.state);
let state = OnceState { let state = OnceState {
poisoned: state == PANICKED, poisoned: state == PANICKED,
@ -208,7 +202,7 @@ mod once {
}; };
f(&state); f(&state);
core::mem::forget(guard); guard.forget();
self.state self.state
.store(state.state_to_set.take(), Ordering::Release); .store(state.state_to_set.take(), Ordering::Release);