GCC 错误地优化了自定义地址处变量的指针相等性测试

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

优化时,GCC 似乎错误地绕过了 #define 测试。

首先,我使用自己的 link.ld 链接描述文件在地址 0xFFF 处提供一个 __foo__ 符号( 实际上是最低位,不是整个地址):

INCLUDE ./default.ld
__foo__ = 0xFFF;

然后,foo.c 源文件检查 __foo__ 的地址:

#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;
}

我期待 ***Expected *** 打印消息,因为 FOO_ADDR_IS_EXPECTED() 应该是真的。

使用 -O0 选项编译,它按预期执行:

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

但是使用 -O1 选项,它不会:

$ 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 ###

下面是-O0中的反汇编:

$ objdump -d ./foo_O0
...
0000000000001169 <main>:
...
    11b5:       b8 00 00 00 00          mov    [=14=]x0,%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    [=14=]xfff,%eax
    11cb:       48 3d ff 0f 00 00       cmp    [=14=]xfff,%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    [=14=]x0,%eax
...

我不是专家,但我可以看到一个 jne 条件和两个 puts 调用,它们与 if (FOO_ADDR_IS_EXPECTED()) 语句匹配。

下面是-O1中的反汇编:

$ objdump -d ./foo_O1
...
0000000000001169 <main>:
...
    11c2:       b8 00 00 00 00          mov    [=15=]x0,%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>
...

这一次,我没有看到条件,而是直接调用 puts(针对 printf("### UNEXPECTED ###\n"); 语句)。

为什么 -O1 优化会修改行为?为什么把FOO_ADDR_IS_EXPECTED()优化为false?

一些有助于您分析的上下文:

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

编辑: 令人惊讶的是,将 0xFFF 值修改为 0xABC 会改变行为:

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

正如 Andrew Henle 所指出的,地址对齐似乎很重要:使用 0xABF 而不是 0xABC 产生与 0xFFF.[=42= 相同的结果]

除非完全禁用优化,否则如果代码在基于外部符号的地址与不基于同一符号的地址之间进行比较,gcc 和 clang 都容易出现无意义的行为。该问题超出了将此类比较视为产生未指定结果的范围,并且可能导致代码行为既不符合产生 true 的比较,也不产生 false。

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

clang 和 gcc 都会生成代码,如果 test 传递给 y 的地址并且它恰好紧跟在 x 之后,则将 y[0] 设置为2 但随后 return 1. 这种行为在几年前就已经被报道过,但我不知道除了 -O0 之外还有什么选项可以让编译器以符合标准的方式处理这样的函数。

__foo__ 的地址无效时,

(intptr_t)(&__foo__) 未定义行为 (UB)。

OP 的 __foo__ = 0xFFF; 可能违反了 int 的对齐规则。

OP 尝试使用 .

限制较少的 char
// extern int __foo__;
extern char __foo__; 

更好的优化往往会利用 UB。
我使用 在没有优化的情况下工作 在高度优化时失败 作为 UB 潜伏在某处的暗示。在这种情况下,&__foo__ 无效。

正如@AndrewHenle and @chux-ReinstateMonica所说,这是一个对齐问题。

__foo__变量类型为int:其地址应为32位对齐,即能被4整除。
0xFFF 不能被 4 整除,因此编译器假定它不是有效的 int 地址:它将相等性测试优化为 false。

__foo__ 的类型更改为 char 会删除对齐约束,并且行为在 -O0-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 ***

我们知道-O会产生“行为”。

但是,-O* 开启了一些更细粒度的 -f 优化选项。

我很好奇 -f 实际上是“责备”。

可在以下位置找到 -f 选项列表:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

产生该行为的具体优化是:

-ftree-bit-ccp

它的文档是:

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.


开始时,我不知道哪个 -f 选项在进行优化。所以,我决定一个一个地应用选项和 rebuild/rerun 测试程序。

懒惰,我不想手工做这个。我写了一个 [perl] 脚本来提取上面的 .html 文件,解析它,然后一个一个地应用各个 -f 选项。

旁注: 具有讽刺意味的是,这可能比手动编辑 .html 文件来创建脚本花费的时间更长,但这很有趣...... 而且,有时我想知道哪个 -f 选项在我自己的代码中进行了给定的优化,但我总是下注。

该脚本有点粗糙,但它可能会在未来被改编并重新用于其他测试程序。

#!/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 = ;

                $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([=11=]);
    $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 = ;

            # 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};