Sync computers

This commit is contained in:
2026-02-28 18:55:10 +01:00
parent c3eb93e701
commit 9a983c56f3
29 changed files with 1564 additions and 28 deletions

View File

@@ -3,4 +3,4 @@ target remote localhost:1234
# break machine_mode_entry
break *0x800bf000
add-symbol-file target/riscv64/debug/test_pic 0x800bf000
c
# c

View File

@@ -12,7 +12,8 @@ embedded-alloc = "0.7"
kernel-macros = { path = "crates/kernel-macros" }
log = "0.4"
critical-section = { version = "1", features = ["restore-state-bool"] }
bffs = { path = "../../../code/bffs", features = ["alloc"] }
shared = { path = "crates/shared" }
bffs = { path = "crates/bffs", features = ["alloc"] }
shared = { path = "crates/shared", features = ["kernel"] }
# ELF parsing helper
goblin = { version = "0.7", default-features = false, features = ["elf32", "elf64", "endian_fd"] }
hashbrown = "0.16"

11
crates/bffs/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "bffs"
version = "0.1.0"
edition = "2024"
[dependencies]
bitflags = "2"
[features]
alloc = []
std = ["alloc"]

2
crates/bffs/justfile Normal file
View File

@@ -0,0 +1,2 @@
run:
cargo r --features std

View File

@@ -0,0 +1,165 @@
use crate::{error::Error, io::{Read, ReadLeExt}};
#[derive(Debug, Clone, Copy)]
pub struct Fat32BootSector {
// --- Jump + OEM ---
pub jump_boot: [u8; 3],
pub oem_name: [u8; 8],
// --- BPB (BIOS Parameter Block) ---
pub bytes_per_sector: u16,
pub sectors_per_cluster: u8,
pub reserved_sector_count: u16,
pub num_fats: u8,
pub root_entry_count: u16,
pub total_sectors_16: u16,
pub media: u8,
pub fat_size_16: u16,
pub sectors_per_track: u16,
pub num_heads: u16,
pub hidden_sectors: u32,
pub total_sectors_32: u32,
// --- FAT32 Extended BPB ---
pub fat_size_32: u32,
pub ext_flags: u16,
pub fs_version: u16,
pub root_cluster: u32,
pub fs_info: u16,
pub bk_boot_sec: u16,
pub reserved: [u8; 12],
// --- FAT32 Drive Info ---
pub drive_number: u8,
pub reserved1: u8,
pub boot_signature: u8,
pub volume_id: u32,
pub volume_label: [u8; 11],
pub fs_type: [u8; 8],
// --- Boot code ---
pub boot_code: [u8; 420],
// --- Signature ---
pub boot_sector_signature: u16, // 0xAA55
}
impl core::fmt::Display for Fat32BootSector {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Fat32BootSector")
.field("jump_boot", &self.jump_boot)
.field("oem_name", &self.get_oem_name())
.field("bytes_per_sector", &self.bytes_per_sector)
.field("sectors_per_cluster", &self.sectors_per_cluster)
.field("reserved_sector_count", &self.reserved_sector_count)
.field("num_fats", &self.num_fats)
.field("root_entry_count", &self.root_entry_count)
.field("total_sectors_16", &self.total_sectors_16)
.field("media", &self.media)
.field("fat_size_16", &self.fat_size_16)
.field("sectors_per_track", &self.sectors_per_track)
.field("num_heads", &self.num_heads)
.field("hidden_sectors", &self.hidden_sectors)
.field("total_sectors_32", &self.total_sectors_32)
.field("fat_size_32", &self.fat_size_32)
.field("ext_flags", &self.ext_flags)
.field("fs_version", &self.fs_version)
.field("root_cluster", &self.root_cluster)
.field("fs_info", &self.fs_info)
.field("bk_boot_sec", &self.bk_boot_sec)
.field("reserved", &self.reserved)
.field("drive_number", &self.drive_number)
.field("reserved1", &self.reserved1)
.field("boot_signature", &self.boot_signature)
.field("volume_id", &self.volume_id)
.field("volume_label", &self.get_volume_label())
.field("fs_type", &self.get_fs_type())
.field("boot_code", &"[_; 420]")
.field("boot_sector_signature", &self.boot_sector_signature)
.finish()
}
}
impl Fat32BootSector {
pub fn get_oem_name(&self) -> Result<&str, core::str::Utf8Error> {
str::from_utf8(&self.oem_name)
}
pub fn get_volume_label(&self) -> Result<&str, core::str::Utf8Error> {
str::from_utf8(&self.volume_label)
}
pub fn get_fs_type(&self) -> Result<&str, core::str::Utf8Error> {
str::from_utf8(&self.fs_type)
}
}
impl Fat32BootSector {
pub fn deserialize<T: Read>(disk: &mut T) -> Result<Self, Error<T::Error>> {
let mut jump_boot = [0u8; _];
disk.read_exact(&mut jump_boot)?;
let mut oem_name = [0u8; _];
disk.read_exact(&mut oem_name)?;
let bytes_per_sector = disk.read_u16_le()?;
let sectors_per_cluster = disk.read_u8()?;
let reserved_sector_count = disk.read_u16_le()?;
let num_fats = disk.read_u8()?;
let root_entry_count = disk.read_u16_le()?;
let total_sectors_16 = disk.read_u16_le()?;
let media = disk.read_u8()?;
let fat_size_16 = disk.read_u16_le()?;
let sectors_per_track = disk.read_u16_le()?;
let num_heads = disk.read_u16_le()?;
let hidden_sectors = disk.read_u32_le()?;
let total_sectors_32 = disk.read_u32_le()?;
let fat_size_32 = disk.read_u32_le()?;
let ext_flags = disk.read_u16_le()?;
let fs_version = disk.read_u16_le()?;
let root_cluster = disk.read_u32_le()?;
let fs_info = disk.read_u16_le()?;
let bk_boot_sec = disk.read_u16_le()?;
let mut reserved = [0u8; _];
disk.read_exact(&mut reserved)?;
let drive_number = disk.read_u8()?;
let reserved1 = disk.read_u8()?;
let boot_signature = disk.read_u8()?;
let volume_id = disk.read_u32_le()?;
let mut volume_label = [0u8; _];
disk.read_exact(&mut volume_label)?;
let mut fs_type = [0u8; _];
disk.read_exact(&mut fs_type)?;
let mut boot_code = [0u8; _];
disk.read_exact(&mut boot_code)?;
let boot_sector_signature = disk.read_u16_le()?;
Ok(Self {
jump_boot,
oem_name,
bytes_per_sector,
sectors_per_cluster,
reserved_sector_count,
num_fats,
root_entry_count,
total_sectors_16,
media,
fat_size_16,
sectors_per_track,
num_heads,
hidden_sectors,
total_sectors_32,
fat_size_32,
ext_flags,
fs_version,
root_cluster,
fs_info,
bk_boot_sec,
reserved,
drive_number,
reserved1,
boot_signature,
volume_id,
volume_label,
fs_type,
boot_code,
boot_sector_signature,
})
}
}

18
crates/bffs/src/consts.rs Normal file
View File

@@ -0,0 +1,18 @@
use bitflags::bitflags;
bitflags! {
pub struct FATAttr: u8 {
const READ_ONLY = 0x01;
const HIDDEN = 0x02;
const SYSTEM = 0x04;
const VOLUME_ID = 0x08;
const DIRECTORY = 0x10;
const ARCHIVE = 0x20;
const LFN = 0x0F;
}
}
pub const FAT32_CLUSTER_MASK: u32 = 0x0FFFFFFF;
pub const FAT32_FREE_CLUSTER: u32 = 0x00000000;
pub const FAT32_BAD_CLUSTER: u32 = 0x0FFFFFF7;
pub const FAT32_END_OF_CHAIN: u32 = 0x0FFFFFF8;

