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 to0x1ef8to 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
spis loaded intor3. - The value at the address pointed to by
sp + 4is loaded intolr. - Afterward,
spis incremented by 8 (tosp + 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
