C++ firmware on small embedded systems - Part 3: What happens before main?


Introduction

We will break this program down into parts in order to try to demystify the question of what happens before the main().

After building the project and generating some form an executable file, it is a good idea to disassemble the program in order to examine the instructions it contains. This allows us to see the actual assembly code generated by the compiler. You can do this using the following command:

arm-none-eabi-objdump -d switch_led_interrupt3_1.axf > switch_led_interrupt3_1.s

This command disassembles the machine code in switch_led_interrupt3_1.axf and saves the resulting assembly instructions in a file named switch_led_interrupt3_1.s.

In our project, we are using the EFM32PG22 microcontroller. Details about setting up the debugger basics were covered in a previous post. Now, lets take a step by step approach to examine the program’s execution.

Reset Handler

The assembly Reset_Handler file in our project looks like this:

Reset_Handler:
                bl       SystemInit

                ldr      r4, =__copy_table_start__
                ldr      r5, =__copy_table_end__

.L_loop0:
                cmp      r4, r5
                bge      .L_loop0_done
                ldr      r1, [r4]                /* source address */
                ldr      r2, [r4, #4]            /* destination address */
                ldr      r3, [r4, #8]            /* word count */
                lsls     r3, r3, #2              /* byte count */

.L_loop0_0:
                subs     r3, #4                  /* decrement byte count */
                ittt     ge
                ldrge    r0, [r1, r3]
                strge    r0, [r2, r3]
                bge      .L_loop0_0

                adds     r4, #12
                b        .L_loop0
.L_loop0_done:

                ldr      r3, =__zero_table_start__
                ldr      r4, =__zero_table_end__

.L_loop2:
                cmp      r3, r4
                bge      .L_loop2_done
                ldr      r1, [r3]                /* destination address */
                ldr      r2, [r3, #4]            /* word count */
                lsls     r2, r2, #2              /* byte count */
                movs     r0, 0

.L_loop2_0:
                subs     r2, #4                  /* decrement byte count */
                itt      ge
                strge    r0, [r1, r2]
                bge      .L_loop2_0

                adds     r3, #8
                b        .L_loop2
.L_loop2_done:

                bl      __START

So, what is happening here? What are these loops, and how do we get to the main? Lets start step by step. The execution immediately jumps to the SystemInit function. Lets see what that looks like.

System Init

After, monitor reset command, we can see that first instruction is the start of Reset Handler function. First instructions are regarding the configuration of the ARM-Cortex M33 core itself.

SCB->VTOR = (uint32_t) (&__VECTOR_TABLE[0]);

SCB is abbreviation for System control block registers. The VTOR indicates the offset of the vector table base address from memory address 0x00000000.

SCB->CPACR |= ((3U << 10U * 2U)           /* set CP10 Full Access */
           |  (3U << 11U * 2U));          /* set CP11 Full Access */

CPACR is Coprocessor Access Control Register. It has bits that are defining the access rights for the floating-point functionality. Here, they are configured for full access to the FP Extension.

CMU->CLKEN1_SET = CMU_CLKEN1_SMU;

Enables PCLK/HCLK for SMU (Security Management Unit) module.

SMU->PPUSATD0_CLR =  _SMU_PPUSATD0_MASK;
SMU->PPUSATD1_CLR = (_SMU_PPUSATD1_MASK & ~SMU_PPUSATD1_SMU);

Clearing all bits in PPUSATD registers beside the bit for SMU peripheral, in other words - configuring SMU to Secure and other peripherals to Non-Secure.

SAU->CTRL = SAU_CTRL_ALLNS_Msk;

Security Attribution Unit Control Register to all Non-secure.

__ASM volatile ("dsb 0xF":::"memory");

DSB (Data Synchronization Barrier) ensures that all previous data accesses (loads and stores) are completed before any subsequent instructions are executed. sy stands for synchronization, and it ensures that the barrier applies to all levels of the memory system, meaning all memory operations before the DSB are completed in the system (from cache to memory).

__ASM volatile ("isb 0xF":::"memory");

Instruction Synchronization Barrier (ISB) flushes the instruction pipeline and ensures that all previously executed instructions have completed and their effects are visible before continuing with the next instructions. The sy stands for System level synchronization in this case, and it ensures that the barrier applies across all levels of the system.

NVIC->ICPR[(((uint32_t)SMU_SECURE_IRQn) >> 5UL)] = (uint32_t)(1UL << (((uint32_t)SMU_SECURE_IRQn) & 0x1FUL));
SMU->IF_CLR = SMU_IF_PPUSEC | SMU_IF_BMPUSEC;

Clearing the SMU_SECURE_IRQn flag in ARM cortex NVIC register and clearing the flags responsible for security fault in the Bus Manager Protection Unit and Peripheral Protection Unit.

NVIC->ISER[(((uint32_t)SMU_SECURE_IRQn) >> 5UL)] = (uint32_t)(1UL << (((uint32_t)SMU_SECURE_IRQn) & 0x1FUL));
SMU->IEN = SMU_IEN_PPUSEC | SMU_IEN_BMPUSEC;

Enabling the interrupt with setting enbled appropriate interrupt flags.

The program has returned from the SystemInit function, and the execution of the Reset Handler continues…

ldr      r4, =__copy_table_start__
ldr      r5, =__copy_table_end__

Loading the start and end addresses of copy table in registers r4 and r5.

.L_loop0:
                cmp      r4, r5

The instruction compares the values in r4 and r5. It sets the condition flags based on the result.

bge      .L_loop0_done

Based on the condition flags set by cmp, the code branches to .L_loop0_done, skipping the loop body.

ldr      r1, [r4]                /* source address */
ldr      r2, [r4, #4]            /* destination address */
ldr      r3, [r4, #8]            /* word count */

Load the value from the address in r4 (source address) into r1. Load the value from the address r4 + 4 (destination address) into r2. Load the value from the address r4 + 8 (word count) into r3.

lsls     r3, r3, #2              /* byte count */

Shifts r3 left by 2 bits. This is equivalent to multiplying the word count by 4 to convert from word to byte count.

.L_loop0_0:
                subs     r3, #4                  /* decrement byte count */

Moving through the data in chunks, processing 4 bytes at a time.

ittt     ge

ittt (“if-then-then” instruction) sets up for conditional execution of multiple subsequent instructions based on the condition flags.

ldrge    r0, [r1, r3]
strge    r0, [r2, r3]

These two instructions are executed if the condition ge (greater than or equal) is met.

bge      .L_loop0_0

Instruction checks the condition flags, and if the comparison was greater than or equal (ge), it branches back to .L_loop0_0 to repeat the loop.

adds     r4, #12
b        .L_loop0

If not, by adding 12 to r4, the code moves to the next entry in the copy table. The b instruction unconditionally branches to .L_loop0.

To sum up: this part handles copying data from a source to a destination using a copy table. It iterates through each entry in the table, loading the source address, destination address, and word count, then copies the data in chunks until the entire table is processed.

.L_loop0_done:
                ldr      r3, =__zero_table_start__
                ldr      r4, =__zero_table_end__

Next instructions working similarly, but since there are not additional bss sections definied in .ld file, so __zero_table_start__ and __zero_table_end__ have the same values, hence the execution is going to .L_loop2_done.

Now, the strange part… At the end of the Reset_Handler in startup file, there is an instruction:

bl      __START

Upon inspecting the disassembly, we notice that the linker somehow placed _mainCRTStartup at this location. However, this function doesn’t seem to be defined anywhere! So, how did it end up there?

We made the project with this command:

arm-none-eabi-gcc -g -gdwarf-2 -mcpu=cortex-m33 -mthumb -T "switch_led_interrupt3_1.ld" -Wl,--no-warn-rwx-segments -Xlinker --gc-sections -Xlinker -Map="switch_led_interrupt3_1.map" -mfpu=fpv5-sp-d16 -mfloat-abi=softfp --specs=nano.specs -o switch_led_interrupt3_1.axf "./CMSIS/EFM32PG22/startup_efm32pg22.o" "./src/main_switch_led_int_s2.o" -Wl,--start-group -lgcc -lc -lnosys -Wl,--end-group

As you can see, several flags are present. Some of them influence the linking of the crt0.o file, which contains the mainCRTStartup. Depending on the flags used, the appropriate crt0.o file will be selected.

In the project’s .map file, we can locate the specific crt0.o file that was used. On our system, it is:

/opt/gcc-arm/bin/../lib/gcc/arm-none-eabi/13.3.1/../../../../arm-none-eabi/lib/thumb/v8-m.main+fp/softfp/crt0.o.

But why this specific file, when there are countless others in /opt/gcc-arm ? You can see all the available files using the command:

find . -type f -name "*crt*.o"

The answer lies in the flags used during the project build. These flags determine which crt0.o file is selected for linking. Let’s dive into the disassembly (since the source file is not available) and continue our investigation…

_mainCRTStartup

_stack_init

00000850 <_stack_init>:
     850:	f5a3 3a80 	sub.w	sl, r3, #65536	@ 0x10000
     854:	4770      	bx	lr
     856:	bf00      	nop
00000858 <_mainCRTStartup>:
     858:	4b17      	ldr	r3, [pc, #92]	@ (8b8 <_mainCRTStartup+0x60>)
     85a:	2b00      	cmp	r3, #0
     85c:	bf08      	it	eq
     85e:	4b13      	ldreq	r3, [pc, #76]	@ (8ac <_mainCRTStartup+0x54>)
     860:	469d      	mov	sp, r3
     862:	f7ff fff5 	bl	850 <_stack_init>

The purpose of these instructions is to calculate an appropriate stack limit and load it into the sl (stack limit) register.

     866:	2100      	movs	r1, #0
     868:	468b      	mov	fp, r1
     86a:	460f      	mov	r7, r1

Setting registers r1 and fp (frame pointer – r11) to 0.

     86c:	4813      	ldr	r0, [pc, #76]	@ (8bc <_mainCRTStartup+0x64>)
     86e:	4a14      	ldr	r2, [pc, #80]	@ (8c0 <_mainCRTStartup+0x68>)
     870:	1a12      	subs	r2, r2, r0
     872:	f001 fb53 	bl	1f1c <memset>
...
     8bc:	20000058 	.word	0x20000058
     8c0:	200001b8 	.word	0x200001b8

    0x20000058          __bss_start__ = .
    0x200001b8          __bss_end__ = .

Loading the start and end addresses to clear the .bss section, followed by a jump to the memset function to perform that operation.

memset

00001f1c <memset>:
    1f1c:	4402      	add	r2, r0
    1f1e:	4603      	mov	r3, r0
    1f20:	4293      	cmp	r3, r2
    1f22:	d100      	bne.n	1f26 <memset+0xa>
    1f24:	4770      	bx	lr
    1f26:	f803 1b01 	strb.w	r1, [r3], #1
    1f2a:	e7f9      	b.n	1f20 <memset+0x4>

Adds r0 to r2. The resulting value in r2 becomes the end address of the memory block. Copies the starting address r0 into r3. Compares the current loop pointer r3 with the end address r2. If r3 is not equal to r2, branch to the memory clearing instruction (continue the loop). Stores the byte value in r1 at the address pointed to by r3, then increments r3 by 1. Remember that r1 value vas previously set to zero.

     876:	4b0e      	ldr	r3, [pc, #56]	@ (8b0 <_mainCRTStartup+0x58>)
     878:	2b00      	cmp	r3, #0
     87a:	d000      	beq.n	87e <_mainCRTStartup+0x26>
     87c:	4798      	blx	r3

The value is loaded into register r3, which is zero, so the execution jumps to address 0x87a.

     87e:	4b0d      	ldr	r3, [pc, #52]	@ (8b4 <_mainCRTStartup+0x5c>)
     880:	2b00      	cmp	r3, #0
     882:	d000      	beq.n	886 <_mainCRTStartup+0x2e>
     884:	4798      	blx	r3

The same process occurs here: if these values were not zero, the execution would jump to the address stored in register r3.

     886:	2000      	movs	r0, #0
     888:	2100      	movs	r1, #0
     88a:	0004      	movs	r4, r0
     88c:	000d      	movs	r5, r1

Clearing registers.

     88e:	480d      	ldr	r0, [pc, #52]	@ (8c4 <_mainCRTStartup+0x6c>)
     890:	2800      	cmp	r0, #0
     892:	d002      	beq.n	89a <_mainCRTStartup+0x42>
     894:	480c      	ldr	r0, [pc, #48]	@ (8c8 <_mainCRTStartup+0x70>)
     896:	f3af 8000 	nop.w
     89a:	f001 fb1b 	bl	1ed4 <__libc_init_array>

Similar to previously instruction blocks.

     89a:	f001 fb1b 	bl	1ed4 <__libc_init_array>

It seems the execution is jumping somewhere…

__libc_init_array

00001ed4 <__libc_init_array>:
    1ed4:	b570      	push	{r4, r5, r6, lr}

Saves the registers values r4, r5, r6, and the return address (lr) onto the stack.

    1ed6:	4d0d      	ldr	r5, [pc, #52]	@ (1f0c <__libc_init_array+0x38>)
    1ed8:	2600      	movs	r6, #0
    1eda:	4c0d      	ldr	r4, [pc, #52]	@ (1f10 <__libc_init_array+0x3c>)
    1edc:	1b64      	subs	r4, r4, r5
    1ede:	10a4      	asrs	r4, r4, #2

r4 holds the number of constructors.

    1ee0:	42a6      	cmp	r6, r4
    1ee2:	d109      	bne.n	1ef8 <__libc_init_array+0x24>
...
    1ef8:	f855 3b04 	ldr.w	r3, [r5], #4
    1efc:	3601      	adds	r6, #1
    1efe:	4798      	blx	r3
    1f00:	e7ee      	b.n	1ee0 <__libc_init_array+0xc>

The .init_array section in the linker script contains the constructor table (an array of pointers to initialization functions), and the symbols __preinit_array_start and __preinit_array_end mark the beginning and end of this array.

  • If r4 == 0: Skip processing because there are no constructors.
  • If r4 > 0: Jump to 0x1ef8 to iterate through the function pointers in the .init_array.

With each iteration, the value in register r6 is incremented by 1. The loop terminates when the values in registers r4 and r6 are equal, indicating that all constructor functions have been executed.

The blx instruction calls the constructor function by jumping to the address stored in register r3. Once the constructor function completes, execution returns to the instruction at address 0x1ee0 to check if r4 and r6 are equal. This ensures that the process continues until all constructor functions have been executed.

_init
    1ee4:	4d0b      	ldr	r5, [pc, #44]	@ (1f14 <__libc_init_array+0x40>)
    1ee6:	2600      	movs	r6, #0
    1ee8:	4c0b      	ldr	r4, [pc, #44]	@ (1f18 <__libc_init_array+0x44>)
    1eea:	f000 f88b 	bl	2004 <_init>
…
00002004 <_init>:
    2004:	b5f8      	push	{r3, r4, r5, r6, r7, lr}
    2006:	bf00      	nop
    2008:	bcf8      	pop	{r3, r4, r5, r6, r7}
    200a:	bc08      	pop	{r3}
    200c:	469e      	mov	lr, r3
    200e:	4770      	bx	lr

This implementation of _init doesn’t achieve any functional purpose. The values of the registers and the stack remain the same as at the start. The sequence exists as a placeholder.

    1eee:	1b64      	subs	r4, r4, r5
    1ef0:	10a4      	asrs	r4, r4, #2
    1ef2:	42a6      	cmp	r6, r4
    1ef4:	d105      	bne.n	1f02 <__libc_init_array+0x2e>
    1ef6:	bd70      	pop	{r4, r5, r6, pc}
...
    1f02:	f855 3b04 	ldr.w	r3, [r5], #4
    1f06:	3601      	adds	r6, #1
    1f08:	4798      	blx	r3
    1f0a:	e7f2      	b.n	1ef2 <__libc_init_array+0x1e>

Generaly this part is the same as one of previous, with difference that here we are doing with __init_array_start and __init_array_end.

Since the value 0x82d is stored at address 0x20000050 and was previously loaded into register r3, the program execution goes to the value held in r3, jumping to address 0x82d.

And now we are jumping to function on that address…

frame_dummy
    82c:	b508      	push	{r3, lr}

This saves r3 and lr (Link Register) onto the stack for later restoration. It’s the function prologue.

    82e:	4b05      	ldr	r3, [pc, #20]	@ (844 <frame_dummy+0x18>)
    830:	b11b      	cbz	r3, 83a <frame_dummy+0xe>

Compare the value in the register to zero and branch if it is equal to zero.

     832:	4905      	ldr	r1, [pc, #20]	@ (848 <frame_dummy+0x1c>)
     834:	4805      	ldr	r0, [pc, #20]	@ (84c <frame_dummy+0x20>)
     836:	f3af 8000 	nop.w
     83a:	e8bd 4008 	ldmia.w	sp!, {r3, lr}

It loads two 32-bit values from the memory pointed to by the stack pointer (sp) into registers r3 and lr. After loading, the stack pointer (sp) is incremented by 8 (2 × 32-bit), effectively popping those values from the stack.

  • The value at the address pointed to by sp is loaded into r3.
  • The value at the address pointed to by sp + 4 is loaded into lr.
  • Afterward, sp is incremented by 8 (to sp + 8).
     83e:	f7ff bfcd 	b.w	7dc <register_tm_clones>

After that execution goes to this address.

  • b: A branch instruction that causes the program to jump to another address.
  • .w: Forces the instruction to use the 32-bit encoding, which allows for a larger range of offsets to branch to, compared to the 16-bit encoding.

16-bit branch: Can only jump within ±1 KB (512 instructions).

32-bit branch (b.w): Can jump within ±16 MB (8,388,608 instructions).

In this specific case, b.w 7dc allows the program to branch to the address 0x7dc, which is within the allowable range for both 16-bit and 32-bit encodings. However, the .w ensures that the instruction is explicitly encoded as 32-bit.

And here we are jumping again into some unknown fucntion…

register_tm_clones
000007dc <register_tm_clones>:
     7dc:	4b06      	ldr	r3, [pc, #24]	@ (7f8 <register_tm_clones+0x1c>)
     7de:	4907      	ldr	r1, [pc, #28]	@ (7fc <register_tm_clones+0x20>)
     7e0:	1ac9      	subs	r1, r1, r3
     7e2:	1089      	asrs	r1, r1, #2
     7e4:	bf48      	it	mi
     7e6:	3101      	addmi	r1, #1
     7e8:	1049      	asrs	r1, r1, #1
     7ea:	d003      	beq.n	7f4 <register_tm_clones+0x18>
     7ec:	4b04      	ldr	r3, [pc, #16]	@ (800 <register_tm_clones+0x24>)
     7ee:	b10b      	cbz	r3, 7f4 <register_tm_clones+0x18>
     7f0:	4801      	ldr	r0, [pc, #4]	@ (7f8 <register_tm_clones+0x1c>)
     7f2:	4718      	bx	r3
     7f4:	4770      	bx	lr
     7f6:	bf00      	nop
     7f8:	20000058 	.word	0x20000058
     7fc:	20000058 	.word	0x20000058
     800:	00000000 	.word	0x00000000

There is the thing called tm_clone_table that resides at the address 0x20000058 in RAM and is part of the .bss section. It is used by the GCC runtime (e.g., register_tm_clones) to manage transactional memory clones. In our case, the table is empty, so jump at 0x7ea is executed.

…and after all of this things we are going back at mainCRTStartup from which we will go to the main() finaly.

     89e:	0020      	movs	r0, r4
     8a0:	0029      	movs	r1, r5
     8a2:	f000 fb69 	bl	f78 <main>
     8a6:	f001 fb03 	bl	1eb0 <exit>
     8aa:	bf00      	nop

Sources

  • arm Developer, The Cortex-M33 Peripherals (link)
  • arm Developer, The ARM instruction sets (link)