103
crates/bffs/src/dir.rs Normal file
View File

@@ -0,0 +1,103 @@
use crate::{
entry::{DirEntry, DirectoryIterator},
error::Error,
file::{File, RawFile},
io::{self, IoBase, Read, Seek, Write},
path::Path,
Fat32FileSystem, ReadSeek, ReadWriteSeek,
};
pub struct Dir<'a, T> {
raw: RawFile<'a, T>,
fs: &'a Fat32FileSystem<T>,
}
impl<'a, T: IoBase> IoBase for Dir<'a, T> {
type Error = T::Error;
}
impl<'a, T: ReadSeek> Seek for Dir<'a, T> {
fn seek(&mut self, pos: io::SeekFrom) -> Result<u64, Error<Self::Error>> {
self.raw.seek(pos)
}
}
impl<'a, T: ReadSeek> Read for Dir<'a, T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error<Self::Error>> {
self.raw.read(buf)
}
}
impl<'a, T: ReadWriteSeek> Write for Dir<'a, T> {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error<Self::Error>> {
self.raw.write(buf)
}
fn flush(&mut self) -> Result<(), Error<Self::Error>> {
self.raw.flush()
}
}
impl<'a, T> Dir<'a, T> {
pub fn new(raw: RawFile<'a, T>, fs: &'a Fat32FileSystem<T>) -> Self {
Self { raw, fs }
}
pub fn iter(&self) -> DirectoryIterator<'a, T> {
DirectoryIterator::new(RawFile::new(self.fs, self.raw.first_cluster(), None))
}
}
impl<'a, T: ReadSeek> Dir<'a, T> {
pub fn open_entry<'b, P: Into<Path<'b>>>(
&self,
path: P,
) -> Result<DirEntry<'a, T>, Error<T::Error>> {
let path = path.into();
if path.is_absolute() {
return self.fs.open_entry(path);
}
for file in self.iter() {
let f = file?;
if f.name_is(path.as_str()) {
return Ok(f);
}
}
Err(Error::NotFound)
}
pub fn open_file<'b, P: Into<Path<'b>>>(
&self,
path: P,
) -> Result<File<'a, T>, Error<T::Error>> {
let path = path.into();
if path.is_absolute() {
return self.fs.open_file(path);
}
let (start, dirname) = path.split_path();
let entry = self.open_entry(start)?;
match dirname {
Some(name) => {
if !entry.is_dir() {
return Err(Error::NotFound);
}
entry.to_dir().open_file(name)
}
None => {
if !entry.is_file() {
return Err(Error::NotFound);
}
Ok(entry.to_file())
}
}
}
pub fn open_dir<'b, P: Into<Path<'b>>>(&self, path: P) -> Result<Self, Error<T::Error>> {
let path = path.into();
if path.is_absolute() {
return self.fs.open_dir(path);
}
let (start, dirname) = path.split_path();
let entry = self.open_entry(start)?;
if !entry.is_dir() {
return Err(Error::NotFound);
}
match dirname {
Some(name) => entry.to_dir().open_dir(name),
None => Ok(entry.to_dir()),
}
}
}

346
crates/bffs/src/entry.rs Normal file
View File

