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.
00008198 <_mainCRTStartup>:Load the address of the end of the RAM and assign its value to stack pointer (sp).
8198: e59f30f0 ldr r3, [pc, #240] ; 8290 <_mainCRTStartup+0xf8>
819c: e3530000 cmp r3, #0
81a0: 059f30e4 ldreq r3, [pc, #228] ; 828c <_mainCRTStartup+0xf4>
81a4: e1a0d003 mov sp, r3Set the value of sp for various modes, the sizes of the stacks are determined by the compiler itself.
81a8: e10f2000 mrs r2, CPSR
81ac: e312000f tst r2, #15
81b0: 0a000015 beq 820c <_mainCRTStartup+0x74>
81b4: e321f0d1 msr CPSR_c, #209 ; 0xd1
81b8: e1a0d003 mov sp, r3
81bc: e24daa01 sub sl, sp, #4096 ; 0x1000
81c0: e1a0300a mov r3, sl
81c4: e321f0d7 msr CPSR_c, #215 ; 0xd7
81c8: e1a0d003 mov sp, r3
81cc: e2433a01 sub r3, r3, #4096 ; 0x1000
81d0: e321f0db msr CPSR_c, #219 ; 0xdb
81d4: e1a0d003 mov sp, r3
81d8: e2433a01 sub r3, r3, #4096 ; 0x1000
81dc: e321f0d2 msr CPSR_c, #210 ; 0xd2
81e0: e1a0d003 mov sp, r3
81e4: e2433a02 sub r3, r3, #8192 ; 0x2000
81e8: e321f0d3 msr CPSR_c, #211 ; 0xd3
81ec: e1a0d003 mov sp, r3
81f0: e2433902 sub r3, r3, #32768 ; 0x8000
81f4: e3c330ff bic r3, r3, #255 ; 0xff
81f8: e3c33cff bic r3, r3, #65280 ; 0xff00
81fc: e5033004 str r3, [r3, #-4]
8200: e9532000 ldmdb r3, {sp}^
8204: e38220c0 orr r2, r2, #192 ; 0xc0
8208: e121f002 msr CPSR_c, r2
820c: e243a801 sub sl, r3, #65536 ; 0x10000
8210: e3b01000 movs r1, #0
8214: e1a0b001 mov fp, r1
8218: e1a07001 mov r7, r1Load 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
Was this helpful?