I'm using C++ for a low-level firmware project for a microcontroller. Everything was fine for a while, with code size kept very small, until I introduced this line of code:
uint8_t index = __builtin_ctz(exti_set_mask);
auto &gpio_irq_info = irq_callback_table[index];
The issue here is that irq_callback_table
is an std::array<>
of size 16. exti_set_mask
is a uint16_t
that is probably being promoted to a uint32_t
(32-bit CPU architecture), so the compiler cannot be sure that the result of __builtin_ctz()
returns something smaller than 16. With the potential of an out-of-bounds access, the program code size blows up because of all the code added to support throwing of an exception.
Masking the result before using it as an index gets rid of all of that exception code:
uint8_t index = __builtin_ctz(exti_set_mask) & 0xf;
auto &gpio_irq_info = irq_callback_table[index];
Which brings the program code size back down to about 4 kB v.s. 90 kB!
I have these flags set via meson
:
# Specify global compiler flags.
add_project_arguments(
# Free standing environment (no OS, or stdlib)
'-ffreestanding', '-nostdlib', '-specs=nosys.specs',
# Enable linker garbage collection
'-ffunction-sections', '-fdata-sections',
# No dynamic memory allocation
'-fno-builtin-malloc', '-fno-builtin-calloc',
'-fno-builtin-realloc', '-fno-builtin-free',
# Debug optimization
'-Og',
language: ['cpp', 'c']
)
add_project_arguments(
'-std=c++20',
'-fno-exceptions',
'-fno-rtti',
language: ['cpp']
)
But the -fno-exceptions
flag does not seem to be enough to prevent emitting of exception code. Ideally, I would want any potential emission of exceptions (or malloc, etc) to show up as a warning or error instead. Any ideas on how to accomplish that?
EDIT: For those that want to compare the .elf file objdump output, here's the fully linked .elf with the masked index: https://pastebin.com/hSJgF3PW
And the .elf with the non-masked index:
The function where the two deviate:
Non-masked index:
08000d1c <(anonymous namespace)::CommonISRHandler(unsigned short)>:
void CommonISRHandler([[maybe_unused]] uint16_t exti_set_mask) {
8000d1c: b570 push {r4, r5, r6, lr}
8000d1e: 0005 movs r5, r0
while (exti_set_mask) {
8000d20: e011 b.n 8000d46 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x2a>
// Element access.
[[__nodiscard__]]
_GLIBCXX17_CONSTEXPR reference
operator[](size_type __n) noexcept
{
__glibcxx_requires_subscript(__n);
8000d22: 4b18 ldr r3, [pc, #96] @ (8000d84 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x68>)
8000d24: 4a18 ldr r2, [pc, #96] @ (8000d88 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x6c>)
8000d26: 4819 ldr r0, [pc, #100] @ (8000d8c <(anonymous namespace)::CommonISRHandler(unsigned short)+0x70>)
8000d28: 21ca movs r1, #202 @ 0xca
8000d2a: f000 f97d bl 8001028 <std::__glibcxx_assert_fail(char const*, int, char const*, char const*)>
EXTI->RPR1 &= (1 << index);
8000d2e: 4a18 ldr r2, [pc, #96] @ (8000d90 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x74>)
8000d30: 68d1 ldr r1, [r2, #12]
8000d32: 2301 movs r3, #1
8000d34: 40a3 lsls r3, r4
8000d36: 4019 ands r1, r3
8000d38: 60d1 str r1, [r2, #12]
EXTI->FPR1 &= (1 << index);
8000d3a: 6911 ldr r1, [r2, #16]
8000d3c: 4019 ands r1, r3
8000d3e: 6111 str r1, [r2, #16]
exti_set_mask &= ~(1 << index);
8000d40: 43db mvns r3, r3
8000d42: b21b sxth r3, r3
8000d44: 401d ands r5, r3
while (exti_set_mask) {
8000d46: 2d00 cmp r5, #0
8000d48: d01a beq.n 8000d80 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x64>
uint8_t index = __builtin_ctz(exti_set_mask);
8000d4a: 0028 movs r0, r5
8000d4c: f7ff fa40 bl 80001d0 <__ctzsi2>
auto &gpio_irq_info = irq_callback_table[index];
8000d50: 24ff movs r4, #255 @ 0xff
8000d52: 4004 ands r4, r0
8000d54: 2c0f cmp r4, #15
8000d56: d8e4 bhi.n 8000d22 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x6>
return stub_ptr != nullptr;
8000d58: 4b0e ldr r3, [pc, #56] @ (8000d94 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x78>)
8000d5a: 0062 lsls r2, r4, #1
8000d5c: 1912 adds r2, r2, r4
8000d5e: 0092 lsls r2, r2, #2
8000d60: 189b adds r3, r3, r2
8000d62: 689b ldr r3, [r3, #8]
if (gpio_irq_info.callback.IsValid()) {
8000d64: 2b00 cmp r3, #0
8000d66: d0e2 beq.n 8000d2e <(anonymous namespace)::CommonISRHandler(unsigned short)+0x12>
gpio_irq_info.callback(gpio_irq_info.pin);
8000d68: 4a0a ldr r2, [pc, #40] @ (8000d94 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x78>)
8000d6a: 0060 lsls r0, r4, #1
8000d6c: 1901 adds r1, r0, r4
8000d6e: 0089 lsls r1, r1, #2
8000d70: 5c89 ldrb r1, [r1, r2]
8000d72: 1900 adds r0, r0, r4
8000d74: 0080 lsls r0, r0, #2
8000d76: 1880 adds r0, r0, r2
8000d78: 3004 adds r0, #4
8000d7a: f7ff ffbb bl 8000cf4 <rtlib::Delegate<void (mcu::stm32g070::Gpio::GpioId)>::operator()(mcu::stm32g070::Gpio::GpioId) const>
8000d7e: e7d6 b.n 8000d2e <(anonymous namespace)::CommonISRHandler(unsigned short)+0x12>
}
8000d80: bd70 pop {r4, r5, r6, pc}
8000d82: 46c0 nop @ (mov r8, r8)
8000d84: 08013ccc .word 0x08013ccc
8000d88: 08013ce0 .word 0x08013ce0
8000d8c: 08013dc4 .word 0x08013dc4
8000d90: 40021800 .word 0x40021800
8000d94: 20000734 .word 0x20000734
Masked index:
08000710 <(anonymous namespace)::CommonISRHandler(unsigned short)>:
void CommonISRHandler([[maybe_unused]] uint16_t exti_set_mask) {
8000710: b570 push {r4, r5, r6, lr}
8000712: 0005 movs r5, r0
while (exti_set_mask) {
8000714: e00b b.n 800072e <(anonymous namespace)::CommonISRHandler(unsigned short)+0x1e>
EXTI->RPR1 &= (1 << index);
8000716: 4a14 ldr r2, [pc, #80] @ (8000768 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x58>)
8000718: 68d1 ldr r1, [r2, #12]
800071a: 2301 movs r3, #1
800071c: 40a3 lsls r3, r4
800071e: 4019 ands r1, r3
8000720: 60d1 str r1, [r2, #12]
EXTI->FPR1 &= (1 << index);
8000722: 6911 ldr r1, [r2, #16]
8000724: 4019 ands r1, r3
8000726: 6111 str r1, [r2, #16]
exti_set_mask &= ~(1 << index);
8000728: 43db mvns r3, r3
800072a: b21b sxth r3, r3
800072c: 401d ands r5, r3
while (exti_set_mask) {
800072e: 2d00 cmp r5, #0
8000730: d018 beq.n 8000764 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x54>
uint8_t index = __builtin_ctz(exti_set_mask) & 0xf;
8000732: 0028 movs r0, r5
8000734: f7ff fcc0 bl 80000b8 <__ctzsi2>
8000738: 240f movs r4, #15
800073a: 4004 ands r4, r0
return stub_ptr != nullptr;
800073c: 4b0b ldr r3, [pc, #44] @ (800076c <(anonymous namespace)::CommonISRHandler(unsigned short)+0x5c>)
800073e: 0062 lsls r2, r4, #1
8000740: 1912 adds r2, r2, r4
8000742: 0092 lsls r2, r2, #2
8000744: 189b adds r3, r3, r2
8000746: 689b ldr r3, [r3, #8]
if (gpio_irq_info.callback.IsValid()) {
8000748: 2b00 cmp r3, #0
800074a: d0e4 beq.n 8000716 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x6>
gpio_irq_info.callback(gpio_irq_info.pin);
800074c: 4a07 ldr r2, [pc, #28] @ (800076c <(anonymous namespace)::CommonISRHandler(unsigned short)+0x5c>)
800074e: 0060 lsls r0, r4, #1
8000750: 1901 adds r1, r0, r4
8000752: 0089 lsls r1, r1, #2
8000754: 5c89 ldrb r1, [r1, r2]
8000756: 1900 adds r0, r0, r4
8000758: 0080 lsls r0, r0, #2
800075a: 1880 adds r0, r0, r2
800075c: 3004 adds r0, #4
800075e: f7ff ffc3 bl 80006e8 <rtlib::Delegate<void (mcu::stm32g070::Gpio::GpioId)>::operator()(mcu::stm32g070::Gpio::GpioId) const>
8000762: e7d8 b.n 8000716 <(anonymous namespace)::CommonISRHandler(unsigned short)+0x6>
}
8000764: bd70 pop {r4, r5, r6, pc}
8000766: 46c0 nop @ (mov r8, r8)
8000768: 40021800 .word 0x40021800
800076c: 2000005c .word 0x2000005c
EDIT #2, Some good comments so far, thank you. My reason for believing that exceptions are being pulled in is because of the presence of many functions in the non-masked objdump that are not present in the masked objdump output (grep -i exception
, grep -i throw
):
08000438 <_Unwind_RaiseException>:
800045e: f009 f831 bl 80094c4 <__gnu_Unwind_RaiseException>
8001764: f007 fb0e bl 8008d84 <__cxa_current_exception_type>
080089d8 <__cxxabiv1::__is_gxx_exception_class(char*)>:
80089e0: d000 beq.n 80089e4 <__cxxabiv1::__is_gxx_exception_class(char*)+0xc>
80089e8: d1fb bne.n 80089e2 <__cxxabiv1::__is_gxx_exception_class(char*)+0xa>
80089ee: d1f8 bne.n 80089e2 <__cxxabiv1::__is_gxx_exception_class(char*)+0xa>
80089f4: d1f5 bne.n 80089e2 <__cxxabiv1::__is_gxx_exception_class(char*)+0xa>
80089fa: d1f2 bne.n 80089e2 <__cxxabiv1::__is_gxx_exception_class(char*)+0xa>
8008a00: d1ef bne.n 80089e2 <__cxxabiv1::__is_gxx_exception_class(char*)+0xa>
8008a06: d1ec bne.n 80089e2 <__cxxabiv1::__is_gxx_exception_class(char*)+0xa>
8008a12: e7e6 b.n 80089e2 <__cxxabiv1::__is_gxx_exception_class(char*)+0xa>
8008a20: f7ff ffda bl 80089d8 <__cxxabiv1::__is_gxx_exception_class(char*)>
8008a44: f7ff ffc8 bl 80089d8 <__cxxabiv1::__is_gxx_exception_class(char*)>
8008b02: f000 fad3 bl 80090ac <__cxa_allocate_exception>
8008bc0: f000 fce6 bl 8009590 <_Unwind_DeleteException>
8008c2c: f000 fcb0 bl 8009590 <_Unwind_DeleteException>
08008c34 <std::bad_exception::~bad_exception()>:
08008c38 <transaction clone for std::bad_exception::what() const>:
8008c38: 4800 ldr r0, [pc, #0] @ (8008c3c <transaction clone for std::bad_exception::what() const+0x4>)
08008c40 <std::bad_exception::~bad_exception()>:
08008c60 <__gxx_exception_cleanup(_Unwind_Reason_Code, _Unwind_Control_Block*)>:
8008c64: d812 bhi.n 8008c8c <__gxx_exception_cleanup(_Unwind_Reason_Code, _Unwind_Control_Block*)+0x2c>
8008c72: d000 beq.n 8008c76 <__gxx_exception_cleanup(_Unwind_Reason_Code, _Unwind_Control_Block*)+0x16>
8008c7e: d001 beq.n 8008c84 <__gxx_exception_cleanup(_Unwind_Reason_Code, _Unwind_Control_Block*)+0x24>
8008c86: f000 fa27 bl 80090d8 <__cxa_free_exception>
8008c8a: e7f3 b.n 8008c74 <__gxx_exception_cleanup(_Unwind_Reason_Code, _Unwind_Control_Block*)+0x14>
08008c94 <__cxa_init_primary_exception>:
8008cd0: 4b02 ldr r3, [pc, #8] @ (8008cdc <__cxa_init_primary_exception+0x48>)
8008cf8: f7ff ffcc bl 8008c94 <__cxa_init_primary_exception>
8008d06: f7f7 fb97 bl 8000438 <_Unwind_RaiseException>
08008d84 <__cxa_current_exception_type>:
8008d8e: d004 beq.n 8008d9a <__cxa_current_exception_type+0x16>
8008d96: d001 beq.n 8008d9c <__cxa_current_exception_type+0x18>
8008da0: e7fa b.n 8008d98 <__cxa_current_exception_type+0x14>
080090ac <__cxa_allocate_exception>:
80090b8: d007 beq.n 80090ca <__cxa_allocate_exception+0x1e>
80090d2: d1f2 bne.n 80090ba <__cxa_allocate_exception+0xe>
080090d8 <__cxa_free_exception>:
80090d8: 4b07 ldr r3, [pc, #28] @ (80090f8 <__cxa_free_exception+0x20>)
80090e6: d201 bcs.n 80090ec <__cxa_free_exception+0x14>
80090ea: d302 bcc.n 80090f2 <__cxa_free_exception+0x1a>
80090f6: e7fb b.n 80090f0 <__cxa_free_exception+0x18>
080094c4 <__gnu_Unwind_RaiseException>:
80094e0: e006 b.n 80094f0 <__gnu_Unwind_RaiseException+0x2c>
80094ee: d108 bne.n 8009502 <__gnu_Unwind_RaiseException+0x3e>
80094fa: d0f2 beq.n 80094e2 <__gnu_Unwind_RaiseException+0x1e>
800950a: d1f7 bne.n 80094fc <__gnu_Unwind_RaiseException+0x38>
8009584: f7ff ff9e bl 80094c4 <__gnu_Unwind_RaiseException>
08009590 <_Unwind_DeleteException>:
8009598: d001 beq.n 800959e <_Unwind_DeleteException+0xe>
08000498 <_Unwind_Resume_or_Rethrow>:
80004be: f009 f857 bl 8009570 <__gnu_Unwind_Resume_or_Rethrow>
80017c6: f007 faa5 bl 8008d14 <__cxa_rethrow>
8008ad0: f000 f920 bl 8008d14 <__cxa_rethrow>
8008b0e: f000 f8e7 bl 8008ce0 <__cxa_throw>
08008ce0 <__cxa_throw>:
08008d14 <__cxa_rethrow>:
8008d24: d00c beq.n 8008d40 <__cxa_rethrow+0x2c>
8008d2c: d00a beq.n 8008d44 <__cxa_rethrow+0x30>
8008d36: f7f7 fbaf bl 8000498 <_Unwind_Resume_or_Rethrow>
8008d4a: d1f0 bne.n 8008d2e <__cxa_rethrow+0x1a>
8008d52: d1ec bne.n 8008d2e <__cxa_rethrow+0x1a>
8008d5a: d1e8 bne.n 8008d2e <__cxa_rethrow+0x1a>
8008d62: d1e4 bne.n 8008d2e <__cxa_rethrow+0x1a>
8008d6a: d1e0 bne.n 8008d2e <__cxa_rethrow+0x1a>
8008d72: d1dc bne.n 8008d2e <__cxa_rethrow+0x1a>
8008d7a: d8d8 bhi.n 8008d2e <__cxa_rethrow+0x1a>
8008d82: e7d6 b.n 8008d32 <__cxa_rethrow+0x1e>
08009570 <__gnu_Unwind_Resume_or_Rethrow>:
8009576: d005 beq.n 8009584 <__gnu_Unwind_Resume_or_Rethrow+0x14>
8009588: e7fb b.n 8009582 <__gnu_Unwind_Resume_or_Rethrow+0x12>
In recent years, safety has become a nuance in SW development world. As a result, compiler and library designers have shifted toward hardening programs against usual sources of UB. As part of their attemps recent compilers have hardening flags enabled by default in debug builds. Hardening flags eventually affect the behavior of indexing operators for std::array
, std::vector
and contagious ranges in general. As a result, index operator(operator[]
) for the afformentioned classes practically become equivalent to at
method.
In order to disable the adverse effects of hardening on performance, the easiest solution is to compile in release mode(force define _NDRBUG
via compiler flags). But this totally disables runtime debuggers and traps. The better choice is to keep the debug mode, but dissable the harmful hardening options.
_GLIBCXX_ASSERTIONS
is a macro that enables selective assertions in GNU libraries and related frameworks. As part of code hardening, developers can opt to define it via the D
switch(as -D_GLIBCXX_ASSERTIONS
). But as mentioned before, that's the default in debug builds. Thus, in case necessary definition of that macro can be suppressed via the U
compiler switch(-U_GLIBCXX_ASSERTIONS
). Tracing the definitions in IDE, we can see that _GLIBCXX_ASSERTIONS
macro conditionally controls bounds checks in gnu implementation of std::array::operator[]
.
hardened
compiler option is a more recent fortification in newer versions of GNU and CLANG platforms' debug mode compilation. The default in debug mode is the f
switch(-fhardened
), and defines _GLIBCXX_ASSERTIONS
along with some more hardening options in debug builds. Disabling it with fno-
switch (-fno-hardened
), eventually suppresses the macro too.
Disabling the hardening options eventually removes the bounds-checking logic from operator[]
invocations and optimizes the exra dependency out, resulting in binary size shrinking. For further reading explor the GNU gcc docs, or search the keywords mentioned in this response.