While browsing some vendor-supplied startup code for a commodity MCU and reading this excellent SO post about GCC's naked
attribute, I seem to have come to a contradiction. The linked GCC documentation states (emphasis mine):
All other statements, including declarations of local variables, if statements, and so forth, should be avoided.
However, the code clearly seems to violate that:
__attribute__ ((naked, section(".after_vectors.reset")))
void ResetISR(void) {
// ...
// Set timeout value
*((volatile unsigned int *)0x40052008) = 0xFFFF; // OK, direct memory access
// Now disable watchdog via control register
volatile unsigned int *WDOG_CS = (unsigned int *) 0x40052000; // OK, literal address
*WDOG_CS = (*WDOG_CS & ~(1 << 7)) | (1 << 5); // literal doesn't need mem for ptr itself
#endif // (__USE_CMSIS)
//
// Copy the data sections from flash to SRAM.
//
unsigned int LoadAddr, ExeAddr, SectionLen; // <---- HOW IS THIS POSSIBLE?
unsigned int *SectionTableAddr;
// Load base address of Global Section Table
SectionTableAddr = &__data_section_table;
// Copy the data sections from flash to SRAM.
while (SectionTableAddr < &__data_section_table_end) {
LoadAddr = *SectionTableAddr++; // Variables accessed here
ExeAddr = *SectionTableAddr++;
SectionLen = *SectionTableAddr++;
data_init(LoadAddr, ExeAddr, SectionLen);
}
That SO post does go on to mention that the stack is fully set up. Are these locals safe because we are in the special condition of being just out of reset and so there is nothing to clobber? If that is the case, what is the address of these locals as there is no stack frame, per se?
What am I missing?
The reset ISR and a general hand-crafted function with inline assembler are different things.
That SO post does go on to mention that the stack is fully set up.
They are not referring to the specific case of a reset ISR. In the reset ISR on a traditional MCU, the stack does not "exist" - or rather the SP has not yet been initialized. Everywhere else in the program, the stack will be available however.
In general terms:
Whenever a plain old function is called, the return address, status registers and function parameters and function return data are stored as per calling convention for the given ABI. This means that a bunch of things get stored in CPU registers and/or on the stack, either on the callee side and/or on the caller side.
For bare metal microcontrollers, this is super-specific as there's often not an universal ABI, so it might be up to a particular compiler port to come up with an ABI.
Interrupt service routines (ISR) usually have their own specific calling convention and here it is at least partially specified by the CPU manufacturer, because upon interrupts some things are stored by hardware, not by software.
This calling convention for interrupts may or may not be the same as the calling convention for plain functions. In case they differ, then some non-standard function qualifier keyword like interrupt
is usually cooked up by the compiler, so that correct instructions for calling convention and return are generated.
Or alternatively, if you wish to craft all that by hand in inline assembler, something like gcc naked
attribute can be used, in which case the programmer is responsible for manually writing code following the calling convention.
A reset ISR has no caller in software so following a calling convention for how it got called isn't relevant. Furthermore, after setting everything up from the reset ISR we never want to return there either, so storing stuff like the return address would be a waste of memory. And so naked
makes perfect sense.
Depending on the CPU, we may or may not set up the stack pointer (SP) before using traditional C and local variables. On most microcontrollers, the SP is set manually from the reset ISR, which means that if we wrote the ISR in C we should not declare any local variables there or we might get really strange behavior with variable values disappearing out into the void etc.
For example if we have this code:
void reset_ISR (void)
{
asm("load_sp, 0x1000"); // some made-up ISA instruction setting the stack pointer
int x = 123;
...
}
Then the write to the local variable x
could very likely be rearranged by the compiler so that the value gets loaded onto the stack the first thing that happens when the ISR starts, before the code setting the SP is even executed - which would be nonsense. Meaning we'd either write the value 123
out in the wild and causing problems, or we simply lose that value and x
will contain gibberish at the point where SP does get set.
Therefore the rule of thumb for traditional microcontrollers is that from the function setting up the SP - very likely the reset ISR - we should not have any C code potentially storing things on the stack. No variable declarations, no function calls. Using CPU registers would be fine, but there's no way in C to tell the compiler to always use a register. Not even register
- that's just a recommendation. So often all code inside the ISR is either hand-crafted assembler or we simply call a different function and continue from there. The reset ISR typically calls the "C run-time" (CRT), which we can either design ourselves if we fancy, or otherwise use some provided library by the MCU compiler port.
The code posted is in fact typical CRT stuff: setting up the section often called .data
by filling it up with values from flash. The CRT should also set .bss
and in case of C++ call constructors of static storage duration objects.
However on ARM Cortex M cores specifically, the stack pointer is loaded from flash automatically by hardware at the point where the reset ISR is called, so we need not do that from software. In that specific case, declaring local variables would be safe since the SP is already set.
Googling around a bit suggests that the sloppily written magic number 0x40052008
is the address of the wdog register on for example MK20dx parts, a NXP Kinetis family of ARM Cortex M4 MCUs. Which will have the stack pointer pre-loaded.
ARM Cortex in general tend to have registers from the 0x40000000
address range. I don't remember how much of this that's specified by ARM CMSIS and how much that is up to the manufacturer to specify.