Get Simple Application Compiled
Let's try to compile simple application of infinite loop, called test_cpp_simple.
A linker script is required to get all the generated objects successfully linked. It states what code/data sections need to be loaded at what addresses as well as defines several symbols that may be required by the sources. Here is a good manual of linker script syntax and here is the linker script I use to get applications linked for Raspberry Pi platform.
Depending on your compiler, the link may fail because some symbols are missing. For example __exidx_start
and __exidx_end
are needed when the application is compiled with exceptions support, or __bss_start__
and __bss_end__
may be required by standard library if it contains the code for zeroing .bss
section.
Every application must have a startup code usually written in Assembler. This startup code must perform the following steps: 1. Write the interrupt vector table at appropriate location (usually at address 0x0000). 1. Set the stack pointers for every runtime mode. 1. Zero the .bss section 1. Call constructors of global (static) objects (applicable only to C++) 1. Call the main function.
It may happen that compiler generates some startup code for you, especially if you haven't excluded standard library (stdlib) from compilation. To check whether this is the case, we need to analyse assembler listing of the successfully compiled and linked image binary. All the generated files for a test application will reside in <build_dir>/src/test_cpp/<app_name>
. The assembler listing file will have kernel.list
name.
Side note: the assembler listing can be generated using the following command:
arm-none-eabi-objdump -D -S app_binary > app.list
Open the listing file and look for function with CRT string in it. CRT stands for “C Run-Time”. When using this compiler, the function that compiler has generated, is called _mainCRTStartup
. Let's take closer look what this function does.
Load the address of the end of the RAM and assign its value to stack pointer (sp).
Set the value of sp for various modes, the sizes of the stacks are determined by the compiler itself.
Load the addresses of __bss_start__
and __bss_end__
symbols and zero all the area in between.
Call the __libc_init_array
function provided by standard library which will initialise all the global objects. It will treat the area between __init_array_start
and __init_array_end
as list of pointers to initialisation functions and call them one by one.
Call the main function.
If main
function returns for some reason, call the exit function, which probably must be implemented as infinite loop or jumping back to the beginning of the startup code.
Here comes local data
The only missing stage in the startup process is updating the interrupt vector table. After the latter is updated properly, it is possible to call the provided _mainCRTStartup
function. However, if your compiler doesn't provide such function you have no other choice but to write the whole startup code yourself. Here is an example of such code.
Please note, that .bss
section by definition contains uninitialised data that must be zeroed at startup. Even if you don't have uninitialised variables in your code, zeroing .bss
is a must have operation. This is because compiler might put variables that are explicitly initialised to 0 into the .bss
for performance reasons and count on this section being zeroed at startup.
Also note, that pointers to initialisation functions of global variables reside in .init.array
section. To initialise your global objects you just iterate over all entries in this section and call them one by one.
To implement the missing stage for use the following assembler instructions:
Please note that at interrupt vector table that resides at address 0x0000 contains branch instructions to the appropriate handlers, not just addresses of the handlers. Let's take a closer look how these branching instructions look in our assembler listing file:
The branching instructions load address of the interrupt function to “pc” register. However the address of the function is stored somewhere and compiler generates access to this storage using relative offset to current “pc” register. This is the reason why we have to copy not just the branching instructions, but also the storage area where addresses of interrupt routines are stored:
Last updated