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;
gcc ... -Wl,-verbose
command resultThen, 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
.
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 ***
(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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With