ARM Cortex-M Interrupt Handler Misdirection in C++ Due to Name Mangling
When working with ARM Cortex-M processors, particularly when implementing interrupt handlers in C++, a common issue arises where the interrupt service routine (ISR) is not correctly linked to the intended function. This misdirection often results in the processor executing a default handler or an infinite loop, typically indicated by a B . (branch to self) instruction in the disassembly. The root cause of this behavior is frequently related to C++ name mangling, which alters the function names during compilation, causing the linker to fail to resolve the correct ISR address. This issue is exacerbated when ISRs are defined in C++ files without proper linkage specifications, leading to a mismatch between the expected symbol names in the vector table and the actual mangled names generated by the C++ compiler.
C++ Name Mangling and Its Impact on Interrupt Handlers
C++ compilers perform name mangling to support function overloading and type-safe linking. Name mangling encodes function names with additional information about their parameter types and return values, resulting in unique symbols for each overloaded function. For example, a function void EINT0_IRQHandler() might be mangled to a symbol like _Z15EINT0_IRQHandlerv in the object file. While this is beneficial for C++ features, it becomes problematic when interfacing with low-level hardware constructs like interrupt vectors, which expect unmangled C-style function names.
In the ARM Cortex-M architecture, the vector table contains addresses of ISRs, and these addresses must correspond to the exact symbols defined in the code. If the ISR is defined in a C++ file without explicit C linkage, the mangled name will not match the expected symbol in the vector table. Consequently, the linker cannot resolve the correct address, and the processor defaults to the weak-defined handler, often leading to an infinite loop.
Linker Behavior and Weak Symbol Resolution
The ARM Cortex-M startup code typically defines weak symbols for all interrupt handlers. These weak symbols act as placeholders, allowing the linker to override them with user-defined implementations if available. For example, the startup code might include:
EXPORT EINT0_IRQHandler [WEAK]
EINT0_IRQHandler
B .
Here, EINT0_IRQHandler is defined as a weak symbol, meaning it can be overridden by a strong symbol with the same name in the user code. However, if the user-defined ISR is mangled due to C++ compilation, the linker cannot find a matching strong symbol, and the weak definition remains in place. This results in the processor branching to the default handler (B .) instead of the intended ISR.
Differences Between C and C++ Compilation
When ISRs are defined in a C file, the compiler does not mangle the function names, and the symbols match exactly with those expected by the vector table. For example, a C function void EINT0_IRQHandler() will produce a symbol EINT0_IRQHandler in the object file, which the linker can resolve correctly. However, in C++, the same function will produce a mangled symbol unless explicitly declared with C linkage using extern "C".
This discrepancy explains why moving the ISR definitions to a C file resolves the issue: the symbols remain unmangled and match the expected names in the vector table. In contrast, when the ISRs are defined in a C++ file without extern "C", the mangled symbols do not match, leading to the misdirection problem.
Implementing Correct C++ Interrupt Handlers with extern "C"
To ensure that interrupt handlers are correctly linked in C++ projects, the ISRs must be declared with C linkage using the extern "C" specifier. This prevents name mangling and ensures that the function names match the symbols expected by the vector table. The following steps outline the correct implementation:
Declaring ISRs with extern "C"
When defining ISRs in a C++ file, wrap the function declarations in an extern "C" block to enforce C linkage. For example:
extern "C" {
void EINT0_IRQHandler(void) {
LPC_SC->EXTINT &= (1 << 0); // Clear interrupt flag
}
void EINT1_IRQHandler(void) {
LPC_SC->EXTINT &= (1 << 1); // Clear interrupt flag
}
void EINT2_IRQHandler(void) {
LPC_SC->EXTINT &= (1 << 2); // Clear interrupt flag
}
}
This ensures that the symbols EINT0_IRQHandler, EINT1_IRQHandler, and EINT2_IRQHandler are not mangled and match the names expected by the vector table.
Verifying Symbol Names in the Object File
After compiling the code, verify the symbol names in the object file using tools like objdump or nm. For example, running nm on the object file should show unmangled symbols for the ISRs:
00000000 T EINT0_IRQHandler
00000010 T EINT1_IRQHandler
00000020 T EINT2_IRQHandler
If the symbols appear mangled, double-check the extern "C" declaration and ensure it is correctly applied.
Removing Weak Attributes from Startup Code
To diagnose linkage issues, consider removing the [WEAK] attribute from the ISR exports in the startup code. This forces the linker to generate an error if it cannot find a matching strong symbol for the ISR. For example, modify the startup code as follows:
EXPORT EINT0_IRQHandler
EINT0_IRQHandler
B .
If the ISR is not correctly defined with C linkage, the linker will produce an error, indicating that the symbol is unresolved. This approach helps identify issues early in the development process.
Ensuring Consistent Function Signatures
When using extern "C", ensure that the function signatures match exactly between the ISR definitions and the vector table. Any mismatch in function names or parameters can lead to unresolved symbols or runtime errors. For example, the following ISR definition would cause a mismatch:
extern "C" void EINT0_IRQHandler(int param) { // Incorrect signature
LPC_SC->EXTINT &= (1 << 0);
}
The correct signature should match the expected prototype:
extern "C" void EINT0_IRQHandler(void) {
LPC_SC->EXTINT &= (1 << 0);
}
Debugging Techniques for Interrupt Handler Issues
When debugging interrupt handler misdirection, use the following techniques to identify and resolve issues:
-
Disassembly Analysis: Examine the disassembly of the vector table and the ISR functions to verify that the addresses match. Look for the
B .instruction, which indicates a default handler. -
Linker Map File: Generate a linker map file to inspect the symbol addresses and ensure that the ISR symbols are correctly resolved.
-
Breakpoints and Stepping: Set breakpoints at the ISR entry points and step through the code to verify that the correct handler is executed.
-
Symbol Verification: Use tools like
objdumpornmto verify that the ISR symbols are unmangled and match the expected names.
Example Implementation
The following example demonstrates the correct implementation of interrupt handlers in a C++ project:
#include "LPC17xx.h"
extern "C" {
void EINT0_IRQHandler(void) {
LPC_SC->EXTINT &= (1 << 0); // Clear interrupt flag
}
void EINT1_IRQHandler(void) {
LPC_SC->EXTINT &= (1 << 1); // Clear interrupt flag
}
void EINT2_IRQHandler(void) {
LPC_SC->EXTINT &= (1 << 2); // Clear interrupt flag
}
}
int main(void) {
SystemInit();
ButtonInit();
while (1);
}
In this implementation, the ISRs are declared with extern "C" to prevent name mangling, ensuring that the symbols match those expected by the vector table. The main function initializes the system and enters an infinite loop, waiting for interrupts to occur.
Conclusion
Interrupt handler misdirection in C++ projects on ARM Cortex-M processors is a common issue caused by C++ name mangling. By declaring ISRs with extern "C", developers can ensure that the function names match the symbols expected by the vector table, enabling correct interrupt handling. Additionally, verifying symbol names, removing weak attributes, and using debugging techniques can help identify and resolve linkage issues. Following these best practices ensures reliable and efficient interrupt handling in embedded systems.