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

Viewed 223

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.

4 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 ***

(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.

Unless optimizations are completely disabled, both gcc and clang are prone to behave nonsensically if code performs a comparison between an address which is based upon an external symbol and an address which is not based upon that same symbol. The issue extends beyond treating such comparisons as yielding an Unspecified Result, and may result in code behavior which is consistent neither with the comparison yielding true nor with it yielding false.

extern int x[1],y[1];
int test(int *p)
{
    y[0] = 1;
    if (p == x+1)
        *p = 2;
    return y[0];
}

Both clang and gcc will generate code that will, if test is passed the address of y and it happens to immediately follow x, set y[0] to 2 but then return 1. Such behavior has been reported years ago, but I don't know of any options other than -O0 to make the compilers process such a function in a fashion consistent with the Standard.

We know that -O will produce the "behavior".

But, -O* turns on a number of finer grain -f optimization options.

I was curious as to which -f actually was to "blame".

A list of -f options can be found at: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

The specific optimization that produces the behavior is:

-ftree-bit-ccp

The documentation for it is:

Perform sparse conditional bit constant propagation on trees and propagate pointer alignment information. This pass only operates on local scalar variables and is enabled by default at -O1 and higher, except for -Og. It requires that -ftree-ccp is enabled.


Starting out, I didn't know which -f option was doing the optimization. So, I decided to apply the options one by one and rebuild/rerun the test program.

Being lazy, I didn't want to do this by hand. I wrote a [perl] script to pull the above .html file, parse it, and apply the individual -f options, one by one.

Side note: Ironically, this probably took longer than hand editing the .html file to create a script, but it was fun ... And, there have been times when I've wanted to know which -f option was doing a given optimization in my own code, but I always punted.

The script is a bit crude, but it could probably be adapted and reused for other test programs in the future.

#!/usr/bin/perl
# gccblame -- decide which -f option causes issues
#
# options:
#   "-A" -- specify __foo__ address (DEFAULT: FFF)
#   "-arr" -- define __foo__ as array
#   "-clean" -- clean generated files
#   "-doc" -- show documentation
#   "-f" -- preclean and force reload
#   "-no" -- apply -fno-foobar instead of -ffoobar
#   "-T<type>" -- specify __foo__ type (DEFAULT: int)
#   "-url" -- (DFT: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html)

master(@ARGV);
exit(0);

# master -- master control
sub master
{
    my(@argv) = @_;

    # get command line options
    optdcd(\@argv,
        qw(opt_A opt_arr opt_clean opt_doc opt_f opt_no opt_T opt_url));
    $opt_T //= "int";
    $opt_A //= "FFF";
    $opt_A =~ s/^0x//;
    $opt_A = "0x" . $opt_A;
    $opt_arr = $opt_arr ? "[]" : "";
    $opt_url //= "https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html";

    $root = "fopturl";
    $fopt_ifile = clean("$root.html");
    $fopt_ofile = clean("$root.txt");

    $nul_c = clean("nul.c");
    $dftlink = clean("./default.ld");

    # compiled output
    clean("foo.o");
    clean("foo");

    $tmp = clean("tmp.txt");

    # clean generated files
    if ($opt_clean or $opt_f) {
        # get more files to clean
        sysall(0);

        foreach $file (sort(keys(%clean))) {
            if (-e $file) {
                printf("cleaning %s\n",$file);
                unlink($file);
            }
        }

        exit(0) if ($opt_clean);
    }

    # get the options documentation from the net
    $fopturl = fopturl();

    # parse it
    foptparse(@$fopturl);

    # create all static files
    sysall(1);

    # create linker scripts and test source file
    dftlink();

    ###exit(0);

    # start with just the -O option
    dopgm($opt_no ? "-O3" : "-Og");

    # test all -f options
    dolist();

    printf("\n");
    docstat()
        if ($opt_doc);
    printf("all options passed!\n");
}

# optdcd -- decode command line options
sub optdcd
{
    my(@syms) = @_;
    my($argv);
    my($arg);
    my($sym,$val,$match);

    $argv = shift(@syms);

    # get options
    while (@$argv > 0) {
        $arg = $argv->[0];
        last unless ($arg =~ /^-/);

        shift(@$argv);

        $match = 0;
        foreach $sym (@syms) {
            $opt = $sym;
            $opt =~ s/^opt_/-/;

            if ($arg =~ /^$opt(.*)$/) {
                $val = $1;

                $val =~ s/^=//;
                $val = 1
                    if ($val eq "");

                $$sym = $val;
                $match = 1;
                last;
            }
        }

        sysfault("optdcd: unknown option -- '%s'\n",$arg)
            unless ($match);
    }
}

# clean -- add to clean list
sub clean
{
    my($file) = @_;
    my($self,$tail);

    $self = filetail($0);
    $tail = filetail($file);

    sysfault("clean: attempt to clean script -- '%s'\n",$tail)
        if ($tail eq $self);

    $clean{$tail} = 1;

    $file;
}

# dftlink -- get default linker script
sub dftlink
{
    my($xfdst);
    my($buf,$body);
    my($grabflg);
    my($lno);

    # build it to get default link file
    $code = doexec("gcc","-o","/dev/null",$nul_c,
        "-v","-Wl,--verbose",">$dftlink","2>&1");
    exit(1) if ($code);

    # get all messages
    $body = fileload($dftlink);

    # split off the linker script from all the verbose messages
    open($xfdst,">$dftlink");
    while (1) {
        $buf = shift(@$body);
        last unless (defined($buf));

        if ($grabflg) {
            last if ($buf =~ /^=======/);
            print($xfdst $buf,"\n");
            ++$lno;
        }

        # get starting section and skip the "=======" line following
        if ($buf =~ /^using internal linker script:/) {
            $grabflg = 1;
            shift(@$body);
        }
    }
    close($xfdst);

    printf("dftlink: got %d lines\n",$lno);
    exit(1) if ($lno <= 0);
}

# sysall -- extract all files
sub sysall
{
    my($goflg) = @_;
    my($xfsrc,$xfdst,$buf);
    my($otail,$ofile);

    $xfsrc = sysdata("gccblame");

    while ($buf = <$xfsrc>) {
        chomp($buf);

        # apply variable substitution
        $buf = subenv($buf);

        # start new file
        if ($buf =~ /^%\s+(\S+)$/) {
            $otail = $1;

            # add to list of files to clean
            clean($otail);
            next unless ($goflg);

            close($xfdst)
                if (defined($ofile));
            $ofile = $otail;

            printf("dftlink: creating %s ...\n",$ofile);
            open($xfdst,">$ofile") or
                sysfault("dftlink: unable to open '%s' -- $!\n",$ofile);

            next;
        }

        print($xfdst $buf,"\n")
            if (defined($ofile));
    }

    close($xfdst)
        if (defined($ofile));
}

# fileload -- load up file contents
sub fileload
{
    my($file) = @_;
    my($xf);
    my(@data);

    open($xf,"<$file") or
        sysfault("fileload: unable to open '%s' -- $!\n",$file);

    @data = <$xf>;
    chomp(@data);

    close($xf);

    \@data;
}

# fopturl -- fetch and convert remote documentation file
sub fopturl
{
    my($sti,$sto);
    my($data);

    # get GCC's optimization options from remote server
    $sti = _fopturl($fopt_ifile,"curl","-s",$opt_url);

    # convert it to text
    $sto = _fopturl($sti,$fopt_ofile,"html2text",$fopt_ifile);

    # read in the semi-clean data
    $data = fileload($fopt_ofile);

    $data;
}

# _fopturl -- grab data
sub _fopturl
{
    my(@argv) = @_;
    my($sti);
    my($ofile);
    my($sto);

    $ofile = shift(@argv);
    if (ref($ofile)) {
        $sti = $ofile;
        $ofile = shift(@argv);
    }
    else {
        $sti = {};
    }

    while (1) {
        $sto = sysstat($ofile);
        if (ref($sto)) {
            last if ($sto->{st_mtime} >= $sti->{st_mtime});
        }

        $code = doexec(@argv,">$tmp");
        exit(1) if ($code);

        msgv("fopturl: RENAME",$tmp,$ofile);
        rename($tmp,$ofile) or
            sysfault("fopturl: unable to rename '%s' to '%s' -- $!\n",
                $tmp,$ofile);
    }

    $sto;
}

# foptparse -- parse and cross reference the options
sub foptparse
{
    local(@argv) = @_;
    local($buf);
    local($env);
    my(%uniq);

    $env = "xO";

    while (1) {
        $buf = shift(@argv);
        last unless (defined($buf));

        if ($buf =~ /^`-f/) {
            $env = "xB";
        }

        # initial are:
        #   -ffoo -fbar
        if (($env eq "xO") and ($buf =~ /^\s*-f/)) {
            _foptparse(0);
            next;
        }

        # later we have:
        # `-ffoo`
        # doclines
        if (($env eq "xB") and ($buf =~ /^`-f/)) {
            _foptparse(1);
            next;
        }

        if ($buf =~ /^`-O/) {
            printf("foptparse: OLVL %s\n",$buf);
            next;
        }
    }

    xrefuniq("xO","xB");
    xrefuniq("xB","xO");

    foreach $opt (@xO,@xB) {
        next if ($uniq{$opt});
        $uniq{$opt} = 1;
        push(@foptall,$opt);
    }
}

sub _foptparse
{
    my($fix) = @_;
    my($docsym,$docptr);

    $buf =~ s/^\s+//;
    $buf =~ s/\s+$//;

    if ($fix) {
        $buf =~ s/`//g;
    }

    printf("foptparse: %s %s\n",$env,$buf);

    @rhs = split(" ",$buf);
    foreach $buf (@rhs) {
        next if ($env->{$buf});
        $env->{$buf} = 1;

        push(@$env,$buf);

        $docsym //= $buf;
    }

    # get documentation for option
    if ($fix) {
        $docptr = [];
        $foptdoc{$docsym} = $docptr;

        while (1) {
            $buf = shift(@argv);
            last unless (defined($buf));

            # put back _next_ option
            if ($buf =~ /^`/) {
                unshift(@argv,$buf);
                last;
            }

            push(@$docptr,$buf);
        }

        # strip leading whitespace lines
        while (@$docptr > 0) {
            $buf = $docptr->[0];
            last if ($buf =~ /\S/);
            shift(@$docptr);
        }

        # strip trailing whitespace lines
        while (@$docptr > 0) {
            $buf = $docptr->[$#$docptr];
            last if ($buf =~ /\S/);
            pop(@$docptr);
        }
    }
}

# xrefuniq -- get unique set of options
sub xrefuniq
{
    my($envlhs,$envrhs) = @_;
    my($sym,$lhs,$rhs);

    while (($sym,$lhs) = each(%$envlhs)) {
        $rhs = $envrhs->{$sym};
        next if ($rhs);
        printf("xrefuniq: %s %s\n",$envlhs,$sym);
    }
}

# dolist -- process all -f options
sub dolist
{
    my($foptnew);

    foreach $foptnew (@foptall) {
        dopgm($foptnew);
    }
}

# dopgm -- compile, link, and run the "foo" program
sub dopgm
{
    my($foptnew) = @_;
    my($code);

    $foptnew =~ s/^-f/-fno-/
        if ($opt_no);

    printf("\n");
    printf("NEWOPT: %s\n",$foptnew);

    # show documentation
    docshow($foptnew);

    {
        # compile to .o -- this proves that the compiler is changing things
        # and _not_ some link time optimization
        $code = doexec(qw(gcc -Wall -Wextra -Werror foo.c -c),
            @foptlhs,$foptnew);

        # the source should always compile cleanly -- if not, the option is
        # just bad/unknown
        if ($code) {
            printf("IGNORING: %s\n",$foptnew);
            ###pop(@foptlhs);
            last;
        }
        push(@foptlhs,$foptnew);

        # build the final program
        $code = doexec(qw(gcc -Wall -Wextra -Werror foo.o -o foo),
            "-T","link.ld");
        exit(1) if ($code);

        # run the program
        $code = doexec("./foo");

        # if it runs cleanly, we have the bad option
        if ($opt_no) {
            $code = ! $code;
        }

        if ($code) {
            printf("\n");
            printf("BADOPT: %s\n",$foptnew);
            exit(1);
        }
    }
}

# docshow -- show documentation
sub docshow
{
    my($foptnew) = @_;
    my($docptr,$docrhs,$doclhs,$doclen);
    my(@opt);

    {
        last unless ($opt_doc);

        $docptr = $foptdoc{$foptnew};
        last unless (ref($docptr));

        push(@opt,"-pre=#","#");

        foreach $docrhs (@$docptr) {
            $doclen = length($docrhs);

            # remember max length
            if ($doclen > $docmax) {
                $docmax = $doclen;
                printf("NEWMAX: %d\n",$docmax);
            }

            $dochisto[$doclen] += 1;

            if ($doclen > 78) {
                msgv(@opt,split(" ",$docrhs));
            }
            else {
                msgv(@opt,$docrhs);
            }
        }
    }
}

# docstat -- show documentations statistics
sub docstat
{
    my($curlen);
    my($cnt);

    printf("DOCMAX: %d\n",$docmax);

    $curlen = -1;
    foreach $cnt (@dochisto) {
        ++$curlen;
        next if ($cnt <= 0);

        $ptr = $lookup[$cnt];
        $ptr //= [];
        $lookup[$cnt] = $ptr;

        push(@$ptr,$curlen);
    }

    $cnt = -1;
    foreach $ptr (@lookup) {
        ++$cnt;
        next unless (ref($ptr));
        msgv("DOCLEN: $cnt",@$ptr);
    }
}

# doexec -- execute a program
sub doexec
{
    my(@argv) = @_;
    my($cmd);
    my($code);

    msgv("doexec: EXEC",@argv);

    $cmd = join(" ",@argv);
    system($cmd);

    $code = ($? >> 8) & 0xFF;

    $code;
}

# filetail -- get file tail
sub filetail
{
    my($file) = @_;

    $file =~ s,.*/,,g;

    $file;
}

# msgv -- output a message
sub msgv
{
    my(@argv) = @_;
    local($opt_pre);
    my($seplen);
    my($rhs);
    my($prenow);
    my($lhs);
    my($lno);

    optdcd(\@argv,qw(opt_pre));
    $opt_pre //= "+";
    $opt_pre .= " ";

    foreach $rhs (@argv) {
        $seplen = (length($lhs) > 0);

        if ((length($prenow) + length($lhs) + $seplen + length($rhs)) > 80) {
            printf("%s%s\n",$prenow,$lhs);
            undef($lhs);
            $prenow = $opt_pre;
            ++$lno;
        }

        $lhs .= " "
            if (length($lhs) > 0);

        $lhs .= $rhs;
    }

    if (length($lhs) > 0) {
        printf("%s%s\n",$prenow,$lhs);
        ++$lno;
    }

    $lno;
}

# subenv -- substitute environment
sub subenv
{
    my($rhs) = @_;
    my($ix);
    my($sym,$val);
    my($lhs);

    while (1) {
        $ix = index($rhs,'${');
        last if ($ix < 0);

        $lhs .= substr($rhs,0,$ix);
        $rhs = substr($rhs,$ix + 2);

        $ix = index($rhs,"}");
        $sym = substr($rhs,0,$ix);
        $rhs = substr($rhs,$ix + 1);

        $val = $$sym;
        sysfault("subenv: unknown symbol -- '%s'\n",$sym)
            unless (defined($val));

        $lhs .= $val;
    }

    $lhs .= $rhs;

    $lhs;
}

# sysdata -- locate the __DATA__ unit
sub sysdata
{
    my($pkgsrc) = @_;
    my($xfsrc,$sym,$pos);

    $pkgsrc //= caller();

    {
        $sym = $pkgsrc . "::DATA";

        $xfsrc = \*$sym;

        # remember the starting position -- since perl doesn't :-(
        $pos = \$sysdata_rewind{$pkgsrc};

        $$pos = tell($xfsrc)
            unless (defined($$pos));

        last if (seek($xfsrc,$$pos,0));

        sysfault("sysdata: seek fault pkgsrc='$pkgsrc' pos=$$pos -- $!\n");
    }

    return wantarray ? ($xfsrc,$sym,$$pos) : $xfsrc;
}

# sysfault -- fault
sub sysfault
{

    printf(@_);
    exit(1);
}

# sysstat -- get file status
sub sysstat
{
    my($file) = @_;
    my(@st);
    my($st);

    @st = stat($file);

    if (@st > 0) {
        $st = {};

        ($st->{st_dev},
        $st->{st_ino},
        $st->{st_mode},
        $st->{st_nlink},
        $st->{st_uid},
        $st->{st_gid},
        $st->{st_rdev},
        $st->{st_size},
        $st->{st_atime},
        $st->{st_mtime},
        $st->{st_ctime},
        $st->{st_blksize},
        $st->{st_blocks}) = @st;
    }

    $st;
}

package gccblame;
__DATA__
% foo.c
#include <stdint.h>
#include <stdio.h>

extern ${opt_T} __foo__${opt_arr};

#define IPTR(_adr)              ((intptr_t) _adr)
#define ADDR_MASK               IPTR(0xFFF)
#define EXPECTED_ADDR           IPTR(${opt_A})
#define FOO_ADDR                (IPTR(&__foo__) & ADDR_MASK)
#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);

    int ok = FOO_ADDR_IS_EXPECTED();

    if (ok) {
        printf("***Expected ***\n");
    }
    else {
        printf("### UNEXPECTED ###\n");
    }

    return ! ok;
}
% ${nul_c}
int
main(void)
{
    return 0;
}
% link.ld
INCLUDE ${dftlink}
__foo__ = ${opt_A};
Related