Most embedded tutorials advise that your code begins at app_main(). However, as engineers, we know that a whole world exists before the Operating System even wakes up.
I wanted to explore that world.
The Challenge:I set myself a goal: Hijack the ESP32 startup sequence. I wanted to inject a custom "Hardware Health Check" that runs directly on the bare silicon—no FreeRTOS, no drivers, no scheduler—before the kernel even loads.
The Failure:I wrote the hook. I manipulated the raw GPIO registers because standard drivers don't exist in the bootloader. I hit "Build & Flash."...and nothing happened. The board booted straight to the app. My code was ignored.
The Diagnosis:realisedoptimisationI spent hours debugging a silent failure. Finally, I realized the culprit wasn't my C code—it was the GCC Linker. Because the default bootloader didn't explicitly call my function, the Linker's optimization engine (--gc-sections) saw my code as "unused junk" and quietly deleted it from the binary to save space.
The Fix:I had to fight the build system. I dove into CMake and learned about Interface Linker Flags. I added target_link_libraries(... "-u bootloader_before_init"). That little -u flag screamed at the Linker: "Do not touch this symbol. I don't care if you think it's unused. Keep it."
The Result:The next flash was a victory.
Reset Board.
- Reset Board.
GPIO 2 blinks rapidly (My bare metal code running in IRAM).
- GPIO 2 blinks rapidly (My bare metal code running in IRAM).
Console logs:[BOOT] Diagnostics Passed.
- Console logs:
[BOOT] Diagnostics Passed.
FreeRTOS Kernel starts.
- FreeRTOS Kernel starts.
The Takeaway:Writing code is easy. Understanding how that code is compiled, linked, and placed into memory is Engineering.
#EmbeddedSystems #ESP32 #BareMetal #CProgramming #EngineeringLife #Bootloader







Comments