Compiling bare-metal C code for an ARM Cortex-M microcontroller using arm-none-eabi-gcc often results in a frustrating linker failure: undefined reference to _exit or undefined reference to _sbrk. This error happens because standard C library functions (like exit(), printf(), and malloc()) expect an underlying operating system to provide low-level POSIX system calls.
The Cause: Bare-Metal Firmware Has No OS System Calls
When your main() function returns or calls exit(), Newlib (the C library shipped with ARM GCC) attempts to invoke _exit(status) to yield control back to the OS kernel. On a bare-metal microcontroller with no OS, _exit() does not exist:
main() return -> exit() -> _exit() -> POSIX System Call (MISSING on Bare-Metal!)
|
+--> Linker Error: undefined reference to _exitSolution 1: Add --specs=nosys.specs to Linker Flags (Recommended)
The cleanest fix is telling the ARM GCC linker to use nosys.specs, which automatically supplies stub implementation functions for all missing OS system calls (_exit, _sbrk, _write, _read):
# Build bare-metal firmware using nano C library and nosys stubs
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb main.c -o firmware.elf \
--specs=nano.specs \
--specs=nosys.specsSolution 2: Implement a Custom _exit() Stub in C
If you prefer not to pull in nosys.specs, implement your own _exit() stub function that enters an infinite hardware loop to prevent system runaway:
#include <unistd.h>
// Custom _exit stub for bare-metal ARM microcontrollers
void _exit(int status) {
(void)status; // Suppress unused parameter warning
// Disable interrupts and trap CPU in an infinite loop
__asm__ volatile ("cpsid i"); // Disable IRQ interrupts
while (1) {
__asm__ volatile ("wfi"); // Wait for Interrupt (Low power idle)
}
}Why Infinite Loop _exit() is Necessary on Microcontrollers:
Prevents CPU Instruction Drift: If
main()exits without an infinite loop, the CPU program counter will attempt to execute uninitialized flash memory.
Comments and corrections