-
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
What about: volatile accesses and memory-mapped IO #33
Comments
If an lvalue is accessed through only volatile loads, LLVM will not add accesses that aren't volatile. |
@ubsan Awesome! Is that documented somewhere? |
I've not seen this guarantee stated anywhere in LLVM docs. It seems reasonable on its own but the While one may be tempted to say "OK but don't do that if there are volatile accesses", that's not really possible. The transformation that wants to insert a load may not be able to see the volatile accesses (e.g., because they are in another function), or there may not even be any accesses at all to the location (e.g., if the source program creates a This is not an issue for Clang compiling C code handling pointers-to-volatile because C pointers are not annotated |
@cramertj shoot! I may absolutely be wrong. I am pretty sure I saw that before, but I may be imagining it; or I may have seen it in a talk. I'll see if I can find it -.- |
For reference, this is similar to the old, long, thread https://internals.rust-lang.org/t/volatile-and-sensitive-memory/3188/46. My conclusion from the previous thread is that Rust should add a new type modifier (maybe two, one for reads and one for writes) similar to |
@briansmith Thanks for the link! Yeah, that is similar. The bit I'm especially interested in is @nikomatsakis's conclusion here:
i've had several experts tell me this isn't true, and that speculative reads may be introduced even when no normal reads could possibly occur. In fact, there are several passes that almost definitely do today -- ASAN and structure splitting, for example. However, this would mean that the guarantee that you and I are asking for doesn't exist even in C/C++ today, and that all non-ASM MMIO code is busted in this way. If that's true, it might be necessary to introduce something new into LLVM to mark pointers with an "exactly once" property. |
C11 defines "access" as "execution-time action〉 to read or modify the value of an object" and says:
So, I think that C does provide quite a strong guarantee as long as you only refer to this memory using My understanding is that LLVM has a pass that erases the As far as Rust is concerned, there's no |
Are Rust pointers annotated [0] MMIO is always mutably aliased, so if you create a |
The |
I don't see where in here it makes any guarantees about not inserting extra reads or writes. It says they won't be optimized out, but not that new accesses won't be inserted. |
I think it's implied by "Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3." In particular, IIRC (I'm not going to re-read it now), section 5.1.2.3 at least implies that each expression is evaluated exactly once. |
@briansmith Perhaps? That wasn't my reading of it, but if you can find more support for that, I'd be glad to hear that was the case. That doesn't appear to be the behavior implemented by LLVM-- @hfinkel on the llvm-dev list said that "I don't believe that the LangRef provides that guarantee" and pointed out that |
They also said in the same email that LLVM probably should provide such a guarantee (citing that volatile accesses usually lower to the same instruction as normal ones, so adding non-volatile accesses is in many ways like adding another volatile access). FWIW, I agree. Let's try to get LLVM to guarantee that. Dereferenceable is still incompatible with MMIO, though, for reasons I outlined earlier and as also confirmed on llvm-dev. That indicates we either need to find alternatives to |
That's still not clear to me-- that point was made based on the idea that MMIO didn't count as allocated memory, which was contradicted by the second point. I'd imagine that the points that check and care about "defeferenceable" would be the same paths that would want to check for !volatile (namely, isSafeToLoadUnconditionally). |
OK, fair, Eli Friedman's argument is not the same as mine and doesn't really convince me either (I'm with Hal, MMIO memory is allocated, just not by LLVM). However, re: this:
Consider for example the function fn maybe_use_volatile(x: &VolatileCell<u8>, flag: bool) {
if flag {
use_volatile(x);
}
} This maps to LLVM IR which receives a To resolve this contradiction, one could weaken the meaning of |
Ack. FWIW, I think MMIO in C is just as broken without such a guarantee. And while they are at it, let's also extend the "number of memory accesses doesn't change" to "number and size doesn't change" -- @cramertj suggested in a meeting earlier that there are cases where this actually happens? Or did I misunderstand? Basically, I think the way to think about volatile accesses is as follows: A This also clearly shows, in my view, that Now, to the unfortunate side of things: However, in a recent discussion of internals it was discovered that the way we use This thread doesn't actually look so bleak to me, @cramertj -- why did you say "MMIO requires inline assembly"?^^ |
Losing |
Because, as you say,
There's no indication of a correct and spec-compliant way to get exactly-once access in either C, C++, or Rust, so it seems developers needing this behavior today are stuck without inline assembly. If the LLVM devs and the broader C community decide that they want "volatile" to mean exactly-once-access (which I think is the only reasonable behavior for it to have) then GCC, Clang, and the C and C++ references all need to be updated. I think this is what should happen, but I don't presume to know how this will go across in those communities. Everything below here is unrelated to the bug we're discussing
Nah, sorry for the confusion-- this is an issue with trying to do non-volatile reads/writes to uncached memory. Uncached memory shouldn't necessarily require volatile, but on ARM a fault will occur if you attempt an unaligned access. This example (it's easy to generate such examples) shows how a "normal" load can result in unaligned accesses. It'd be nice to have a non-volatile way to do a load that guaranteed no unaligned accesses. There's also another orthogonal Fuchsia-specific reason that we're probably going to wind up using inline ASM for MMIO, which is that Linux KVM doesn't implement load/store instructions to MMIO addresses when using writeback (e.g. |
+1. This makes sense to me. It might well be that, in the future, if we want to be more "fine grained" about this, we might want to add different "types" of @cramertj we should be able to interact with MMIO without having to use inline assembly. MMIO might not be super common, but I think it is common enough and involves enough pitfalls, that we should do our best to make it as simply as possible to use it correctly. |
Take the case where there are no calls to
I disagree. This doesn't match the mental model of the programmer who is trying to access the memory-mapped I/O region (a particular object) in the correct way. When I have a volatile object then I want to enforce all accesses to the object to have volatile semantics and to prevent any non-volatile accesses. That means in particular I don't want to allow creation of a non-volatile reference/pointer to the volatile object such that the non-volatile reference/pointer could be passed to some function that would then operate on the object with non-volatile accesses. I want the compiler to understand that that object is immovable (since moves are non-volatile accesses) and can't be
I think it's too early to say that until there's a spec for what Rust is trying to specify, and also until there's at least one implementation of the semantics that Rust specifies. In particular, if LLVM doesn't guarantee the exactly-once behavior then we'd have to see how much performance loss (if any) there would be if/when LLVM is changed to do that. Right now it's not even clear how we would verify that some change to LLVM is sufficient to guarantee those semantics with its current design. It seems to me it would be easier to verify that LLVM is doing the correct thing if it too internally switched to C-like semantics where |
Oh totally! I want that too. I'm just disappointed that the current compilers and specs don't seem to be set up to allow this. |
Certainly that is a better abstraction for MMIO, but adding a whole other storage category to the language is a huge step and can significantly complicate the type system (as
I am confident that will never happen. If anything, LLVM is moving even more from putting things on the pointer to instead putting them on memory accesses through those pointers. For example, pointee types are slated for removal (instead of If it's a property of the allocation, that also means you can't do a volatile access to an object not allocated as such, which is sometimes useful. Furthermore, I've seen nothing indicating that a storage classifier instead of a tag on accesses is necessary or even helpful for making and maintaining the exactly-once guarantee. The omission of such a guarantee could just as easily happen if the LangRef was instead describing loads and stores through pointers-to-volatile, and in terms of implementation there is just as much tension as sharing code (which then can lead to incorrectly applying non-volatile reasoning to code using volatile) between |
It is my interpretation that the behavior you want was always the intention of the spec, just so far they did not manage to write down what they actually meant. Also, there are people writing MMIO in C -- AFAIK the Linux kernel is full of it -- so in practice, at least for GCC, this seems to work. Or not? I don't know where to lobby to get the C/C++ spec improved, but we can start with LLVM and they seem to agree.
I think it is as justified for I am pretty sure one could reproduce the async fn fetch_sub_yield(x: &Cell<usize>) -> usize {
let old = x.get();
x.set(old-1);
await!(yield()); // because why not
// Now `x` might be dangling
old
}
// In the destructor
let old = await!(fetch_sub_yield(&self.inner().strong));
if old == 1 { return; } If I think that if we tell people that concurrent mutation is okay, they can reasonably assume that this includes deallocation.
I assume you meant to say "assume" or so? If it isn't for
That is a high-level programming concept: You want to enforce an invariant. Well, use the usual tools -- an abstraction sealing direct access to the location and enforcing that all accesses be volatile. You don't need any language support for that. It's like saying "I want to enforce all accesses to the object use atomic semantics", and we have In describing the effect, the operational behavior, of volatile, accesses are all there is. LLVM demonstrates that this is true, because it doesn't even have a notion of volatile objects, so clearly you don't need it.
I do not see any benefit at all from adding this extra notion. It makes things more complicated, what do we gain? Nothing. We still have to handle the case where pointers are casted between volatile and non-volatile, which is legal in C. If you come from volatile objects, this seems strange, but it really isn't -- and with volatile accesses, it's completely natural. The situation is very much like it is with atomic accesses: For the programmer, it is convenient to have a type that makes sure that all accesses to some memory location are atomic, just to make sure they don't screw up. But for the language, it is entirely unnecessary and just a burden to have a notion of an "atomic object" or so. None of the atomic memory models does that. The reasons why volatile seems more confusing is (a) you don't usually want "mixed-access" locations that are sometimes volatile and sometimes not (though the Linux kernel has lots of that too, but AFAIK that's using "GCC C" to implement atomic accesses), and (b) the C standard does a really bad job of describing what effect the volatile modified has on an access. I mean, the C standard starts out saying "this specifies an abstract machine, optimizations are not a concern here but a consequence of the behavior of the abstract machine", but then for volatile it goes on not doing that and instead says things about not duplicating memory accesses and the like. That's a bad spec. A proper spec, in "abstract machine" style, is something like what I wrote above: Volatile accesses are like syscalls, they are externally visible behavior with generally unknown side-effects. They are more restricted than syscalls in the effects they can have, but that's about it. Now, not duplicating volatile accesses is a consequence of the spec, the way it should be. Not inserting new accesses is, too. And the entirely unnecessary notion of a volatile object can be removed from the abstract machine. (It is still useful in the surface language! But that's not the topic of discussion here, I don't think.) |
Not only that they didn't write it down, but it wasn't implemented-- I've spoken with multiple people at Google who are telling me they wrote existing LLVM and GCC passes that not only don't uphold this guarantee, but can't uphold it, since there's no mechanism for preventing non-volatile loads from being inserted. In practice, there are certainly people using this, so it's definitely the case that these extra loads aren't being added often enough that people have noticed, figured out the root problem, reported it, etc. but that certainly doesn't mean it can't/won't happen, or that we can provide Rust users with a guarantee that it won't happen in their programs. |
One of my coworkers stumbled on a fun paper, "Volatiles Are Miscompiled, and What to Do about It". It's quite relevant. Some choice sections:
|
That's really surprising to me. If all of these passes treat volatile accesses as opaque operations they don't know anything about -- which is something they have to support as it can happen any time in actual code, by calling an extern function -- then they should uphold the guarantee. So I am puzzled how it is even possible that the fundamental design of an optimization makes it impossible to uphold this guarantee. They don't have to specifically avoid inserting non-volatile accesses to locations only used in a volatile way. They have to (a) ignore volatile accesses entirely, and (b) avoid inserting accesses to locations not used at all. The former seems trivial, and the latter is required for soundness even if you entirely ignore volatile. So, if you can get one of your coworkers to expand on that a bit, I'd be very interested what it is that I am missing here. (I don't have a deep understanding of how modern compilers work, so I probably am wrong, but I'd like to know how wrong.^^) |
With the current design of LLVM, it isn't valid to insert any non-volatile access unless/until you've proved that a non-volatile access would otherwise occur. But, proving that a non-volatile access would occur is hard and people write passes that insert accesses without such proofs. I imagine most of the broken passes are probably not doing anything with volatile at all; probably the authors didn't realize that they have to assume that all memory is volatile by default. |
See this reddit thread for a relevant discussion between @comex and me. I think we reached a conclusion that we can both live with, but I hope @comex can respond there or here to confirm. :) I had first proposed half-of-a-spec for Because the compiler has to preserve the sequence of observable events, it is not allowed to e.g. reorder to volatile memory accesses. The new things I now realized are:
Basically, since events can also feed data into the program, they can affect how the program goes on behaving after they received that input. The Rust language spec does not care; it does not require the kernel to do any particular thing when calling the Hm, somehow this is harder to explain than I thought, and I feel the above got rather confusing... :/ |
(I edited the prior post to expand on what I mean, but it got confusing, so now I am trying an example.) Consider the following Rust program: fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let n: i32 = input.parse().unwrap();
if n == 7 {
unsafe { *(0 as *mut i32) = 0; } // cause UB
} else {
println!("{}", n);
}
} Let us say the events of our language are given by enum Event { Read(String), Write(String), Panic } The behavior of writing to stdout is to cause a (To make everything work out formally, we also have to make a progress requirement: if a program can cause With this definition, the possible sequences ("traces") of events of our program depend on what got picked for the
Notice how The compiler can compile our program in any way as long as the final program's behaviors are included in the behaviors described above. For example, it could decide that the "then" branch of our conditional is dead code, and always just run
This is included in the above because Now consider the case where we are working in an environment where we know the input will never be We just argued about a program that does I/O, without saying anything about how I/O works! We can do exactly the same for volatile: we would add This entire approach is very closely related to communicating sequential processes, where we see our program as one process and other things like the kernel or MMIO devices as other processes, and because the compiler has to preserve the way that the program reacts to external events, we can basically imagine the Rust Abstract Machine to be "plugged together" with the kernel or the MMIO devices. The interface for these "connections" is given by the possible events, or messages being exchanged both ways. This material can fill entire books, so I am by far not doing it justice here. I hope this still helps to shed some light on the way I am thinking about volatile. At some point I should turn this into a blog post, or more likely a series of posts, but I have to finish a PhD thesis first... |
While bickering stopped, I'd like to ask if I can fix broken reference to memory mapped object by somehow forcing compiler to do all read/writes as volatile using asm and fence like: asm!("" ::: "memory" : "volatile")
core::sync::atomic::compiler_fence(Ordering::SeqCst); Will llvm be allowed to ignore it or will it see asm and use only volatile read/writes after this instruction? |
short answer: no, not with how you've described it you would need to create an abstraction around whatever you're using to only ever use volatile reads/writes, see something like voladdress for the current most sound solution to the problem (AKA never use references, only ever pointers) |
Closing in favor of #411. The original questions have been answered (using this pattern with raw pointers is fine, but with references it is not currently fine). |
Folks who want to write drivers and embedded code using Rust need to have a way to guarantee exactly-once access to certain memory locations. Today, the embedded wg makes extensive use of @japaric's
VolatileCell
crate, along with RegisterBlock structures containingVolatileCell
wrappers around each field of the register block, and a function to provide a single access to the register block at a fixed address. The API exposed in the thestdm32f103xx
crate and similar only expose*const RegisterBlock
values (example) from the overallPeripherals
object. This then requires unsafe code to access and mutate any particular field.Asks:
unsafe { (*x.volatile_cell_field).set(...) }
, and that the number of reads will exactly match the number of calls tounsafe { (*x.volatile_cell_field).get(...) }
? it seems like it should be.&
? It would be possible to provide a customRegisterRef<'a, T>
that consisted of a raw pointer internally as well as a custom derive for projecting this to fields of the register block, but this seems unfortunately complicated and unergonomic.Complicating factors:
VolatileCell
is special, similar toUnsafeCell
, and cannot have "dereferenceable" applied to references to it (and objects that contain it), in order to prevent this misoptimization? This seems potentially more complicated and intrusive, but IMO still worth considering.cc @RalfJung @kulakowski @teisenbe @rkruppe
The text was updated successfully, but these errors were encountered: