Persistent Storage and PIC¶
BOLOS applications have access to two different types of memory in the Secure
Element: a small amount of RAM for the call stack and certain global variables,
and a considerably larger amount of flash memory for persistent storage. Access
to flash memory is regulated by the Memory Protection Unit which is configured
by BOLOS to prevent applications from tampering with parts of flash memory that
they shouldn’t. However, applications are able to access the part of flash
memory where their constant data and code is defined. This data includes code
and const
variables, but applications may also allocate extra space in NVRAM
to be used at runtime for persistent storage.
Types of Memory¶
All global variables that are declared as const
are stored in read-only
flash memory, right next to code. All normal global variables that are declared
as non-const
are stored in RAM. However, thanks to the link script
(script.ld
) in the SDK, global variables that are declared as non-const
and are given the prefix N_
are placed in a special write-permitted location
of NVRAM. This data can be read in the same way that regular global variables
are read. However, writing to NVRAM variables must be done using the
nvm_write(...)
function defined by the SDK, which performs a syscall. When
loading the app, NVRAM variables are initialized with data specified in the
app’s hex file (this is usually just zero bytes).
Warning
Initializers of global non-const
variables (including NVRAM variables)
are ignored. As such, this data must be initialized by application code.
Flash Memory Endurance¶
The flash memory for the ST31G480, which is the Secure Element used in the Ledger Blue, is rated for 500 000 erase / write cycles. This should be more than enough to last the expected lifetime of the device, but only if applications use it properly. Applications should avoid erasures as much as possible. Here are some techniques for avoiding wearing out the device’s flash memory.
Firstly, if you intend to be changing data in flash memory many times while an
application is running, consider caching the data in RAM and then flushing to
flash memory when the application has finished its operation. This of course has
the downside of possible data loss if the user powers off the device (perhaps by
unplugging it, in the case of the Nano S) before the data has been written to
persistent storage. Secondly, developers should be aware that flash memory pages
are aligned to 64-byte boundaries. The rating of 500 000 erase / write cycles
mentioned earlier means that each page in flash memory is expected to survive
500 000 erasures. As such, one can develop an application that writes to as few
pages as possible. For example, if you intend to store 32 bytes of data in flash
memory, write amplification can be avoided by making sure that 32 bytes of data
is contained entirely within a single page (and modified using only a single
call to nvm_write(...)
). If the data crossed a 64-byte page boundary, then
writing to it once may require two pages to be erased instead of just one.
In the future, Ledger will provide various persistent storage utilities within BOLOS and the SDKs to simplify the process of using flash memory efficiently.
PIC and Model Implications¶
PIC stands for Position-Independent Code. The BOLOS toolchain produces PIC to
allow for the code Link address to be different from the code Execution
address. For example, the main
function is linked in the generated
application at address 0xC0D00000
. However, the slot used when loaded into
the Secure Element could be 0x10E40400
. Therefore, if the code makes a
reference to 0xC0D00000
, even with an offset, it would be denied access as
the application is locked by the Memory Protection Unit (not to mention, this is
not the correct address of the main
function at runtime).
The PIC assembly generator makes sure every dereference is relative to the Program Counter, and never to an arbitrary address resolved during the link stage. This behavior is supported by clang versions 4.0.0 and later.
Traditionally, PIC code implies the BSS segment (RAM variables) is at a constant
offset of the code. For example, if code is at 0xC0D00000
, then global vars
may be at 0xC2D00000
, so if loaded at 0x10E00000
then global vars would
be at 0x12E00000
. However, BOLOS uses a fixed address for global vars. The
global variables start address and length are defined in the link script. Only
the code is meant to be placed at different addresses (in flash memory, rather
than RAM).
The model we chose has limitations, which are related to the way const
data
and code is referenced in other const
data. Here is a simple example:
const char array1[] = {1, 2, 3, 4};
const char array2[] = {1, 2, 3, 4};
const char *array_2d[] = {array1, array2};
void main() {
int sum, i, j;
sum = 0;
for (i = 0; i < 2; i++) {
for (j = 0; j < 4; j++) {
sum += array_2d[i][j]; // Segmentation Fault!
}
}
}
In the example above, when dereferencing array_2d
, the compiler uses a
link-time address (in the 0xC0D00000
space, following the previous
examples). This is not where the program is loaded in memory at runtime.
Therefore, when the dereference is executed, it causes a segmentation fault that
effectively stalls the SE. Luckily, the solution is pretty simple, thanks to a
small piece of assembly provided with the SDKs which is invoked with the
PIC(...)
macro. PIC(...)
uses the current load address to adjust the
link-time address in order to acquire the correct runtime address of const
data and code. The above examples can be corrected by modifying the line where
array_2d
is dereferenced to do the following:
sum += ((const char*) PIC(array_2d[i]))[j];
The same mechanism must be applied when storing function pointers in const
data. The PIC call cast is just different. Additionally, if a non-link-time
address is passed to PIC(...)
, then it will be preserved. This is possible
due to the wisely chosen link-time address which is beyond both real RAM and
loadable addresses. For example, PIC(...)
is used during a call to
io_seproxyhal_display_default(...)
, all display elements can hold a
reference to a string to be displayed with the element, and the string could be
in RAM or code, and therefore PIC(...)
is applied to acquire the correct
runtime address of the string, even if it’s in RAM.