Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GCC wrongly optimizes a pointer-equality test for a variable at a custom address

When optimizing, GCC seems to bypass wrongly a #define test.

First of all, I'm using my own link.ld linker script to provide a __foo__ symbol at the address 0xFFF (actually the lowest bits, not the whole address):

INCLUDE ./default.ld
__foo__ = 0xFFF;
  • NB: default.ld is the default linker script, obtained through with the gcc ... -Wl,-verbose command result

Then, a foo.c source file checks the __foo__'s address:

#include <stdint.h>
#include <stdio.h>

extern int __foo__;

#define EXPECTED_ADDR          ((intptr_t)(0xFFF))
#define FOO_ADDR               (((intptr_t)(&__foo__)) & EXPECTED_ADDR)
#define FOO_ADDR_IS_EXPECTED() (FOO_ADDR == EXPECTED_ADDR)

int main(void)
{
    printf("__foo__ at %p\n", &__foo__);
    printf("FOO_ADDR=0x%lx\n", FOO_ADDR);
    printf("EXPECTED_ADDR=0x%lx\n", EXPECTED_ADDR);
    if (FOO_ADDR_IS_EXPECTED())
    {
        printf("***Expected ***\n");
    }
    else
    {
        printf("### UNEXPECTED ###\n");
    }
    return 0;
}

I'm expecting the ***Expected *** print message, as FOO_ADDR_IS_EXPECTED() should be true.

Compiling with -O0 option, it executes as expected:

$ gcc -Wall -Wextra -Werror foo.c -O0 -o foo_O0 -T link.ld && ./foo_O0
__foo__ at 0x5603f4005fff
FOO_ADDR=0xfff
EXPECTED_ADDR=0xfff
***Expected ***

But with -O1 option, it does not:

$ gcc -Wall -Wextra -Werror foo.c -O1 -o foo_O1 -T link.ld && ./foo_O1
__foo__ at 0x5580202d0fff
FOO_ADDR=0xfff
EXPECTED_ADDR=0xfff
### UNEXPECTED ###

Here is the disassembly in -O0:

$ objdump -d ./foo_O0
...
0000000000001169 <main>:
...
    11b5:       b8 00 00 00 00          mov    $0x0,%eax
    11ba:       e8 b1 fe ff ff          callq  1070 <printf@plt>
    11bf:       48 8d 05 39 fe ff ff    lea    -0x1c7(%rip),%rax        # fff <__foo__>
    11c6:       25 ff 0f 00 00          and    $0xfff,%eax
    11cb:       48 3d ff 0f 00 00       cmp    $0xfff,%rax
    11d1:       75 0e                   jne    11e1 <main+0x78>
    11d3:       48 8d 3d 5e 0e 00 00    lea    0xe5e(%rip),%rdi        # 2038 <_IO_stdin_used+0x38>
    11da:       e8 81 fe ff ff          callq  1060 <puts@plt>
    11df:       eb 0c                   jmp    11ed <main+0x84>
    11e1:       48 8d 3d 60 0e 00 00    lea    0xe60(%rip),%rdi        # 2048 <_IO_stdin_used+0x48>
    11e8:       e8 73 fe ff ff          callq  1060 <puts@plt>
    11ed:       b8 00 00 00 00          mov    $0x0,%eax
...

I'm no expert, but I can see a jne condition and two calls of puts, that matches the if (FOO_ADDR_IS_EXPECTED()) statement.

Here is the disassembly in -O1:

$ objdump -d ./foo_O1
...
0000000000001169 <main>:
...
    11c2:       b8 00 00 00 00          mov    $0x0,%eax
    11c7:       e8 a4 fe ff ff          callq  1070 <__printf_chk@plt>
    11cc:       48 8d 3d 65 0e 00 00    lea    0xe65(%rip),%rdi        # 2038 <_IO_stdin_used+0x38>
    11d3:       e8 88 fe ff ff          callq  1060 <puts@plt>
...

This time, I see no condition and a straight call to puts (for the printf("### UNEXPECTED ###\n"); statement).

Why is the -O1 optimization modifying the behaviour? Why does it optimize FOO_ADDR_IS_EXPECTED() to be false ?

A bit of context to help your analysis:

$ uname -rm
5.4.0-73-generic x86_64
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Edit: Surprisingly, modifying the 0xFFF value to 0xABC changes the behaviour:

$ gcc -Wall -Wextra -Werror foo.c -O0 -o foo_O0 -T link.ld && ./foo_O0
__foo__ at 0x5653a7d4eabc
FOO_ADDR=0xabc
EXPECTED_ADDR=0xabc
***Expected ***

$ gcc -Wall -Wextra -Werror foo.c -O1 -o foo_O1 -T link.ld && ./foo_O1
__foo__ at 0x564323dddabc
FOO_ADDR=0xabc
EXPECTED_ADDR=0xabc
***Expected ***

As pointed out by Andrew Henle, the address alignment seems to matter: using 0xABF instead of 0xABC produces the same result than 0xFFF.

like image 435
Lrnt Gr Avatar asked Jul 15 '21 15:07

Lrnt Gr


2 Answers

As @AndrewHenle and @chux-ReinstateMonica suggested, this is an alignment problem.

The __foo__ variable type is int: its address should be 32bits aligned, meaning divisible by 4.
0xFFF is not divisible by 4, so the compiler assumes that it cannot be a valid int address: it optimizes the equality test to be false.

Changing __foo__'s type to char removes the alignment constraint, and the behaviour remains the same in -O0 and -O1:

// In foo.c
...
extern char __foo__;
...


$ gcc -Wall -Wextra -Werror foo.c -O0 -o foo_O0 -T link.ld && ./foo_O0
__foo__ at 0x55fbf8bedfff
FOO_ADDR=0xfff
EXPECTED_ADDR=0xfff
***Expected ***

$ gcc -Wall -Wextra -Werror foo.c -O1 -o foo_O1 -T link.ld && ./foo_O1
__foo__ at 0x5568d2debfff
FOO_ADDR=0xfff
EXPECTED_ADDR=0xfff
***Expected ***

like image 123
Lrnt Gr Avatar answered Oct 12 '22 11:10

Lrnt Gr


(intptr_t)(&__foo__) is undefined behavior (UB) when the address of __foo__ is invalid.

OP's __foo__ = 0xFFF; may violate alignment rules for int.

OP tried the less restrictive char with success.

// extern int __foo__;
extern char __foo__; 

Greater optimizations tends to take advantage of UB.
I use works with no optimization yet fails at high optimization as a hint that UB lurks somewhere. In this case the &__foo__ was invalid.

like image 45
chux - Reinstate Monica Avatar answered Oct 12 '22 12:10

chux - Reinstate Monica