-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BEAM] Runtime initialization of static variables #112
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
# Runtime initialization of static variables | ||
|
||
Sometimes one needs to initialize a `static mut` variable, that will be | ||
accessed by an interrupt handler, at *runtime*. For this scenario we have a | ||
pattern where the variable starts uninitialized in const context (e.g. `static | ||
X: _ = MaybeUninit::uninit()`) and then it's initialized *before* the first | ||
invocation of the interrupt handler. We use a compiler fence or the "memory" | ||
clobber to enforce strict ordering between the initialization of the static | ||
variable and the first invocation of the interrupt handler, but is that enough? | ||
|
||
Note that: | ||
|
||
- The programs in this document target the [Basic Embedded Abstract Machine | ||
(BEAM)][beam]. Please become familiar with the linked specification before you | ||
read the rest of this document. | ||
|
||
[beam]: https://github.com/rust-lang/unsafe-code-guidelines/pull/111 | ||
|
||
- In these programs we assume that [rust-lang/rfcs#2585][rfc2585] has been | ||
accepted and implemented. | ||
|
||
[rfc2585]: https://github.com/rust-lang/rfcs/pull/2585 | ||
|
||
## Memory clobber synchronization | ||
|
||
Consider this program | ||
|
||
``` rust | ||
#![no_std] | ||
|
||
use core::mem::MaybeUninit; | ||
|
||
static mut X: MaybeUninit<bool> = MaybeUninit::uninit(); | ||
|
||
#[no_mangle] | ||
unsafe fn main() -> ! { | ||
X.write(false); | ||
|
||
unsafe { | ||
asm!("ENABLE_INTERRUPTS" : : : "memory" : "volatile"); | ||
// ^^^^^^^^ | ||
} | ||
|
||
// `INTERRUPT0` can preempt `main` from this point on and at any time | ||
|
||
loop { | ||
// .. any safe code .. | ||
} | ||
} | ||
|
||
#[no_mangle] | ||
unsafe fn INTERRUPT0() { | ||
let x: &mut bool = unsafe { &mut *X.as_mut_ptr() }; | ||
|
||
// .. any safe code .. | ||
} | ||
``` | ||
|
||
Note that "any safe code" can *not* call `main` or `INTERRUPT0` (because they | ||
are `unsafe` functions), use `asm!` or access registers. | ||
|
||
**Claim**: the memory clobber is sufficient to prevent misoptimizations. | ||
|
||
"Why is the memory clobber required?" If the compiler reorders `X.write` to | ||
after the `asm!` block `INTERRUPT0` could observe `X` in an uninitialized state. | ||
|
||
## Compiler fence synchronization | ||
|
||
Consider this program which is a slight variation of the first one: | ||
|
||
``` rust | ||
#![no_std] | ||
|
||
use core::{ | ||
cell::UnsafeCell, | ||
mem::MaybeUninit, | ||
ptr, | ||
sync::atomic::{self, Ordering}, | ||
}; | ||
|
||
extern "C" { | ||
static MASK_INTERRUPT: UnsafeCell<u8>; | ||
gnzlbg marked this conversation as resolved.
Show resolved
Hide resolved
|
||
static UNMASK_INTERRUPT: UnsafeCell<u8>; | ||
} | ||
|
||
const ORDERING: Ordering = ..; | ||
|
||
static mut X: MaybeUninit<bool> = MaybeUninit::uninit(); | ||
|
||
#[no_mangle] | ||
unsafe fn main() -> ! { | ||
unsafe { | ||
// mask INTERRUPT0 | ||
ptr::write_volatile(MASK_INTERRUPT.get(), 1 << 0); | ||
asm!("ENABLE_INTERRUPTS" : : : : "volatile"); | ||
} | ||
|
||
X.write(false); | ||
|
||
atomic::compiler_fence(ORDERING); | ||
|
||
unsafe { | ||
// unmask INTERRUPT0 | ||
ptr::write_volatile(UNMASK_INTERRUPT.get(), 1 << 0); | ||
} | ||
|
||
// `INTERRUPT0` can preempt `main` from this point on and at any time | ||
|
||
loop { | ||
// .. any safe code .. | ||
} | ||
} | ||
|
||
#[no_mangle] | ||
unsafe fn INTERRUPT0() { | ||
let x: &mut bool = unsafe { &mut *X.as_mut_ptr() }; | ||
|
||
// .. any safe code .. | ||
} | ||
``` | ||
|
||
**Claim**: the compiler fence is sufficient to prevent misoptimization provided | ||
that `ORDERING` is any of: `Ordering::SeqCst` `Ordering::AcqRel` or | ||
`Ordering::Release`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the interrupt be executed before From the RFC (rust-lang/rfcs#888) these were intended to be equivalent to C11's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
No, the handler won't run while the interrupt is masked or while interrupts are globally disabled. The volatile writes ensure that INTERRUPT0 is masked before interrupts are globally enabled. |
||
|
||
"Why is the compiler fence required?" if the compiler reorders `X.write` to | ||
after the second `write_volatile` statement `INTERRUPT0` could observe `X` in an | ||
uninitialized state. | ||
|
||
## Questions | ||
|
||
- Can these programs be misoptimized by the compiler? For example, if the | ||
compiler optimizes away the `X.write(..)` statement `INTERRUPT0` would observe | ||
an `X` in an uninitialized state. Is it allowed to do that? | ||
|
||
- If yes, what additional constraints are required to prevent misoptimization? | ||
Should `X` be initialized using a volatile write? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn't need to be. Just for the record:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I am not sure about this. Volatile accesses are ordered with respect to each other, but here you assume they are also ordered wrt. whatever will make it so that "interrupts can only happen after this point".
That's just a "should". It basically just means "that's likely incorrect and you should re-think what you are doing". |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be slightly better off rewriting this example to use a
core::mem::black_box(X.as_mut_ptr())
that has some guaranteed semantics, but AFAICT that would be an intrinsic that's anop
in the virtual machine, but somehow is guaranteed to disable some optimizations. Which optimizations exactly is a question that would need answering.Otherwise, it feels that modeling this would require a Rust memory model that understands inline assembly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not been actively following that RFC but I recall reading that using
black_box
for memory safety was discouraged ("it should only be used for benchmarks", is what I recall). Is that no longer the case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no RFC for a
core::mem::black_box
operation with guaranteed semantics. Somebody would need to write one. There is an RFC for acore::hint::black_box
intrinsics open, but that is not what I was suggesting to use here.Since inline assembly is not a part of the Rust language (as seen in the other threads, it's not even clear what
volatile
/memory
as clobbers do / should do in Rust), I don't think discussing anything related to inline assembly is a good use of this working group's time at this point.