https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116946
Bug ID: 116946 Summary: LTO gets confused about code path and incorrectly triggers build-time assertion Product: gcc Version: 13.2.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: lto Assignee: unassigned at gcc dot gnu.org Reporter: jwerner at chromium dot org Target Milestone: --- We're using `__builtin_constant_p()` in combination with generating a function call to an undefined reference as a build-time assertion mechanism. This works perfectly fine without LTO (if the value is build-time constant it will go down this path, if it is not it will generate a normal runtime `assert()` call). When enabling LTO it seems that more values are `__builtin_constant_p()`, which is fine, but we have found a test case where LTO seems to get confused about the code flow and hits an assertion that the code wouldn't actually reach. I've reduced the problem to a minimal test case across 3 files. Taking out any more than this seems to make the issue disappear. (The original problem comes from the coreboot project and this file: https://github.com/coreboot/coreboot/blob/main/src/soc/mediatek/mt8186/mt6366.c ) ---------------------- test1.c ----------------------- int build_time_assertion_failed(void) __attribute__((noreturn)); #define build_time_assert(x) \ (__builtin_constant_p(x) ? ((x) ? 1 : build_time_assertion_failed()) : 0) #define assert(x) { \ if (!build_time_assert(x) && !(x)) { \ do_thing(2); \ } \ } static void do_thing(unsigned long x) { *(volatile unsigned long *)0x12345678ul = x; } static void not_a_problem(unsigned int value) { do_thing(value); } static void never_called(unsigned int value) { assert(value <= 3); do_thing(value); } void inner_func(int id, unsigned int value) { switch (id) { case 2: never_called(value); break; case 3: not_a_problem(value); break; } } ---------------------- test2.c ----------------------- void wrapper_func(int id, unsigned int value); void _start(void) { wrapper_func(3, 5); wrapper_func(3, 5); } ---------------------- test3.c ----------------------- void inner_func(int id, unsigned int value); static int load_bearing_switch(int id) { switch (id) { case 2: return 2; case 3: return 3; } } void wrapper_func(int id, unsigned int value) { inner_func(load_bearing_switch(id), value); } ------------------------------------------------------ I am building this with an AArch64 GCC 13.2.0 cross-compiler like this: aarch64-elf-gcc -o test1.o -c test1.c -Os -flto aarch64-elf-gcc -o test2.o -c test2.c -Os -flto aarch64-elf-gcc -o test3.o -c test3.c -Os -flto aarch64-elf-gcc -ffreestanding -nostdlib -flto -o test test1.o test2.o test3.o And getting this output: aarch64-elf/bin/ld: /tmp/cc1rtFH6.ltrans0.ltrans.o: in function `inner_func': <artificial>:(.text+0x38): undefined reference to `build_time_assertion_failed' collect2: error: ld returned 1 exit status As you can easily see from the code, the call graph should be _start() -> wrapper_func(3, 5) -> inner_func(3, 5) -> not_a_problem(5) -> wrapper_func(3, 5) -> inner_func(3, 5) -> not_a_problem(5) The never_called() function never gets called because the `id` constant passed in from _start() is 3, not 2. Without LTO this builds fine because __builtin_constant_p(value) evaluates to false. With LTO it seems that the compiler recognizes that the value is a constant when passed in from _start(), but it doesn't realize that `id` is also a constant and forces the switch statement into a different path. (FWIW, if `id` was not a constant then this would still be a problem. I think with LTO the semantics of __builtin_constant_p(x) may need to be expanded to "x is compile-time constant AND the compiler can guarantee that the code path from the point where x is defined to the __builtin_constant_p() call is actually taken".)