UNIT 05 · LESSON 5 OF 6

From Startup Code to the Application

Which code runs before main(), in what order, and what is each piece allowed to assume?

INTERACTIVEThe chain from reset to main()
The start-up call chain with the facilities available at the selected step1. Reset_Handler entered2. SystemInit(): early hardware3. copy .data4. zero .bss5. __libc_init_array(): constructors6. main()7. main() returnsmay rely on:✓ stack pointer✗ final clock✗ initialised statics✗ zeroed statics✗ constructorsSystemInit(): early hardware: CMSIS’s template sets VTOR; ST’s also enables the FPU.Neither raises the clock. It must not rely on static variables: they are notinitialised yet.
The start-up call chain with the facilities available at the selected step1. Reset_Handler entered2. SystemInit(): early hardware3. copy .data4. zero .bss5. __libc_init_array(): constructors6. main()7. main() returnsmay rely on:✓ stack pointer✗ final clock✗ initialised statics✗ zeroed statics✗ constructorsSystemInit(): early hardware: CMSIS’s template setsVTOR; ST’s also enables the FPU. Neither raises theclock. It must not rely on static variables: theyare not initialised yet.

Try this

Start-up
CMSIS step 2, SystemInit(): early hardware. Available: stack pointer.

Two published start-up patterns. CMSIS and ST’s templates call SystemInit (VTOR, FPU; not the clock) and then initialise memory, leaving the clock to the application; the pico-sdk’s crt0.S initialises memory first, on the slow boot clock, and sets up clocks and constructors in runtime_init(). Step through to see what code may rely on at each point.

What you will be able to do
  • List the steps between Reset_Handler and main() in a CMSIS-style start-up and in the pico-sdk, and say what code may rely on at each step.
  • Explain why SystemInit() must not rely on static variables, and predict what happens to a value it stores in one.
  • Predict the order in which constructors run from their priorities and link order.
  • Explain how the linker script collects constructors into .init_array and how start-up code calls them.
  • Describe what happens when main() returns on bare metal and why firmware usually never returns.
Before you start
  • Copying .data and zeroing .bss (lesson 4).
  • Sections, KEEP() and SORT() in linker scripts (unit 4, lesson 4).
Steps in this lesson
  1. Two published orders
  2. Constructors
  3. When main() returns
  4. Worked example: in what order does it all run?
  5. Common misconceptions

The puzzle

main() feels like the start of the program, but by the time it runs, a dozen functions have already been called, some of them yours. A logging module’s constructor may have run, the clock may already be at full speed, or it may not. Which code runs before main(), in what order, and what is each piece allowed to assume?

STEP 1

Two published orders

A full start-up does the same jobs: early hardware set-up, initialise memory, run constructors, call main(), with the clock raised somewhere along the way. They differ in the order, and the order decides what each step may rely on.

↑ This step uses the figure at the top of the page.

CMSIS and ST’s templates call SystemInit() first: a vendor hook for early hardware set-up. CMSIS’s own template only sets VTOR and the SystemCoreClock variable, and ST’s current F4 version sets the FPU access bits and VTOR; raising the clock is left to application code. Some vendors’ versions do raise the clock here, before memory is initialised. Then the memory loops run, the C library runs constructors (ST calls __libc_init_array; CMSIS’s __cmsis_start walks its copy and zero tables and calls the library’s _start), and main() is called.

The pico-sdk initialises memory first, on the slow boot clock, then calls runtime_init(): it resets most peripherals, releases those that need only the system clock, runs .preinit_array, calls clocks_init(), releases the rest, moves the vector table to RAM and, last, runs the constructors. Only then does crt0.S call main().

The consequence to remember: SystemInit() runs before static variables are valid. A value it writes to a .bss variable is wiped by the zero loop, and one it writes to an initialised variable is overwritten by the copy. CMSIS’s own template stores SystemCoreClock, an initialised global, whose initial value happens to be the same, so the copy changes nothing.