@@ -0,0 +1,346 @@
use core::{array::IntoIter, iter::Copied, str::Utf8Error};
use core::{char::DecodeUtf16, marker::PhantomData, slice::Iter};
use crate::{
consts::FATAttr,
dir::Dir,
error::Error,
file::{File, RawFile},
io::{IoBase, Read, ReadLeExt},
Fat32FileSystem, ReadSeek,
};
#[cfg(feature = "alloc")]
use alloc::borrow::ToOwned;
#[cfg(feature = "alloc")]
use alloc::string::String;
#[derive(Debug, Clone, Copy)]
pub struct FatDirEntry {
pub name: [u8; 11],
pub attr: u8,
pub nt_reserved: u8,
pub creation_time_tenth: u8,
pub creation_time: u16,
pub creation_date: u16,
pub last_access_date: u16,
pub first_cluster_high: u16,
pub write_time: u16,
pub write_date: u16,
pub first_cluster_low: u16,
pub file_size: u32,
}
impl FatDirEntry {
pub fn deserialize<T: Read>(reader: &mut T) -> Result<Self, Error<T::Error>> {
let mut name = [0u8; _];
reader.read_exact(&mut name)?;
let attr = reader.read_u8()?;
let nt_reserved = reader.read_u8()?;
let creation_time_tenth = reader.read_u8()?;
let creation_time = reader.read_u16_le()?;
let creation_date = reader.read_u16_le()?;
let last_access_date = reader.read_u16_le()?;
let first_cluster_high = reader.read_u16_le()?;
let write_time = reader.read_u16_le()?;
let write_date = reader.read_u16_le()?;
let first_cluster_low = reader.read_u16_le()?;
let file_size = reader.read_u32_le()?;
Ok(Self {
name,
attr,
nt_reserved,
creation_time_tenth,
creation_time,
creation_date,
last_access_date,
first_cluster_high,
write_time,
write_date,
first_cluster_low,
file_size,
})
}
pub fn is_dir(&self) -> bool {
(self.attr & FATAttr::DIRECTORY.bits()) != 0
}
pub fn is_file(&self) -> bool {
!self.is_dir()
}
pub fn first_cluster(&self) -> u32 {
((self.first_cluster_high as u32) << 16) | (self.first_cluster_low as u32)
}
}
#[derive(Debug, Clone, Copy)]
pub struct FatLfnEntry {
pub sequence_number: u8,
pub name_part1: [u16; 5],
pub attr: u8,
pub type_u: u8,
pub checksum: u8,
pub name_part2: [u16; 6],
pub first_cluster: u16,
pub name_part3: [u16; 2],
}
impl From<FatDirEntry> for FatLfnEntry {
fn from(value: FatDirEntry) -> Self {
let sequence_number = value.name[0];
let name_part1 = [
u16::from_le_bytes([value.name[1], value.name[2]]),
u16::from_le_bytes([value.name[3], value.name[4]]),
u16::from_le_bytes([value.name[5], value.name[6]]),
u16::from_le_bytes([value.name[7], value.name[8]]),
u16::from_le_bytes([value.name[9], value.name[10]]),
];
let name_part2 = [
value.creation_time,
value.creation_date,
value.last_access_date,
value.first_cluster_high,
value.write_time,
value.write_date,
];
let name_part3 = [
value.file_size as u16,
(value.file_size >> u16::BITS) as u16,
];
Self {
sequence_number,
name_part1,
attr: value.attr,
type_u: value.nt_reserved,
checksum: value.creation_time_tenth,
name_part2,
first_cluster: value.first_cluster_low,
name_part3,
}
}
}
pub type FatEntry = u32;
#[derive(Debug, Clone, Copy)]
pub struct LongFileNameBuilder<T> {
inner: [u16; 256],
_phantom: PhantomData<T>,
}
impl<T: IoBase> LongFileNameBuilder<T> {
pub fn new() -> Self {
Self {
inner: [0; _],
_phantom: PhantomData,
}
}
pub fn push(&mut self, entry: &FatLfnEntry, index: usize) {
// Each name entry are 13 bytes long
let char_offset = index * 13;
if char_offset <= 256 {
// First segment
self.inner[char_offset..char_offset + 5].copy_from_slice(&entry.name_part1);
if char_offset + 13 <= 256 {
// Second segment
self.inner[char_offset + 5..char_offset + 11].copy_from_slice(&entry.name_part2);
// Third segment
self.inner[char_offset + 11..char_offset + 13].copy_from_slice(&entry.name_part3);
} else {
// If the name is longer than 247 characters, the last entry is only 9 characters long.
self.inner[char_offset + 5..char_offset + 9]
.copy_from_slice(&entry.name_part2[..4]);
}
}
}
#[cfg(feature = "alloc")]
pub fn to_string(&self) -> Result<String, Error<T::Error>> {
self.iter().try_collect::<String>()
}
}
impl<T: IoBase> IntoIterator for LongFileNameBuilder<T> {
type Item = <LongFileNameIterator<T, IntoIter<u16, 256>> as Iterator>::Item;
type IntoIter = LongFileNameIterator<T, IntoIter<u16, 256>>;
fn into_iter(self) -> Self::IntoIter {
LongFileNameIterator {
iterator: char::decode_utf16(self.inner),
_phantom: PhantomData,
}
}
}
impl<T: IoBase> LongFileNameBuilder<T> {
pub fn iter(&self) -> LongFileNameIterator<T, Copied<Iter<'_, u16>>> {
LongFileNameIterator {
iterator: char::decode_utf16(self.inner.iter().copied()),
_phantom: PhantomData,
}
}
}
pub struct LongFileNameIterator<T, I: Iterator<Item = u16>> {
iterator: DecodeUtf16<I>,
_phantom: PhantomData<T>,
}
impl<T: IoBase, I: Iterator<Item = u16>> Iterator for LongFileNameIterator<T, I> {
type Item = Result<char, Error<T::Error>>;
fn next(&mut self) -> Option<Self::Item> {
match self.iterator.next()? {
Ok(value) => {
if value == '\0' {
None
} else {
Some(Ok(value))
}
}
Err(_) => Some(Err(Error::UnsupportedFileNameCharacter)),
}
}
}
impl<T: IoBase> Default for LongFileNameBuilder<T> {
fn default() -> Self {
Self::new()
}
}
pub struct DirectoryIterator<'a, T> {
directory_file: RawFile<'a, T>,
}
impl<'a, T> DirectoryIterator<'a, T> {
pub fn new(directory_file: RawFile<'a, T>) -> Self {
Self { directory_file }
}
}
impl<'a, T: ReadSeek> Iterator for DirectoryIterator<'a, T> {
type Item = Result<DirEntry<'a, T>, Error<T::Error>>;
fn next(&mut self) -> Option<Self::Item> {
let mut lfn_builder = LongFileNameBuilder::new();
let mut has_lfn = false;
loop {
let entry = match FatDirEntry::deserialize(&mut self.directory_file) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
// There is no entry after this one
if entry.name[0] == 0x00 {
return None;
}
// The file was suppressed
if entry.name[0] == 0xE5 {
return self.next();
}
if entry.attr == FATAttr::LFN.bits() {
// Current entry is a LongFileName entry
let lfn: FatLfnEntry = entry.into();
let index = ((lfn.sequence_number & 0x1F) - 1) as usize;
lfn_builder.push(&lfn, index);
has_lfn = true;
} else {
// Check if the entry is "." or ".."
if entry.name[0] == b'.'
&& (entry.name[1] == b'.' || entry.name[1] == b' ')
&& entry.name.iter().skip(2).all(|c| *c == b' ')
{
return self.next();
} else {
return Some(Ok(DirEntry::new(
entry,
entry.name,
has_lfn.then_some(lfn_builder),
self.directory_file.fs,
)));
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct DirEntry<'a, T> {
entry: FatDirEntry,
short_name: [u8; 11],
long_name: Option<LongFileNameBuilder<T>>,
fs: &'a Fat32FileSystem<T>,
}
impl<'a, T: IoBase> DirEntry<'a, T> {
fn new(
entry: FatDirEntry,
short_name: [u8; 11],
long_name: Option<LongFileNameBuilder<T>>,
fs: &'a Fat32FileSystem<T>,
) -> Self {
Self {
entry,
short_name,
long_name,
fs,
}
}
#[cfg(feature = "alloc")]
pub fn name(&self) -> Result<String, Error<T::Error>> {
if let Some(long_name) = self.long_name() {
long_name.to_string()
} else {
self.short_name()
.map(|n| n.to_owned())
.map_err(|_| Error::UnsupportedFileNameCharacter)
}
}
pub fn long_name(&self) -> Option<&LongFileNameBuilder<T>> {
self.long_name.as_ref()
}
pub fn short_name(&self) -> Result<&str, Utf8Error> {
str::from_utf8(&self.short_name)
}
pub fn name_is(&self, name: &str) -> bool {
if let Some(long_name) = self.long_name() {
long_name
.iter()
.eq_by(name.chars(), |a, b| a.is_ok_and(|a| a == b))
} else {
self.short_name().is_ok_and(|n| n == name)
}
}
pub fn is_dir(&self) -> bool {
self.entry.is_dir()
}
pub fn is_file(&self) -> bool {
self.entry.is_file()
}
pub fn to_dir(&self) -> Dir<'a, T> {
Dir::new(
RawFile::new(self.fs, self.entry.first_cluster(), None),
self.fs,
)
}
pub fn to_file(&self) -> File<'a, T> {
File::new(
RawFile::new(
self.fs,
self.entry.first_cluster(),
Some(self.entry.file_size),
),
self.fs,
)
}
}

117
crates/bffs/src/error.rs Normal file
View File

@@ -0,0 +1,117 @@
/// Error enum with all errors that can be returned by functions from this crate
///
/// Generic parameter `T` is a type of external error returned by the user provided storage
#[derive(Debug)]
#[non_exhaustive]
pub enum Error<T> {
/// A user provided storage instance returned an error during an input/output operation.
Io(T),
/// A read operation cannot be completed because an end of a file has been reached prematurely.
UnexpectedEof,
/// A write operation cannot be completed because `Write::write` returned 0.
WriteZero,
/// A parameter was incorrect.
InvalidInput,
/// A requested file or directory has not been found.
NotFound,
/// A file or a directory with the same name already exists.
AlreadyExists,
/// An operation cannot be finished because a directory is not empty.
DirectoryIsNotEmpty,
/// File system internal structures are corrupted/invalid.
CorruptedFileSystem,
/// There is not enough free space on the storage to finish the requested operation.
NotEnoughSpace,
/// The provided file name is either too long or empty.
InvalidFileNameLength,
/// The provided file name contains an invalid character.
UnsupportedFileNameCharacter,
/// The file content contains invalid UTF-8 characters.
InvalidUTF8,
}
impl<T: IoError> From<T> for Error<T> {
fn from(error: T) -> Self {
Error::Io(error)
}
}
impl<T: core::fmt::Display> core::fmt::Display for Error<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::Io(io_error) => write!(f, "IO error: {}", io_error),
Error::UnexpectedEof => write!(f, "Unexpected end of file"),
Error::NotEnoughSpace => write!(f, "Not enough space"),
Error::WriteZero => write!(f, "Write zero"),
Error::InvalidInput => write!(f, "Invalid input"),
Error::InvalidFileNameLength => write!(f, "Invalid file name length"),
Error::UnsupportedFileNameCharacter => write!(f, "Unsupported file name character"),
Error::DirectoryIsNotEmpty => write!(f, "Directory is not empty"),
Error::NotFound => write!(f, "No such file or directory"),
Error::AlreadyExists => write!(f, "File or directory already exists"),
Error::CorruptedFileSystem => write!(f, "Corrupted file system"),
Error::InvalidUTF8 => write!(f, "File contains invalid UTF-8 characters"),
}
}
}
/// Trait that should be implemented by errors returned from the user supplied storage.
///
/// Implementations for `std::io::Error` and `()` are provided by this crate.
pub trait IoError: core::fmt::Debug {
fn is_interrupted(&self) -> bool;
fn new_unexpected_eof_error() -> Self;
fn new_write_zero_error() -> Self;
}
impl<T: core::fmt::Debug + IoError> IoError for Error<T> {
fn is_interrupted(&self) -> bool {
match self {
Error::<T>::Io(io_error) => io_error.is_interrupted(),
_ => false,
}
}
fn new_unexpected_eof_error() -> Self {
Error::<T>::UnexpectedEof
}
fn new_write_zero_error() -> Self {
Error::<T>::WriteZero
}
}
impl IoError for () {
fn is_interrupted(&self) -> bool {
false
}
fn new_unexpected_eof_error() -> Self {
// empty
}
fn new_write_zero_error() -> Self {
// empty
}
}
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
impl IoError for std::io::Error {
fn is_interrupted(&self) -> bool {
self.kind() == std::io::ErrorKind::Interrupted
}
fn new_unexpected_eof_error() -> Self {
Self::new(
std::io::ErrorKind::UnexpectedEof,
"failed to fill whole buffer",
)
}
fn new_write_zero_error() -> Self {
Self::new(
std::io::ErrorKind::WriteZero,
"failed to write whole buffer",
)
}
}

