Do not use volatile
in production, i.e. non-test, silicon creator code unless you are implementing a library explicitly for this purpose like sec_mmio
, abs_mmio
, or hardened
.
Specifically, do not use volatile
for
- Accessing memory-mapped registers,
- Instead, use
sec_mmio
orabs_mmio
.
- Instead, use
- Hardening purposes.
- Instead, use the primitives in
hardened.h
.
- Instead, use the primitives in
- Variables initialized outside the lifetime of the program, e.g. a pointer to some data in flash or retention SRAM, or
- Instead, declare as non-
const
and non-volatile
. Use a getter to enforceconst
-ness if needed.
- Instead, declare as non-
- Pointers created out of thin air.
- Compiler cannot optimize the first access (assuming that the pointer doesn't point to something already known by the compiler).
Do use volatile
if a variable is shared between an interrupt/exception handler and the rest of the on-target test program to ensure correctness.
- Since silicon creator code does not use handlers in the conventional sense, this essentially limits
volatile
usage to on-target tests.
When in doubt, please do not hesitate to reach out by creating a GitHub issue (preferably with the "Type:Question" label).
The goal of this document is to provide guidance for using volatile
in OpenTitan Silicon Creator code, i.e. rom
, rom_ext
, related tests, and examples.
There are several reasons for this guidance:
volatile
is contagious and, similar toconst
-correctness,volatile
-correctness can be hard to achieve and maintain. Once a variable is declared asvolatile
, thevolatile
keyword must appear throughout the call stack in all other variables that will reference the same object.volatile
pointers cannot be passed as arguments to standard functions such asmemcpy
,memset
, etc. Castingvolatile
-ness away in such cases results in undefined behavior. This is an easy-to-make but hard-to-catch mistake.- Semantic meaning of
volatile
is not very clear and it can have significant effects on the size and efficiency of the generated code. Often what we want isvolatile
access or forced loads/stores as opposed to the type of the variable beingvolatile
.abs_mmio
andsec_mmio
libraries, for example, implementvolatile
access for memory mapped registers.
Review the links provided in the References section for more information and some criticism on volatile
.
Consider the following toy example:
static bool my_var = false;
void my_function(void) {
my_var = true;
// Code that doesn't touch `my_var`
...
if (my_var) { // <- `my_var` must be `true`.
...
} else {
...
}
}
Since the value of my_var
is guaranteed to be true
, an optimizing compiler can optimize away the comparison and the else
branch.
Now, assume we add an ISR that resets my_var
:
static bool my_var = false;
// `my_isr` can modify `my_var` at any time but the compiler doesn't know that.
void my_isr(void) {
my_var = false;
}
void my_function(void) {
my_var = true;
// Code that doesn't touch `my_var` and doesn't call `my_isr()`.
...
if (my_var) { // <- The compiler would think that `my_var` must be `true`.
...
} else {
...
}
}
Using the same kind of analysis, an optimizing compiler would reach the same conclusion as in the previous example, i.e. optimize away the comparison along the else branch, and break our program.
This is where volatile
comes into play:
// `volatile`: `my_var` may be modified in ways unknown to the implementation.
static volatile bool my_var = false;
// Indeed, `my_isr` can modify `my_var` at any time.
void my_isr(void) {
my_var = false;
}
void my_function(void) {
my_var = true;
// Code that doesn't touch `my_var` and doesn't call `my_isr()`.
...
if (my_var) { // <- Cannot remove this check since `my_var` may be modified
... // in ways unknown to the implementation.
} else {
...
}
}
In this case, an optimizing compiler cannot optimize away the comparison since the value of my_var
may have changed since it has been set to true
.
Rule 1: No volatile
in Production Silicon Creator C Code1
Cases like the example above, however, do not typically appear in production, i.e. non-test, silicon creator C code since we don't enable interrupts and our handlers shut down the chip instead of returning.
Thus, volatile
should not be used in production silicon creator C code.
The following subsections cover the four most relevant cases where volatile
might be considered and the rationale behind this guidance.
abs_mmio
and sec_mmio
libraries already encapsulate volatile
access semantics.
When accessing memory mapped registers, any new code must use abs_mmio
or sec_mmio
libraries instead of declaring volatile
pointers or performing volatile
accesses by casting at the point of dereferencing.
Do not use volatile
for hardening purposes.
OpenTitan has invested considerably to define simple and predictable building blocks and to leverage them to harden various patterns in rom
and rom_ext
.
See hardened.h
for the hardening primitives we have.
There are several cases in OpenTitan silicon creator code where a variable with static storage duration is initialized either partially or completely in a way that isn’t visible to the C source code overriding the static initializer of the C object.
A const
example to this case is the definition of kManifest
in sw/device/silicon_creator/lib/manifest_def.c
.
kManifest
is an aggregate object of type manifest_t
that resides in flash memory whose actual value at runtime is different from the initializer in source code because the binary is modified by the build system before it is loaded into flash memory.
Non-const
examples include the struct
s in the static_critical
section of the main SRAM.
Such variables must be declared as
- non-
const
, and- Rationale: Even if the object resides in read-only memory, it must not be declared as a
const
-qualified type. Ifconst
is used, the compiler is not required to allocate space for the object and may replace reads with the value specified in the initializer. If needed,const
related compiler diagnostics must be enabled by providing a getter that returns aconst
pointer to the actual object instead of exposing the non-const
declaration.
- Rationale: Even if the object resides in read-only memory, it must not be declared as a
- non-
volatile
.- Rationale: The
volatile
qualifier prevents various optimizations and the use ofmemcpy
,memset
, and many other standard functions. The specification states that "All objects with static storage duration shall be initialized (set to their initial values) before program startup. The manner and timing of such initialization are otherwise unspecified." (C11, section 5.1.2, paragraph 1). By not usingvolatile
we’re no longer informing the compiler that the value of the object "may be modified in ways unknown to the implementation" (C11, section 6.7.3, paragraph 7). In principle, this could be problematic in situations where the implementation is allowed to assume that the object contains its initial value, e.g. when the object isconst
or when performing LTO and the compiler assumes that it has visibility into the whole program (using the-fwhole-program
flag in gcc)2. Since neither of these are true in this case, it seems safe to conclude that the compiler has to be conservative and cannot assume that the object has not been modified since the program startup.
- Rationale: The
OpenTitan secure boot consists of at least three stages: rom
, rom_ext
, and the first owner boot stage.
All boot stages except rom
are stored in flash and all in-flash boot stages are required to start with a "manifest" so that their integrity and authenticity can be verified.
Since OpenTitan uses a fixed flash layout, rom
and rom_ext
exactly know the next stage's manifest's location in flash.
Since this address is not part of their images, rom
and rom_ext
create pointers essentially out of thin air using functions like _const manifest_t *boot_policy_manifest_a_get(void)_
.
If the pointers in question do not point to something already known by the compiler, an optimizing compiler cannot optimize away the first access.
Thus, there is no need to declare such pointers as volatile
.
rom
and rom_ext
do not enable interrupts and their handlers shut down the chip instead of returning.
Tests, however, can define their custom interrupt/exception handlers for their own purposes.
In cases where a variable is shared between a handler and the rest of the program, it must be declared as volatile
to ensure correctness.
- Final draft of C11
- Deprecating volatile - JF Bastien - CppCon 2019: A longer discussion on
volatile
and c++ with a good set of demotivating examples in the first 20+ minutes - LLVM Language Reference Manual — LLVM 16.0.0git documentation
- PRs that implement the guidance in this doc: #15253, #15286, #15287, #15302, #15303.
Footnotes
-
This rule does not apply to libraries written specifically to leverage or encapsulate
volatile
semantics such asabs_mmio
,sec_mmio
, orhardened
. ↩ -
We can prevent such optimizations by adding
asm volatile("" : : : "memory")
at the entry point when using LTO but that seems unnecessary in practice.-fwhole-program
is not supported by Clang and the more fine-grained flags that Clang has for similar purposes don't seem to interact with this issue. ↩