STEP 2

Constructors

A constructor is a function the runtime calls before main(): in C, one marked __attribute__((constructor)); in C++, the code that builds every object with static storage duration. The compiler puts a pointer to each in a section called .init_array (or .init_array.NNNNN when a priority is given); the linker script gathers them between two symbols, and the start-up walks the list:

extern void (*__init_array_start)(void), (*__init_array_end)(void);
for (void (**p)(void) = &__init_array_start; p < &__init_array_end; ++p)
    (*p)();

That loop is the pico-sdk’s, almost word for word. On a 32-bit chip each entry is a 4-byte pointer, so a map file tells you how many constructors will run:

N=address of __init_array_end−address of __init_array_start4N = \frac{\text{address of } \texttt{\_\_init\_array\_end} - \text{address of } \texttt{\_\_init\_array\_start}}{4}

For example, 0x2000_0118 − 0x2000_010C = 12 bytes: three constructors. The linker script uses KEEP(*(SORT(.init_array.*))) followed by KEEP(*(.init_array)): KEEP because nothing references the pointers, SORT so that prioritised entries come first, ordered by priority.

INTERACTIVEIn what order constructors run
Three constructors with priorities and the order in which they runa.o: __attribute__((constructor)) void clock_setup(void)b.o: __attribute__((constructor(200))) void log_init(void)c.o: __attribute__((constructor(101))) void heap_init(void)run order before main():1. heap_init() (.init_array.00101, from c.o)2. log_init() (.init_array.00200, from b.o)3. clock_setup() (.init_array, from a.o)Priorities 0–100 are reserved for the compiler and its libraries. If one constructordepends on another, give them explicit priorities rather than relying on link order.
Three constructors with priorities and the order in which they runa.o: __attribute__((constructor)) voidclock_setup(void)b.o: __attribute__((constructor(200))) voidlog_init(void)c.o: __attribute__((constructor(101))) voidheap_init(void)run order before main():1. heap_init() (.init_array.00101, from c.o)2. log_init() (.init_array.00200, from b.o)3. clock_setup() (.init_array, from a.o)Priorities 0–100 are reserved for the compiler andits libraries. If one constructor depends onanother, give them explicit priorities rather thanrelying on link order.
Run order: heap_init(), log_init(), clock_setup(), then main().

GCC’s constructor attribute makes a function run before main(); an optional priority from 101 to 65535 orders them, smaller numbers first. With the usual GNU linker script (KEEP(*(SORT(.init_array.*))) then KEEP(*(.init_array))), prioritised entries run first in numerical order and unprioritised ones follow in link order. The files are linked in the order a.o, b.o, c.o.

GCC accepts priorities from 101 to 65535 (0–100 are reserved); smaller numbers run first. Without priorities, the order follows link order, which changes when someone reorders files in a build script. If one constructor depends on another (a logger that needs the heap), give both explicit priorities, or better, call an explicit init function from main().

STEP 3

When main() returns

On a desktop, main() returns to the C library, which calls exit(): atexit handlers and destructors run, and the process ends. On bare metal there is nothing to return to, and what happens depends on the start-up. ST’s template follows bl main with bx lr, so a returning main() spins on that instruction; lesson 6’s start-up parks in a loop. A start-up that enters main() through the C library’s _start calls exit(), which runs atexit handlers and destructors and then _exit(). The pico-sdk calls exit() but skips that machinery: its _exit() by default executes a breakpoint, which halts in an attached debugger and otherwise faults (a build option makes it enter USB boot instead). Most firmware makes the question moot by never leaving main()’s loop.

STEP 4

Worked example: in what order does it all run?

A pico-sdk program links three files, a.o, b.o and c.o, with these constructors:

/* a.o */ __attribute__((constructor))      void clock_setup(void);
/* b.o */ __attribute__((constructor(200))) void log_init(void);
/* c.o */ __attribute__((constructor(101))) void heap_init(void);