160
crates/bffs/src/file.rs Normal file
View File

@@ -0,0 +1,160 @@
use crate::{
consts::{FAT32_BAD_CLUSTER, FAT32_END_OF_CHAIN},
error::Error,
io::{self, IoBase, Read, Seek, Write},
Fat32FileSystem, ReadSeek, ReadWriteSeek,
};
#[derive(Debug, Clone)]
pub struct RawFile<'a, T> {
pub(crate) fs: &'a Fat32FileSystem<T>,
first_cluster: u32,
current_cluster: u32,
pos: u64,
size: Option<u32>,
}
impl<'a, T> RawFile<'a, T> {
pub fn new(fs: &'a Fat32FileSystem<T>, start_cluster: u32, size: Option<u32>) -> Self {
Self {
fs,
first_cluster: start_cluster,
current_cluster: start_cluster,
pos: 0,
size,
}
}
pub fn first_cluster(&self) -> u32 {
self.first_cluster
}
}
impl<'a, T: IoBase> IoBase for RawFile<'a, T> {
type Error = T::Error;
}
impl<'a, T: ReadSeek> Seek for RawFile<'a, T> {
fn seek(&mut self, pos: io::SeekFrom) -> Result<u64, Error<Self::Error>> {
let new_pos = match pos {
io::SeekFrom::Start(s) => s,
io::SeekFrom::Current(c) => (self.pos as i64 + c) as u64,
io::SeekFrom::End(e) => {
let s = self.size.unwrap_or(0) as i64;
(s + e) as u64
}
};
// Position changed, compute the new cluster
if new_pos != self.pos {
let cluster_size = self.fs.cluster_size();
let cluster_index = new_pos / cluster_size;
// Find the new cluster from the start
let mut c = self.first_cluster;
for _ in 0..cluster_index {
c = self.fs.get_next_cluster(c)?;
if c >= FAT32_BAD_CLUSTER {
break;
}
}
self.current_cluster = c;
self.pos = new_pos;
}
Ok(self.pos)
}
}
impl<'a, T: ReadSeek> Read for RawFile<'a, T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error<Self::Error>> {
let max_len = self
.size
.map(|size| core::cmp::min(buf.len(), size as usize - self.pos as usize))
.unwrap_or(buf.len());
let mut total_read = 0;
let cluster_size = self.fs.cluster_size();
while total_read < max_len {
// Compute how much can be read in the current cluster
let offset_in_cluster = self.pos % cluster_size;
let remaining_in_cluster = cluster_size - offset_in_cluster;
let to_read = core::cmp::min(max_len - total_read, remaining_in_cluster as usize);
// Read from disk
let physical_offset =
self.fs.cluster_to_offset(self.current_cluster) + offset_in_cluster;
self.fs
.device
.borrow_mut()
.seek(io::SeekFrom::Start(physical_offset))?;
let read = self
.fs
.device
.borrow_mut()
.read(&mut buf[total_read..total_read + to_read])?;
total_read += read;
self.pos += read as u64;
if read != to_read {
// The read call didn't read all bytes to fill buf, we can return early
return Ok(total_read);
}
// At the end of a cluster, go to the next one for the next read operation
if self.pos.is_multiple_of(cluster_size) {
if self.current_cluster >= FAT32_END_OF_CHAIN {
break; // End of the fat file
}
self.current_cluster = self.fs.get_next_cluster(self.current_cluster)?;
}
}
Ok(total_read)
}
}
impl<'a, T: ReadWriteSeek> Write for RawFile<'a, T> {
fn write(&mut self, _buf: &[u8]) -> Result<usize, Error<Self::Error>> {
todo!()
}
fn flush(&mut self) -> Result<(), Error<Self::Error>> {
todo!()
}
}
#[allow(unused)]
pub struct File<'a, T> {
raw: RawFile<'a, T>,
fs: &'a Fat32FileSystem<T>,
}
impl<'a, T: IoBase> IoBase for File<'a, T> {
type Error = T::Error;
}
impl<'a, T: ReadSeek> Seek for File<'a, T> {
fn seek(&mut self, pos: io::SeekFrom) -> Result<u64, Error<Self::Error>> {
self.raw.seek(pos)
}
}
impl<'a, T: ReadSeek> Read for File<'a, T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error<Self::Error>> {
self.raw.read(buf)
}
}
impl<'a, T: ReadWriteSeek> Write for File<'a, T> {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error<Self::Error>> {
self.raw.write(buf)
}
fn flush(&mut self) -> Result<(), Error<Self::Error>> {
self.raw.flush()
}
}
impl<'a, T> File<'a, T> {
pub fn new(raw: RawFile<'a, T>, fs: &'a Fat32FileSystem<T>) -> Self {
Self { raw, fs }
}
}

293
crates/bffs/src/io.rs Normal file
View File

@@ -0,0 +1,293 @@
use crate::error::{Error, IoError};
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
/// Provides IO error as an associated type.
///
/// Must be implemented for all types that also implement at least one of the following traits: `Read`, `Write`,
/// `Seek`.
pub trait IoBase {
/// Type of errors returned by input/output operations.
type Error: IoError;
}
/// The `Read` trait allows for reading bytes from a source.
///
/// It is based on the `std::io::Read` trait.
pub trait Read: IoBase {
/// Pull some bytes from this source into the specified buffer, returning how many bytes were read.
///
/// This function does not provide any guarantees about whether it blocks waiting for data, but if an object needs
/// to block for a read and cannot, it will typically signal this via an Err return value.
///
/// If the return value of this method is `Ok(n)`, then it must be guaranteed that `0 <= n <= buf.len()`. A nonzero
/// `n` value indicates that the buffer buf has been filled in with n bytes of data from this source. If `n` is
/// `0`, then it can indicate one of two scenarios:
///
/// 1. This reader has reached its "end of file" and will likely no longer be able to produce bytes. Note that this
/// does not mean that the reader will always no longer be able to produce bytes.
/// 2. The buffer specified was 0 bytes in length.
///
/// It is not an error if the returned value `n` is smaller than the buffer size, even when the reader is not at
/// the end of the stream yet. This may happen for example because fewer bytes are actually available right now
/// (e. g. being close to end-of-file) or because `read()` was interrupted by a signal.
///
/// # Errors
///
/// If this function encounters any form of I/O or other error, an error will be returned. If an error is returned
/// then it must be guaranteed that no bytes were read.
/// An error for which `IoError::is_interrupted` returns true is non-fatal and the read operation should be retried
/// if there is nothing else to do.
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error<Self::Error>>;
/// Read the exact number of bytes required to fill `buf`.
///
/// This function reads as many bytes as necessary to completely fill the specified buffer `buf`.
///
/// # Errors
///
/// If this function encounters an error for which `IoError::is_interrupted` returns true then the error is ignored
/// and the operation will continue.
///
/// If this function encounters an end of file before completely filling the buffer, it returns an error
/// instantiated by a call to `IoError::new_unexpected_eof_error`. The contents of `buf` are unspecified in this
/// case.
///
/// If this function returns an error, it is unspecified how many bytes it has read, but it will never read more
/// than would be necessary to completely fill the buffer.
fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<(), Error<Self::Error>> {
while !buf.is_empty() {
match self.read(buf) {
Ok(0) => break,
Ok(n) => {
let tmp = buf;
buf = &mut tmp[n..];
}
Err(ref e) if e.is_interrupted() => {}
Err(e) => return Err(e),
}
}
if buf.is_empty() {
Ok(())
} else {
Err(Error::<Self::Error>::new_unexpected_eof_error())
}
}
#[cfg(feature = "alloc")]
fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize, Error<Self::Error>> {
const CHUNK_SIZE: usize = 32;
let start_len = buf.len();
loop {
let mut chunk_buf = [0; CHUNK_SIZE];
let read = self.read(&mut chunk_buf)?;
buf.extend_from_slice(&chunk_buf[..read]);
if read == 0 {
return Ok(buf.len() - start_len);
}
}
}
#[cfg(feature = "alloc")]
fn read_to_string(&mut self, buf: &mut String) -> Result<usize, Error<Self::Error>> {
let read = self.read_to_end(unsafe { buf.as_mut_vec() })?;
if str::from_utf8(buf.as_bytes()).is_err() {
Err(Error::InvalidUTF8)
} else {
Ok(read)
}
}
}
/// The `Write` trait allows for writing bytes into the sink.
///
/// It is based on the `std::io::Write` trait.
pub trait Write: IoBase {
/// Write a buffer into this writer, returning how many bytes were written.
///
/// # Errors
///
/// Each call to write may generate an I/O error indicating that the operation could not be completed. If an error
/// is returned then no bytes in the buffer were written to this writer.
/// It is not considered an error if the entire buffer could not be written to this writer.
fn write(&mut self, buf: &[u8]) -> Result<usize, Error<Self::Error>>;
/// Attempts to write an entire buffer into this writer.
///
/// This method will continuously call `write` until there is no more data to be written or an error is returned.
/// Errors for which `IoError::is_interrupted` method returns true are being skipped. This method will not return
/// until the entire buffer has been successfully written or such an error occurs.
/// If `write` returns 0 before the entire buffer has been written this method will return an error instantiated by
/// a call to `IoError::new_write_zero_error`.
///
/// # Errors
///
/// This function will return the first error for which `IoError::is_interrupted` method returns false that `write`
/// returns.
fn write_all(&mut self, mut buf: &[u8]) -> Result<(), Error<Self::Error>> {
while !buf.is_empty() {
match self.write(buf) {
Ok(0) => {
return Err(Error::<Self::Error>::new_write_zero_error());
}
Ok(n) => buf = &buf[n..],
Err(ref e) if e.is_interrupted() => {}
Err(e) => return Err(e),
}
}
Ok(())
}
/// Flush this output stream, ensuring that all intermediately buffered contents reach their destination.
///
/// # Errors
///
/// It is considered an error if not all bytes could be written due to I/O errors or EOF being reached.
fn flush(&mut self) -> Result<(), Error<Self::Error>>;
}
/// Enumeration of possible methods to seek within an I/O object.
///
/// It is based on the `std::io::SeekFrom` enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SeekFrom {
/// Sets the offset to the provided number of bytes.
Start(u64),
/// Sets the offset to the size of this object plus the specified number of bytes.
End(i64),
/// Sets the offset to the current position plus the specified number of bytes.
Current(i64),
}
/// The `Seek` trait provides a cursor which can be moved within a stream of bytes.
///
/// It is based on the `std::io::Seek` trait.
pub trait Seek: IoBase {
/// Seek to an offset, in bytes, in a stream.
///
/// A seek beyond the end of a stream or to a negative position is not allowed.
///
/// If the seek operation completed successfully, this method returns the new position from the start of the
/// stream. That position can be used later with `SeekFrom::Start`.
///
/// # Errors
/// Seeking to a negative offset is considered an error.
fn seek(&mut self, pos: SeekFrom) -> Result<u64, Error<Self::Error>>;
}
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
impl From<SeekFrom> for std::io::SeekFrom {
fn from(from: SeekFrom) -> Self {
match from {
SeekFrom::Start(n) => std::io::SeekFrom::Start(n),
SeekFrom::End(n) => std::io::SeekFrom::End(n),
SeekFrom::Current(n) => std::io::SeekFrom::Current(n),
}
}
}
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
impl From<std::io::SeekFrom> for SeekFrom {
fn from(from: std::io::SeekFrom) -> Self {
match from {
std::io::SeekFrom::Start(n) => SeekFrom::Start(n),
std::io::SeekFrom::End(n) => SeekFrom::End(n),
std::io::SeekFrom::Current(n) => SeekFrom::Current(n),
}
}
}
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
impl IoBase for std::fs::File {
type Error = std::io::Error;
}
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
impl<T: std::io::Read + IoBase<Error = std::io::Error>> Read for T {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, Error<Self::Error>> {
self.read(buf).map_err(Error::Io)
}
fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), Error<Self::Error>> {
self.read_exact(buf).map_err(Error::Io)
}
}
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
impl<T: std::io::Write + IoBase<Error = std::io::Error>> Write for T {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error<Self::Error>> {
self.write(buf).map_err(Error::Io)
}
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error<Self::Error>> {
self.write_all(buf).map_err(Error::Io)
}
fn flush(&mut self) -> Result<(), Error<Self::Error>> {
self.flush().map_err(Error::Io)
}
}
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
impl<T: std::io::Seek + IoBase<Error = std::io::Error>> Seek for T {
fn seek(&mut self, pos: SeekFrom) -> Result<u64, Error<Self::Error>> {
self.seek(pos.into()).map_err(Error::Io)
}
}
pub(crate) trait ReadLeExt {
type Error;
fn read_u8(&mut self) -> Result<u8, Error<Self::Error>>;
fn read_u16_le(&mut self) -> Result<u16, Error<Self::Error>>;
fn read_u32_le(&mut self) -> Result<u32, Error<Self::Error>>;
}
impl<T: Read> ReadLeExt for T {
type Error = <Self as IoBase>::Error;
fn read_u8(&mut self) -> Result<u8, Error<Self::Error>> {
let mut buf = [0_u8; 1];
self.read_exact(&mut buf)?;
Ok(buf[0])
}
fn read_u16_le(&mut self) -> Result<u16, Error<Self::Error>> {
let mut buf = [0_u8; 2];
self.read_exact(&mut buf)?;
Ok(u16::from_le_bytes(buf))
}
fn read_u32_le(&mut self) -> Result<u32, Error<Self::Error>> {
let mut buf = [0_u8; 4];
self.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
}
#[allow(unused)]
pub(crate) trait WriteLeExt {
type Error;
fn write_u8(&mut self, n: u8) -> Result<(), Error<Self::Error>>;
fn write_u16_le(&mut self, n: u16) -> Result<(), Error<Self::Error>>;
fn write_u32_le(&mut self, n: u32) -> Result<(), Error<Self::Error>>;
}
impl<T: Write> WriteLeExt for T {
type Error = <Self as IoBase>::Error;
fn write_u8(&mut self, n: u8) -> Result<(), Error<Self::Error>> {
self.write_all(&[n])
}
fn write_u16_le(&mut self, n: u16) -> Result<(), Error<Self::Error>> {
self.write_all(&n.to_le_bytes())
}
fn write_u32_le(&mut self, n: u32) -> Result<(), Error<Self::Error>> {
self.write_all(&n.to_le_bytes())
}
}

157
crates/bffs/src/lib.rs Normal file
View File

@@ -0,0 +1,157 @@
#![feature(iterator_try_collect, iter_order_by)]
#![cfg_attr(any(not(feature = "std"), target_arch = "riscv64"), no_std)]
use core::cell::RefCell;
use core::fmt::Display;
use crate::{
boot_sector::Fat32BootSector,
consts::FAT32_CLUSTER_MASK,
dir::Dir,
entry::{DirEntry, FatEntry},
error::Error,
file::{File, RawFile},
io::{Read, ReadLeExt, Seek, Write},
path::Path,
};
#[cfg(feature = "alloc")]
extern crate alloc;
pub mod boot_sector;
pub mod consts;
pub mod dir;
pub mod entry;
pub mod error;
pub mod file;
pub mod io;
pub mod path;
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
pub trait WriteSeek: Write + Seek {}
impl<T: Write + Seek> WriteSeek for T {}
pub trait ReadWriteSeek: Read + Write + Seek {}
impl<T: Read + Write + Seek> ReadWriteSeek for T {}
#[derive(Debug, Clone, Copy)]
pub struct Fat32FsInfo {
pub lead_signature: u32,
pub reserved1: [u8; 480],
pub struct_signature: u32,
pub free_count: u32,
pub next_free: u32,
pub reserved2: [u8; 12],
pub trail_signature: u32,
}
impl Fat32FsInfo {
pub fn deserialize<T: Read>(disk: &mut T) -> Result<Self, Error<T::Error>> {
let lead_signature = disk.read_u32_le()?;
let mut reserved1 = [0u8; _];
disk.read_exact(&mut reserved1)?;
let struct_signature = disk.read_u32_le()?;
let free_count = disk.read_u32_le()?;
let next_free = disk.read_u32_le()?;
let mut reserved2 = [0u8; _];
disk.read_exact(&mut reserved2)?;
let trail_signature = disk.read_u32_le()?;
Ok(Self {
lead_signature,
reserved1,
struct_signature,
free_count,
next_free,
reserved2,
trail_signature,
})
}
}
#[derive(Debug)]
pub struct Fat32FileSystem<T> {
device: RefCell<T>,
pub boot_sector: Fat32BootSector,
}
impl<T> Display for Fat32FileSystem<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.boot_sector.fmt(f)
}
}
impl<T: ReadSeek> Fat32FileSystem<T> {
pub fn new(mut device: T) -> Result<Self, Error<T::Error>> {
device.seek(io::SeekFrom::Start(0))?;
let boot_sector = Fat32BootSector::deserialize(&mut device)?;
Ok(Self {
device: device.into(),
boot_sector,
})
}
/// Get the next cluster from the current one
fn get_next_cluster(&self, current_cluster: u32) -> Result<u32, Error<T::Error>> {
let fat_offset =
self.fat_start_offset() + (current_cluster as u64 * size_of::<FatEntry>() as u64);
self.device
.borrow_mut()
.seek(io::SeekFrom::Start(fat_offset))?;
let next = self.device.borrow_mut().read_u32_le()? & FAT32_CLUSTER_MASK;
Ok(next)
}
}
impl<T> Fat32FileSystem<T> {
/// Start offset of the FAT
pub fn fat_start_offset(&self) -> u64 {
(self.boot_sector.reserved_sector_count as u64) * (self.boot_sector.bytes_per_sector as u64)
}
/// Start offset for data
fn data_start_offset(&self) -> u64 {
let fat_size = self.boot_sector.fat_size_32 as u64;
let fats = (self.boot_sector.num_fats as u64) * fat_size;
fats * self.boot_sector.bytes_per_sector as u64 + self.fat_start_offset()
}
/// Convert a cluster number to an offset
fn cluster_to_offset(&self, cluster: u32) -> u64 {
let cluster_offset = (cluster.saturating_sub(2)) as u64
* self.boot_sector.sectors_per_cluster as u64
* self.boot_sector.bytes_per_sector as u64;
self.data_start_offset() + cluster_offset
}
fn cluster_size(&self) -> u64 {
(self.boot_sector.sectors_per_cluster as u64) * (self.boot_sector.bytes_per_sector as u64)
}
pub fn root_directory(&self) -> Dir<'_, T> {
Dir::new(
RawFile::new(self, self.boot_sector.root_cluster, None),
self,
)
}
}
impl<T: ReadSeek> Fat32FileSystem<T> {
pub fn open_entry<'b, P: Into<Path<'b>>>(
&self,
path: P,
) -> Result<DirEntry<'_, T>, Error<T::Error>> {
let path = path.into().as_str().trim_start_matches("/");
self.root_directory().open_entry(path)
}
pub fn open_dir<'b, P: Into<Path<'b>>>(&self, path: P) -> Result<Dir<'_, T>, Error<T::Error>> {
let path = path.into().as_str().trim_start_matches("/");
self.root_directory().open_dir(path)
}
pub fn open_file<'b, P: Into<Path<'b>>>(
&self,
path: P,
) -> Result<File<'_, T>, Error<T::Error>> {
let path = path.into().as_str().trim_start_matches("/");
self.root_directory().open_file(path)
}
}

