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".)

Reply via email to