Unexpected memory behavior when write to a raw pointer

While learning to develop a RISC-V OS in rust (target: riscv64gc-unknown-none-elf ), I'm encountering inconsistent memory behavior when using core::ptr::write or direct pointer assignment (*ptr = ). The TrapContext structure appears valid in GDB's variable inspection but shows unexpected zero values in raw memory dumps.
Here is my code:

const KERNEL_STACK_SIZE: usize = 4096*2;

#[repr(align(4096))]
struct KernelStack{
    data: [u8;KERNEL_STACK_SIZE],
}
impl KernelStack{
    fn get_sp(&self) -> usize{
        self.data.as_ptr() as usize + KERNEL_STACK_SIZE
    }
    fn push_context(&self, context: TrapContext) -> &'static mut TrapContext{
        let ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
        unsafe{ core::ptr::write(ptr, context) };
        unsafe{ ptr.as_mut().unwrap() }
    }
}
...
#[derive(Debug)]
#[repr(C)]
pub struct TrapContext{
    pub x: [usize;32],
    pub sstatus: Sstatus,
    pub sepc: usize
}

And is the output of gdb after executed unsafe{ core::ptr::write(ptr, context) };:

>>> print context
$2 = os::trap::context::TrapContext {
  x: [[0] = 0, [1] = 0, [2] = 2149654528, [3] = 0 <repeats 29 times>],
  sstatus: riscv::register::sstatus::Sstatus {
    bits: 8589934592
  },
  sepc: 2151677952
}
>>> print ptr
$2 = (*mut os::trap::context::TrapContext) 0x8020eef0 <os::batch::KERNEL_STACK+7920>
>>> x/32xw 0x8020eef0
0x8020eef0 <_ZN2os5batch12KERNEL_STACK17h80e19175e61eb8f2E+7920>:       0x00000000      0x00000000      0x00000000      0x00000000
0x8020ef00 <_ZN2os5batch12KERNEL_STACK17h80e19175e61eb8f2E+7936>:       0x80211000      0x00000000      0x00000000      0x00000000
(all 0 for the rest)

By the way, I'm using emulator QEMU 9.1.3.
Is this caused by memory align or something else?

I do not know if this is the cause of your problems but I think your code has UB because you modify a pointer derived from the shared reference &self . To my knowledge using a usize round-trip does not change that.

2 Likes

Currently, the os works on single thread, and sync components haven't been implemented. Since the KernelStack is a static variable, I cannot directly use &mut self as the signature of function push_context. I'm not sure what the difference between &mut self and &self is here, aren't they converted to raw pointers with the same value? Could you expand on that?

By accepting &self, you are telling the compiler that all the bytes of *self will not be modified. As the Rustonomicon put it:

More precisely, from the Rust Reference:

That’s what you’re doing here, by taking a &KernelStack and returning a &mut TrapContext to the same memory. You must explicitly mark your data as interior mutable by using UnsafeCell:

use core::cell::UnsafeCell;

struct KernelStack {
    data: UnsafeCell<[u8; KERNEL_STACK_SIZE]>,
}

Then use UnsafeCell::get() to get a pointer to the data. This is the only way to allow mutation. All interior mutability in Rust is built on either UnsafeCell, or using an existing raw pointer to other memory.

1 Like

Sure, but that's not the important thing.

Both are binding promises.

  1. Promise of &mut self: I'm the sole owner of that data, I can change it in a any way I want to, no one else may obsetver it.
  2. Promise of &self: the data behind that pointer is ā€œfrozenā€ and couldn't be changed by anything.

There are no way to ā€œopt outā€ of these promises ā€œlocallyā€. They are global properties that program as whole have to support.

But there are ā€œglobalā€ way to ā€œout-outā€ from #2 promise: UnsafeCell. It gives you the ability to violate that promise wrt the data structure that embeds UnsafeCell into it.

But abuse of UnsafeCell can also be used to violate rules #1 and #2 for other types… and that's what you have to, now, guarantee manually (since compiler could no longer do that for you).

Thanks for all of your advice!
Finally I've found that I was miscalculating the spec offset within the TrapContext structure in my assembly code. Additionally, I spotted a typo in the command – I'd accidentally used x/34xw instead of the correct x/34xg.

By the way, the aliasing and immutability properties of references (& and &mut) apply the same in single-threaded environments, e.g. holding a &mut _ active across a function call that locally creates an aliasing & or &mut (e.g. from a static mut) is UB even on a single thread.

That is to say, single-threaded is usually a red herring. You still have to follow the rules.