For dApps & ServicesDevice AppExplanationMemory constraintsPosition-independant code

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 in const data, 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

Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Stax, Ledger Flex, Ledger Nano, Ledger Nano S, Ledger OS, Ledger Wallet, [LEDGER] (logo), [L] (logo) are trademarks owned by Ledger SAS.