问题
I'm compiling a C++ program to run in a freestanding environment and the CPU I'm running on defines a 32-bit peripheral register to be available (edit: memory-mapped) at PERIPH_ADDRESS (aligned correctly, and not overlapping with any other C++ object, stack etc.).
I compile the following code with PERIPH_ADDRESS predefined, later link it with a full program and run it.
#include <cstdint>
struct Peripheral {
const volatile uint32_t REG;
};
static Peripheral* const p = reinterpret_cast<Peripheral*>(PERIPH_ADDRESS);
uint32_t get_value_1() {
return p->REG;
}
static Peripheral& q = *reinterpret_cast<Peripheral*>(PERIPH_ADDRESS);
uint32_t get_value_2() {
return q.REG;
}
extern Peripheral r;
// the address of r is set in the linking step to PERIPH_ADDRESS
uint32_t get_value_3() {
return r.REG;
}
Does any of the get_value functions (either directly or through p/q) have undefined behavior? If yes, can I fix it?
I think an equivalent question would be: Can any conforming compiler refuse to compile the expected program for me? For example, one with UB sanitezer turned on.
I have looked at [basic.stc.dynamic.safety] and [basic.compound#def:object_pointer_type] but that seems to only restrict the validity of pointers to dynamic objects. I don't think it applies to this code, because the "object" at PERIPH_ADDRESS is never assumed to be dynamic. I think I can safely say that the storage denoted by p never reaches the end of its storage duration, it can be considered static.
I've also looked at Why does C++ disallow the creation of valid pointers from a valid address and type? and the answers given to that question. They also only refer to dynamic objects' addresses and their validity, so they do not answer my question.
Other questions I've considered but couldn't answer myself that might help with the main question:
- Do I run into any UB issues because the object was never constructed within the C++ abstract machine?
- Or can I actually consider the object to be one with static storage duration "constructed" properly?
Obviously, I'd prefer answers that reference any recent C++ standard.
回答1:
It is implementation-defined what a cast from a pointer means [expr.reinterpret.cast]
A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value; mappings between pointers and integers are otherwise implementation-defined.
Therefore this is well-defined. If your implementation promises you the result of the cast is valid, you are fine.†
The linked question is in regards to pointer arithmetic, which is unrelated to the problem at hand.
† By definition, a valid pointer points to an object, implying subsequent indirections are also well-defined. Care should be exercised in making sure the object is within its lifetime.
回答2:
Does any of the get_value functions (either directly or through p/q) have undefined behavior?
Yes. All of them. They are all accessing the value of an object (of type Peripheral) that as far as the C++ object model is concerned does not exist. This is defined in [basic.lval/11], AKA: the strict aliasing rule:
If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined:
It's not the "cast" that's the problem; it's the use of the results of that cast. If there is an object there of the specified type, then the behavior is well-defined. If there isn't, then it is undefined.
And since there is no Peripheral there, it is UB.
Now, if your execution environment promises that there is an object of type Peripheral at that address, then this is well-defined behavior. Otherwise, no.
If yes, can I fix it?
No. Just rely on the UB.
You're working in a restricted environment, using a free-standing implementation, probably meant for a specific architecture. I wouldn't sweat it.
回答3:
This is summarizing the very helpful answers posted originally by @curiousguy @Passer By, @Pete Backer and others. This is mostly based on the standard text (hence the language-lawyer tag) with references provided by other answers. I made this a community wiki because none of the answers were completely satisfying but many had good points. Feel free to edit.
The code is implementation-defined in the best case, but it could have undefined behavior.
The implementation-defined parts:
reinterpret_castfrom integer type to pointer type is implementation-defined. [expr.reinterpret.cast/5]A value of integral type or enumeration type can be explicitly converted to a pointer. A pointer converted to an integer of sufficient size (if any such exists on the implementation) and back to the same pointer type will have its original value; mappings between pointers and integers are otherwise implementation-defined. [ Note: Except as described in [basic.stc.dynamic.safety], the result of such a conversion will not be a safely-derived pointer value. — end note ]
Access to volatile objects is implementation-defined. [dcl.type.cv/5]
The semantics of an access through a volatile glvalue are implementation-defined. If an attempt is made to access an object defined with a volatile-qualified type through the use of a non-volatile glvalue, the behavior is undefined.
The parts where UB has to be avoided:
The pointers must point to a valid object in the C++ abstract machine, otherwise the program has UB.
As far as I can tell, if the implementation of the abstract machine is a program produced by a sane, conformant compiler and linker running in an environment that has the register memory-mapped as described, then the implementation can be said to have a C++
uint32_tobject at that location, and there is no UB with any of the functions. This seems to be allowed by [intro.compliance/8]:A conforming implementation may have extensions (including additional library functions), provided they do not alter the behavior of any well-formed program. [...]
This still requires liberal interpretation of [intro.object/1], because the object is not created in any of the listed ways:
An object is created by a definition ([basic.def]), by a new-expression, when implicitly changing the active member of a union ([class.union]), or when a temporary object is created ([conv.rval], [class.temporary]).
If the implementation of the abstract machine has a compiler with a sanitizer (
-fsanitize=undefined,-fsanitize=address), then one might have to add extra information to the compiler to convince it that there is a valid object at that location.Of course the ABI has to be correct, but that was implied in the question (correct alignment and memory-mapping).
It is implementation-defined whether an implementation has strict or relaxed pointer safety [basic.stc.dynamic.safety/4]. With strict pointer safety, objects with dynamic storage duration can only be accessed through a safely-derived pointer [basic.stc.dynamic.safety]. The
pand&qvalues are not that, but the objects they refer to do not have dynamic storage duration, so this clause does not apply.An implementation may have relaxed pointer safety, in which case the validity of a pointer value does not depend on whether it is a safely-derived pointer value. Alternatively, an implementation may have strict pointer safety, in which case a pointer value referring to an object with dynamic storage duration that is not a safely-derived pointer value is an invalid pointer value [...]. [ Note: The effect of using an invalid pointer value (including passing it to a deallocation function) is undefined, see [basic.stc].
The practical conclusion seems to be that implementation-defined support is needed to avoid UB. For sane compilers, the resulting program is UB-free or it might have UB that can be very well relied on (depending on how you look at it). Sanitizers however can justifiably complain about the code unless they are explicitly told that the correct object exists in the expected location. The derivation of the pointer should not be a practical problem.
回答4:
As a practical matter, of the constructs you suggested, this one
struct Peripheral {
volatile uint32_t REG; // NB: "const volatile" should be avoided
};
extern Peripheral r;
// the address of r is set in the linking step to PERIPH_ADDRESS
uint32_t get_value_3() {
return r.REG;
}
is the most likely not to run foul of "surprising" optimizer behavior, and I would argue that its behavior is implementation-defined at worst.
Because r is, in the context of get_value_3, an object with external linkage that is not defined in this translation unit, the compiler has to assume that that object does exist and has already been properly constructed when generating code for get_value_3. Peripheral is a POD object, so there's no need to worry about static constructor ordering. The feature of defining an object to live at a particular address at link time is the epitome of implementation-defined behavior: it's an officially documented feature of the C++ implementation for the hardware you are working with, but it's not covered by the C++ standard.
Caveat 1: absolutely do not attempt this with a non-POD object; in particular, if Peripheral had a nontrivial constructor or destructor, that would probably cause inappropriate writes to this address at startup.
Caveat 2: Objects that are properly declared as both const and volatile are extremely rare, and therefore compilers tend to have bugs in their handling of such objects. I recommend using only volatile for this hardware register.
Caveat 3: As supercat points out in the comments, there can be only one C++ object in a particular memory region at any one time. For instance, if there are multiple sets of registers multiplexed onto a block of addresses, you need to express that with a single C++ object somehow (perhaps a union would serve), not with several objects assigned the same base address.
回答5:
I don't know if you're looking for a language-lawyer answer here, or a practical answer. I'll give you a practical answer.
The language definition doesn't tell you what that code does. You've gotten an answer that says that the behavior is implementation-defined. I'm not convinced one way or the other, but it doesn't matter. Assume that the behavior is undefined. That doesn't mean that bad things will happen. It means only that the C++ language definition doesn't tell you what that code does. If the compiler you're using documents what it does, that's fine. And if the compiler doesn't document it, but everyone knows what it does, that's fine, too. The code you've shown is a reasonable way of accessing memory-mapped registers in embedded systems; if it didn't work, lots of people would be upset.
回答6:
Neither the C nor the C++ standard formally cover even the act of linking object files compiled by different compilers. The C++ standard doesn't provide any guarantee that you can interface with modules compiled with any C compiler, or even what it means to interface with such modules; the C++ programming language doesn't even defer to the C standard for any core language feature; there is no C++ class formally guaranteed to be compatible with a C struct. (The C++ programming language doesn't even formally recognize that there is a C programming language with some fundamental types with the same spelling as in C++.)
All interfacing between compilers is by definition done by an ABI: Application Binary Interface.
Using objects created outside the implementation must be done following the ABI; that includes system calls that create the representation of objects in memory (like mmap) and volatile objects.
回答7:
Code like the above effectively seeks to use C as a form of "high-level assembler". While some people insist that C is not a high-level assembler, the authors of the C Standard had this to say in their published Rationale document:
Although it strove to give programmers the opportunity to write truly portable programs, the C89 Committee did not want to force programmers into writing portably, to preclude the use of C as a “high-level assembler”: the ability to write machine-specific code is one of the strengths of C. It is this principle which largely motivates drawing the distinction between strictly conforming program and conforming program (§4).
The C and C++ Standards deliberately avoid requiring that all implementations be usable as high-level assemblers, and makes no attempt to define all the behaviors necessary to make them suitable for such purposes. Consequently, behavior of constructs like yours which effectively treat the compiler as a high-level assembler is not defined by the Standard. The authors of the Standard explicitly recognize the value of some programs' ability to use the language as a high-level assembler, however, and thus clearly intend that code like yours be usable on implementations that are designed to support such constructs--the failure to define the behavior in no way implies a view that such code is "broken".
Even before the Standard was written, implementations intended for low-level programming on platforms where it would make sense to process conversions between pointers and like-sized integers as simply reinterpreting the bits thereof, would essentially unanimously process such conversions that way. Such processing greatly facilitates low-level programming on such platforms, but the authors of the Standard saw no reason to mandate it. On platforms where such behavior wouldn't make sense, such a mandate would be harmful, and on those where it would make sense, compiler writers would behave appropriately with or without it, making it unnecessary.
Unfortunately, the authors of the Standard were a little bit too presumptuous. The published Rationale states a desire to uphold the Spirit of C, whose principles include "Don't prevent the programmer from doing what needs to be done". This would suggest if on a platform with naturally-strong memory ordering it might be necessary to have a region of storage that be "owned" by different execution contexts at different times, a quality implementation intended for low-level programming on such a platform, given something like:
extern volatile uint8_t buffer_owner;
extern volatile uint8_t * volatile buffer_address;
buffer_address = buffer;
buffer_owner = BUFF_OWNER_INTERRUPT;
... buffer might be asynchronously written at any time here
while(buffer_owner != BUFF_OWNER_MAINLINE)
{ // Wait until interrupt handler is done with the buffer and...
} // won't be accessing it anymore.
result = buffer[0];
should read a value from buffer[0] after the code has read object_owner and received the value BUFF_OWNER_MAINLINE. Unfortunately, some implementations think it would be better to try to use some earlier-observed value of buffer[0] than treat the volatile accesses as possibly releasing and re-acquiring ownership of the storage in question.
In general, compilers will process such constructs reliably with optimizations disabled (and would in fact do so with or without volatile), but cannot handle such code efficiently without the use of compiler-specific directives (which would also render volatile unnecessary). I would think the Spirit of C should make it clear that quality compilers intended for low-level programming should avoid optimizations that would weaken volatile semantics in ways that would prevent low-level programmers from doing the things that may be needed on the target platform, but apparently it's not clear enough.
来源:https://stackoverflow.com/questions/53213699/is-accessing-registers-through-predefined-static-addresses-undefined-behaviour-i