This is a project intending to port xv6-riscv using Rust.
Most of major differences will be mentioned below,
while minor differences go to the document/comment in relevant source files.
-
can only compile on target triple
riscv64gc-unknown-none-elf
,
refer .cargo/config for detail -
cargo run
may write things below to the console in the end, which is expected.
panickeadni at ckpead niatck 'e'd art 'rrusust_t_mamainin: :enu esd t_main: end of hart 0', src/rmain.rs:34:5
nd of hart 2',o f src/rmain.rs:34:5
hart 1', src/rmain.rs:34:5
Reason: there are 3 harts, and panic
will not acquire the Pr
lock to write,
which could be helpful when debugging.
Run:
cargo run
Objdump:
cargo objdump --bin xv6-riscv-rust -- -d > kernel.asm
// recommended, some instructions is unknown when using rust-objdump/llvm-objdump
// in target/riscv64gc-unknown-none-elf/debug
riscv64-unknown-elf-objdump -S xv6-rsicv > kernel.asm
Unit Test:
cargo run --features "unit_test"
target spec:
rustc -Z unstable-options --print target-spec-json --target riscv64gc-unknown-none-elf
- while xv6-riscv wraps spinlock into a single object,
xv6-riscv-rust wrap a single object into a mutex, which implemented with spinlock. - while xv6-riscv use
initlock
to init lock at run time,
xv6-riscv-rust use const fnSpinLock::new
to init lock at compile time.
xv6-riscv-rust use AtomicBool to replace simple int symbol in xv6-riscv,
in cases when these symbols are only written once or are rarely used.
Reason: safer, don't lose much speed.
STARTED
in rmain.rs:
It is written once by hart0 to tell other harts that some initialization done.Pr.locking
in printf.rs:
It is written once by the thread that panic.
GCC's __sync_lock_test_and_set
generate amoswap
,
while Rust's compare_and_swap
/ LLVM's cmpxchg
generate lr
&sc
.
Helpful reference: Rust Atomic compare and swap 2018 editionのRISC-Vソース〜LLVMを添えて〜
xv6-riscv-rust use conditional compilation trick to implement some unit tests.
Test cases will go in submodule in several mods that is needed to be tested,
typically named pub mod tests
in each mod.
Example:
#[cfg(feature = "unit_test")]
pub mod tests {
use super::*;
/* test cases */
}
With the feature unit_test enabled,
here is the execution flow: after each hart enter and execute rust_main
,
they will call test_main_entry
, which call each tests submodule.
Usage: add cargo options --features "unit_test
global_asm!(include_str!("asm/entry.S"));
global_asm!(include_str!("asm/kernelvec.S"));
xv6-riscv doesn't add .section .text
in kernelvec.S(maybe GCC can infer that),
but it won't work for xv6-riscv-rust, so we have to explicitly add it.
You can try it by commenting .section .text
out and then use objdump to see whether timervec
exists.
.bss : {
*(.bss)
*(.sbss*)
- PROVIDE(end = .); // works for ld, not for lld
}
+ PROVIDE(end = .); // works for lld
consider the codes in kernel.ld above, see the comments.
when converting usize to VirtAddr & PhysAddr, use from or try_from?
seems like From and TryFrom can not be both implemented for <T, U>
so I choose to add a new type for trusted address, i.e.,
impl From<ConstAddr> for VirtAddr
& impl TryFrom<usize> for VirtAddr
(same for PhysAddr
)
Saving sepc
(user program counter) in trampoline.S instead of in user_trap.
Several places deserve to mention:
- Since we apply RAII-style lock in Rust, so unlike C, some function like
sched
act transparently between the initialization and dropping of the lock/guard. sleep
method inProc
struct receive a spinlock guard, instead of a spinlock,
so the caller of this method should reacquire the spinlock if still needed.
ALLOCATED
:
this state marks a runnable process is already allocated in a specific cpu,
in order to temporarily release the proc's lock when
CpuManager::scheduler
calls PROC_MANAGER.alloc_runnable
.
- porting console and uart to support printf, p.s., smp = 1
- add register abstraction to support start using mret to return to rust_main
- cpu abstraction and spinlock, add unit_test feature as temp solution
- us spin e lock to synchronize con print sole's ln, and refactor PRINT
- add kernel frame allocator(kalloc), fix writing bug in
timerinit
- use Unique in self-implemented Box to provide ownership, see this for example
- add Addr and PageTable
- add kvm for kernel, i.e., kernel paging
- cpu and proc basic abstraction(hard time playing around lock and borrow checker)
- add kernel trap handler(panic at
fork_ret
) - add user trap returner and way to user space
- add user code space(initcode) and ecall handing in
user_trap
- add virtio disk driver, plic, buffer cache, inode
- refactor
Proc
into several parts, one need lock to protect, the other is private - separate
Buf
into two parts, one guarded by bcache's lock, the guarded by its own sleeplock - update bio and virtio disk
- replace linked list allocator with buddy system, remove self-implemented Box
- refresh some methods and variable name
- complete sys_exec and add elf loader
- complete a runnable fs
- recycle pgt for uvm(no need to recycle pgt for kvm now)
- remove ConstAddr and PhysAddr?
- stack size need to be 8192 bytes?
- meta data portion of buddy system is too high
- may be too much UB
Why implementing Send trait for Mutex?
Explicitly drop
fixed-size linked list allocator
take ownership from nothing
Unique issue
out of memory
integrate Mutex and MutexGuard
lld linker script
Rust Memory layout
rustc codegen options
- timerinit
- copy
sie
's code tosip
, then clearingSSIP
becomes clearingSSIE
,
which do not allow supervisor software interrupt forever. - setting
stvec
the wrong address, supposed to be in virtual space,
but written asuservec
directly, which is in physical space. - be careful about
pc
when switching page table