34
crates/bffs/src/main.rs Normal file
View File

@@ -0,0 +1,34 @@
#![cfg_attr(any(not(feature = "std"), target_arch = "riscv64"), no_std)]
#[cfg(all(feature = "std", not(target_arch = "riscv64")))]
pub fn main() {
use bffs::{dir::Dir, io::Read, Fat32FileSystem, ReadSeek};
let file = std::fs::File::open("disk.img").unwrap();
let fs = Fat32FileSystem::new(file).unwrap();
// println!("{:#?}", fs);
// walk(fs.root_directory());
let mut f = fs.open_file("/usr/bin/test_pic").unwrap();
let mut content = Vec::new();
f.read_to_end(&mut content).unwrap();
println!("file content len: {}", content.len());
pub fn walk<T: ReadSeek>(dir: Dir<'_, T>) {
for entry in dir.iter() {
let entry = entry.unwrap();
println!("file {}", entry.name().unwrap());
if entry.is_dir() {
walk(entry.to_dir())
} else {
let mut file = entry.to_file();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
println!("file content {}", content);
}
}
}
}
#[cfg(any(not(feature = "std"), target_arch = "riscv64"))]
pub fn main() {}

33
crates/bffs/src/path.rs Normal file
View File

@@ -0,0 +1,33 @@
pub struct Path<'a> {
inner: &'a str,
}
impl<'a> From<&'a str> for Path<'a> {
fn from(value: &'a str) -> Self {
Self { inner: value }
}
}
impl<'a> AsRef<str> for Path<'a> {
fn as_ref(&self) -> &str {
self.inner
}
}
impl<'a> Path<'a> {
pub fn split_path(&self) -> (&'a str, Option<Path<'a>>) {
if let Some((start, end)) = self.inner.split_once("/") {
(start, Some(end.into()))
} else {
(self.inner, None)
}
}
pub fn as_str(&self) -> &'a str {
self.inner
}
pub fn is_absolute(&self) -> bool {
self.inner.starts_with("/")
}
pub fn is_relative(&self) -> bool {
!self.is_absolute()
}
}

