Position-Independent Code
Position-Independent Code (PIC) is a requirement for all Ledger device apps. Because Ledger OS loads apps into different flash memory slots at runtime, your code must not contain hard-coded addresses that reference compile-time link addresses. This page explains how PIC works in the Ledger OS environment, what limitations it introduces, and how to use the PIC() macro to avoid common segmentation faults.
This page is part of the memory management series for Ledger device apps. See also persistent storage and memory alignment.
PIC and model implications
PIC stands for Position-Independent Code. The Ledger OS toolchain produces PIC to allow 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, Ledger OS 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 choose 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 straightforward, 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 example can be corrected by modifying the line where array_2d is dereferenced as follows:
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(...), 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 is in RAM.
Summary
The key rules for writing PIC-compliant code:
- Use the
PIC(...)macro whenever you dereference a pointer stored inconstdata, including function pointers. - Never assume that a link-time address matches the runtime address.
PIC(...)passes through RAM addresses safely; you can apply it even when you are unsure whether a pointer points to RAM or flash.- The PIC model only relocates code in flash; global variables (BSS segment) always live at a fixed address defined in the link script.
Further reading
- Persistent storage — how flash memory and NVRAM are managed in Ledger OS.
- Memory alignment — alignment constraints for the Secure Element.
- Application environment — how Ledger OS isolates and loads apps into the Secure Element.