The two prioritised ones land in .init_array.00101 and .init_array.00200, which SORT orders first; clock_setup is in plain .init_array and follows. The full sequence after reset is: copy .data, zero .bss (boot clock), runtime_init() (peripherals, clocks_init(), vector table), then heap_init(), log_init(), clock_setup(), and finally main(). Note that clock_setup() runs after the SDK has already set the clocks, and after log_init(), which therefore cannot rely on anything it does.

MYTHS AND FACTS

Common misconceptions

main() is the first code of mine that runs

Constructors, SystemInit() and, in some start-ups, hooks such as .preinit_array all run earlier.

SystemInit can set a global for later

It runs before the memory loops; its writes to statics are overwritten.

Constructors run in source order

They run by priority, then in link order.

Returning from main() restarts the program

It ends in a spin loop, an exit() path or a breakpoint, depending on the start-up; nothing restarts unless a watchdog does.

SystemInit sets the clock

Only in some vendors’ files: CMSIS’s and ST’s current templates do not, and the pico-sdk initialises memory on the boot clock and sets clocks in runtime_init().

Check yourself

Answer in your head, then open the card.

In a CMSIS start-up, SystemInit writes boot_count++ to a global uint32_t boot_count = 0;. What is boot_count in main()?
  1. It is an initialised global in .data; the copy loop, which runs after SystemInit, overwrites it with its initial value.
Three constructors have priorities none, 150 and 101. In what order do they run?

101, then 150, then the one without a priority.

Why does the linker script need KEEP() around .init_array?

No code references the function pointers by name; the start-up only walks the range between two symbols. With --gc-sections the linker would discard the unreferenced sections, and the constructors would silently never run.

A pico-sdk program sets a breakpoint on main() and crashes before reaching it. Where would you look?

In what runs between reset and main(): the memory loops (unlikely), runtime_init() and above all the constructors, which run at the end of runtime_init().

Sources (5)
  1. Arm, CMSIS-DFP, Device/ARMCM0plus/Source/startup_ARMCM0plus.c and system_ARMCM0plus.c — Reset_Handler calls SystemInit() then __PROGRAM_START(); the template SystemInit sets SCB->VTOR to the vector table (when present) and SystemCoreClock
  2. Arm, CMSIS 6, CMSIS/Core/Include/m-profile/cmsis_gcc_m.h — __PROGRAM_START is __cmsis_start(): it walks the linker’s copy table and zero table word by word, then calls the C library’s _start()
  3. STMicroelectronics, cmsis-device-f4, Source/Templates/gcc/startup_stm32f407xx.s — SystemInit, the .data copy, the .bss zero loop, bl __libc_init_array (“Call static constructors”), bl main, then bx lr (a returning main spins there); system_stm32f4xx.c: SystemInit sets the FPU access bits (CPACR), optionally the external memory controller, and VTOR, and does not change the clock
  4. Raspberry Pi Ltd, pico-sdk 1.5.1, pico_standard_link/crt0.S, pico_runtime/runtime.c and memmap_default.ld — crt0: core check, .data copy, .bss zero, runtime_init, main, exit, then bkpt; runtime_init: reset peripherals, .preinit_array, clocks_init(), release peripherals, RAM vector table, then the .init_array loop; exit() calls _exit() (“no desire to pull in __call_exitprocs”), which loops on __breakpoint() unless PICO_ENTER_USB_BOOT_ON_EXIT; the script’s KEEP(*(SORT(.init_array.*))) KEEP(*(.init_array)) between __init_array_start and __init_array_end
  5. GCC manual, “Common Function Attributes”: constructor, destructor — constructor functions run before main(); priorities 101–65535, 0–100 reserved; “A constructor with a smaller priority number runs before a constructor with a larger priority number”; no argument is equivalent to 65535 (read from gcc/doc/extend.texi in the gcc-mirror repository)