View File

@@ -5,4 +5,4 @@ edition = "2024"
[dependencies]
os-std-macros = { path = "../os-std-macros" }
shared = { path = "../shared" }
shared = { path = "../shared", features = ["user"] }

View File

@@ -4,6 +4,7 @@ extern crate alloc;
pub mod prelude;
pub use shared::fs;
pub use shared::syscall;
#[macro_export]

View File

@@ -4,3 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
bffs = { path = "../bffs" }
[features]
kernel = []
user = []

9
crates/shared/src/fs.rs Normal file
View File

@@ -0,0 +1,9 @@
pub struct File {
fd: u64,
}
impl File {
pub unsafe fn new(fd: u64) -> Self {
Self { fd }
}
}

View File

@@ -1,3 +1,4 @@
#![no_std]
pub mod fs;
pub mod syscall;

View File

@@ -1,7 +1,10 @@
use core::{alloc::Layout, time::Duration};
use bffs::path::Path;
#[repr(u64)]
pub enum SysCall {
Open = 2,
Alloc = 40,
Dealloc = 41,
Exit = 60,
@@ -14,6 +17,7 @@ pub enum SysCall {
impl From<u64> for SysCall {
fn from(value: u64) -> Self {
match value {
2 => SysCall::Open,
40 => SysCall::Alloc,
41 => SysCall::Dealloc,
60 => SysCall::Exit,
@@ -124,3 +128,12 @@ pub fn dealloc(ptr: *mut u8, layout: core::alloc::Layout) {
syscall!(SysCall::Dealloc, ptr as u64, size as u64, align as u64);
}
}
pub fn open<'a, P: Into<Path<'a>>>(path: P) -> u64 {
unsafe {
let path_str = path.into().as_str();
let ptr = path_str.as_ptr();
let size = path_str.len();
let (fd, ..) = syscall!(SysCall::Open, ptr as u64, size as u64);
fd
}
}

View File

@@ -13,6 +13,7 @@ sync_filesystem:
build_user_prog prog:
RUSTFLAGS="-C relocation-model=pic -C link-arg=-Tuser.ld -C link-arg=-pie" cargo b {{ cargo_flags }} --package {{ prog }}
riscv64-elf-strip {{ bin_path / prog }}
cp {{ bin_path / prog }} {{ "mnt/usr/bin" / prog }}
# riscv64-elf-objcopy -O binary {{ bin_path / prog }} {{ "mnt/usr/bin" / prog }}
@@ -30,6 +31,11 @@ map_dir dir recipe:
qemu := "qemu-system-riscv64 -machine virt -device bochs-display -bios none -m 512M -device loader,file=disk.img,addr=0x90000000"
perf: build
{{ qemu }} -perfmap -kernel {{ bin_path / "kernel-rust" }}&
perf record --output=/tmp/perf.data --call-graph=dwarf -F 999 -p $(pidof qemu-system-riscv64) -- sleep 20; exit 0
cd /tmp && hotspot perf.data
gdb: build
{{ qemu }} -s -S -kernel {{ bin_path / "kernel-rust" }}&
gf2

View File

@@ -2,12 +2,17 @@
//!
//! Implements a minimal disk backend and exposes a global FILE_SYSTEM used by
//! the kernel to load user binaries.
use core::{cell::UnsafeCell, ops::Deref};
use core::{
cell::{LazyCell, UnsafeCell},
ops::Deref,
};
use bffs::{
io::{IoBase, Read, Seek},
path::Path,
Fat32FileSystem,
};
use hashbrown::HashMap;
const DISK_ADDR: *const u8 = 0x9000_0000 as *const _;
@@ -79,10 +84,20 @@ impl Read for Disk {
return Ok(0);
}
let size = usize::min(buf.len(), (self.size - self.pos) as usize);
(0..size).for_each(|i| {
buf[i] = unsafe { *DISK_ADDR.byte_add(i + self.pos as usize) };
});
unsafe {
core::ptr::copy_nonoverlapping(
DISK_ADDR.byte_add(self.pos as usize),
buf.as_mut_ptr(),
size,
);
}
self.pos += size as u64;
Ok(size)
}
}
pub struct KernelFDTable(LazyCell<HashMap<u64, Path<'static>>>);
unsafe impl Sync for KernelFDTable {}
pub static KERNEL_FILE_DESCRIPTOR_TABLE: KernelFDTable = KernelFDTable(LazyCell::new(HashMap::new));

View File

@@ -14,7 +14,7 @@ use crate::{
read_csr,
riscv::disable_interrupt,
scheduler::scheduler_without_ret,
set_csr,
set_csr, syscall,
time::{setup_next_timer_interrupt, IRQ_M_TIMER},
write_csr,
};
@@ -57,7 +57,6 @@ unsafe extern "C" fn machine_trap_handler(
c if c == EextensionID::Time as usize => match fid {
c if c == TimerFunctionID::SetTimer as usize => {
clear_csr!(mip, 1 << 5);
// setup_next_timer_interrupt();
}
_ => {}
},
@@ -122,16 +121,21 @@ unsafe extern "C" fn supervisor_trap_handler(
let a3: u64 = unsafe { (*interrupt_state).a[3] };
let syscall: SysCall = syscall_u64.into();
match syscall {
SysCall::Open => {
let path = unsafe { str::from_raw_parts(a1 as *const u8, a2 as usize) };
let fd = syscall::open(path, false);
unsafe { (*interrupt_state).a[0] = fd.unwrap() };
}
SysCall::Alloc => {
let layout = Layout::from_size_align(a1 as usize, a2 as usize).unwrap();
// Allocate memory and put the pointer in a0
unsafe { (*interrupt_state).a[0] = alloc::alloc::alloc(layout) as u64 };
unsafe { (*interrupt_state).a[0] = syscall::alloc(layout) as u64 };
}
SysCall::Dealloc => {
let ptr = a1 as *mut u8;
let layout = Layout::from_size_align(a2 as usize, a3 as usize).unwrap();
// Free memory
unsafe { alloc::alloc::dealloc(ptr, layout) };
unsafe { syscall::dealloc(ptr, layout) };
}
SysCall::Exit => exit_process(&mut interrupt_state),
SysCall::NanoSleep => sleep(Duration::new(a1, a2 as u32), &mut interrupt_state),

View File

@@ -15,6 +15,7 @@
ptr_metadata
)]
use alloc::boxed::Box;
use embedded_alloc::LlffHeap as Heap;
use log::info;
@@ -38,6 +39,7 @@ mod panic_handler;
mod process;
mod riscv;
mod scheduler;
mod syscall;
mod time;
mod uart;
mod user;
@@ -63,8 +65,8 @@ pub extern "C" fn supervisor_mode_entry() {
info!("Hello World !");
unsafe { Vga::draw_string(10, 10, "Hello World !", Color::WHITE, Color::BLACK) };
create_process(&test, "proc1");
create_process(&proc2, "proc2");
create_process(Box::new(test), "proc1");
create_process(Box::new(proc2), "proc2");
create_process_from_file("/usr/bin/test_pic");

View File

@@ -12,6 +12,7 @@ use core::time::Duration;
use alloc::{boxed::Box, format, string::String, vec::Vec};
use bffs::{io::Read, path::Path};
use goblin::elf::reloc::R_RISCV_RELATIVE;
use hashbrown::HashMap;
use shared::syscall::exit;
use crate::{
@@ -82,13 +83,15 @@ pub struct Process {
/// Current state of the process.
pub state: ProcessState,
/// Optional entry point for the process code.
pub entry: Option<&'static dyn Fn()>,
pub entry: Option<Box<dyn Fn()>>,
/// Wake time for sleeping processes.
pub wake_time: Duration,
/// Saved execution context.
pub ctx: ExecutionContext,
/// Process stack.
pub stack: [u64; STACK_SIZE],
/// File descriptor table.
pub fd_table: HashMap<u64, Path<'static>>
}
impl core::fmt::Debug for Process {
@@ -130,13 +133,19 @@ pub fn create_process_from_file<'a, T: Into<Path<'a>>>(path: T) -> i64 {
// Open and read the binary file
let mut bin = FILE_SYSTEM.open_file(path).unwrap();
println!("Creating process");
let mut content: Vec<u8> = Vec::new();
bin.read_to_end(&mut content).unwrap();
println!("Loading binary at address: {:x?}", content.as_ptr());
println!(
"Loading binary at address: {:x?}, length: {}",
content.as_ptr(),
content.len()
);
// If ELF, use goblin to load PT_LOAD segments and apply relocations
if let Ok(gelf) = goblin::elf::Elf::parse(&content) {
println!("Parsed");
// Determine memory bounds from program headers
let mut min_vaddr = u64::MAX;
let mut max_vaddr = 0u64;
@@ -179,6 +188,7 @@ pub fn create_process_from_file<'a, T: Into<Path<'a>>>(path: T) -> i64 {
}
}
}
println!("Copied");
// Apply relocations using our parser (handles RELA entries)
for rela in gelf.dynrelas.iter() {
@@ -193,15 +203,16 @@ pub fn create_process_from_file<'a, T: Into<Path<'a>>>(path: T) -> i64 {
_ => {}
}
}
println!("Relocated");
// Entry point
let entry_va = gelf.entry;
let entry_addr = unsafe { base.add((entry_va - min_vaddr) as usize) } as *const u8;
let entry_fn =
unsafe { core::mem::transmute::<*const u8, extern "C" fn()>(entry_addr) };
let wrapper = Box::leak(Box::new(move || {
let wrapper = Box::new(move || {
entry_fn();
}));
});
println!("Program loaded at : {:x?}", entry_addr);
return create_process(wrapper, name);
}
@@ -210,9 +221,9 @@ pub fn create_process_from_file<'a, T: Into<Path<'a>>>(path: T) -> i64 {
// Fallback: treat the file as a raw binary blob and execute in-place
let entry_point =
unsafe { core::mem::transmute::<*const u8, extern "C" fn()>(Vec::leak(content).as_ptr()) };
let wrapper = Box::leak(Box::new(move || {
let wrapper = Box::new(move || {
entry_point();
}));
});
create_process(wrapper, name)
}
@@ -236,7 +247,7 @@ pub fn create_process_from_file<'a, T: Into<Path<'a>>>(path: T) -> i64 {
///
/// The provided `code` function will be executed when the process is first
/// scheduled. Returns the new PID, or -1 if the process table is full.
pub fn create_process<T: Into<String>, F: Fn()>(code: &'static F, name: T) -> i64 {
pub fn create_process<T: Into<String>, F: Fn() + 'static>(code: Box<F>, name: T) -> i64 {
// Search for a free slot in the process table
let mut next_pid = 0;
while next_pid < PROCESS_COUNT && unsafe { PROCESS_TABLE[next_pid].state != ProcessState::Dead }
@@ -262,7 +273,7 @@ pub fn create_process<T: Into<String>, F: Fn()>(code: &'static F, name: T) -> i6
// Configure execution context
// a0 contains the pointer to the function to execute
process.ctx.a[0] = process.entry.as_ref().unwrap_unchecked() as *const &dyn Fn() as u64;
process.ctx.a[0] = &raw const *process.entry.as_ref().unwrap_unchecked() as u64;
// mepc points to process_launcher which will call the function
process.ctx.mepc = process_launcher as *const _;
@@ -295,7 +306,7 @@ pub fn create_process<T: Into<String>, F: Fn()>(code: &'static F, name: T) -> i6
/// This function is installed into the process `mepc` so that when the new
/// process is scheduled it will run this launcher which calls the user
/// function and ensures the process exits cleanly.
extern "C" fn process_launcher(code: *const &dyn Fn()) {
extern "C" fn process_launcher(code: *const Box<dyn Fn()>) {
// SAFETY: The code pointer was initialized in create_process
// and points to a valid function.
unsafe { (*code)() };

View File

@@ -105,7 +105,7 @@ macro_rules! mret {
}
#[unsafe(naked)]
pub extern "C" fn exit_qemu() {
pub extern "C" fn exit_qemu() -> ! {
naked_asm!(
"
li a0, 0x100000

View File

@@ -5,7 +5,8 @@
//! and a simple round-robin scheduler used by the kernel.
use core::{arch::riscv64::wfi, array, cell::LazyCell, time::Duration};
use alloc::string::String;
use alloc::{boxed::Box, string::String};
use hashbrown::HashMap;
use log::info;
use crate::{
@@ -43,6 +44,7 @@ pub static mut PROCESS_TABLE: LazyCell<[Process; PROCESS_COUNT]> = LazyCell::new
},
stack: [0; _],
entry: None,
fd_table: HashMap::new()
})
});
@@ -71,7 +73,7 @@ pub fn scheduler_init() {
}
}
create_process(&idle, "idle");
create_process(Box::new(idle), "idle");
unsafe {
PROCESS_TABLE[0].state = ProcessState::Active;
}

27
src/syscall.rs Normal file
View File

@@ -0,0 +1,27 @@
use core::alloc::Layout;
use bffs::{error::Error, path::Path};
use crate::fs::{Disk, FILE_SYSTEM};
pub unsafe fn alloc(layout: Layout) -> *mut u8 {
unsafe { alloc::alloc::alloc(layout) }
}
pub unsafe fn dealloc(ptr: *mut u8, layout: core::alloc::Layout) {
unsafe { alloc::alloc::dealloc(ptr, layout) }
}
pub fn open<'a, P: Into<Path<'a>>>(
path: P,
in_kernel: bool,
) -> Result<u64, Error<<Disk as bffs::io::IoBase>::Error>> {
let path = path.into();
let file = match path.split_path() {
("dev", path) => {
unimplemented!()
}
_ => FILE_SYSTEM.open_file(path)?,
};
Ok(42)
}

View File

@@ -6,8 +6,8 @@ fn main() {
let mut test = String::new();
test.push('A');
test.push('B');
for _ in 0..50 {
for _ in 0..100 {
test.push('C');
}
println!("Hello from PIC program loaded dynamically with custom std and a better justfile, and syscalls !");
println!("Hello from PIC program loaded dynamically with custom std and a better justfile, and syscalls ! {}", test);
}