7

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.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Lrnt Gr
  • 369
  • 2
  • 9
  • Recommend: Enable all warnings to identify sub-par code to insure your issue is real. – chux - Reinstate Monica Jul 15 '21 at 16:01
  • `__foo__ = 0xFFF;` and `__foo__ at 0x5580202d0fff` is something wrong – 0___________ Jul 15 '21 at 16:03
  • @chux-ReinstateMonica I recompiled with `-Wall -Wextra -Werror`, no warning pops – Lrnt Gr Jul 15 '21 at 16:08
  • 2
    *Surprisingly, modifying the 0xFFF value to 0xABC changes the behaviour* That shouldn't be surprising at all. `0x...FFF` is an odd address. `0x...ABC` is divisible by 4. – Andrew Henle Jul 15 '21 at 16:08
  • @AndrewHenle right, but it should not modify the integer `intptr_t` comparison: the `printf` output always show the same value for `FOO_ADDR` and `EXPECTED_ADDR`. – Lrnt Gr Jul 15 '21 at 16:11
  • 3
    You might be violating the x86_64 ABI. See page 14: https://raw.githubusercontent.com/wiki/hjl-tools/x86-psABI/x86-64-psABI-1.0.pdf – Andrew Henle Jul 15 '21 at 16:12
  • 2
    `extern int __foo__;` as `extern char __foo__;` certainly alleviates the alignment issue. Conversion of an `int *` to a integer, in the presence of an invalid `int *`, does not need to produce the integer 0x5603f4005fff. – chux - Reinstate Monica Jul 15 '21 at 16:16
  • @chux-ReinstateMonica Well done! It does work. Thanks! – Lrnt Gr Jul 15 '21 at 16:19
  • @chux-ReinstateMonica Still, the optimization does not care about the `(intptr_t)` cast. I'm making a comparison on `intrptr_t` values, not on addresses. So the address alignment optimization is not totally relevant, is it? – Lrnt Gr Jul 15 '21 at 16:23
  • 2
    @LrntGr `if (FOO_ADDR_IS_EXPECTED())` occurs in C and the emitted code corresponds to the code as a whole, not "I'm making a comparison on intrptr_t values" in isolation. AFAIK, the optimization can assume the `int *` to `intptr_t` always results in an integer with 2 LSBits clear. ... and that is not equal to xFFF – chux - Reinstate Monica Jul 15 '21 at 16:34
  • @chux-ReinstateMonica *AFAIK, the optimization can assume the `int *` to `intptr_t` always results in an integer with 2 LSBits clear. ... and that is not equal to `xFFF`* That certainly appears to be what the compiler is doing here. But I'd defer to anyone with more expertise with the x86_64 ABI as to whether or not that's a proper assumption. I don't have the expertise with the ABI to state whether or not an `int` variable must always have a 4-byte alignment and x86 just generally allows misaligned accesses for convenience, or if an `int` has looser alignment restrictions and this is a bug. – Andrew Henle Jul 15 '21 at 16:49
  • 1
    I've added the "x86" and "x86-64" tags as this appears to be appropriate for both. – Andrew Henle Jul 15 '21 at 16:51
  • 1
    @AndrewHenle "an int variable must always have a 4-byte alignment" is not the issue as many x86 CPU/bus will handle a mis-alligned `int`, but with slower performance. As I see it. the issue is that a compliant C compiler can assume a `int *` is even/quad (lower 1 or 2 bits clear) and form code based on that. – chux - Reinstate Monica Jul 15 '21 at 17:02
  • @LrntGr `void*` is the least restrictive object address type. Could use extern `void *__foo__;`. Yet I do not know how `__foo__` is to be de-referenced - if at all, in other code. – chux - Reinstate Monica Jul 15 '21 at 17:18
  • I suspect the optimizer is making the assumption that `__foo__` must be aligned on something like a 4-byte boundary, and that the pointer must therefore end in two zero bits. In that case, the comparison must be false. – Tom Karzes Jul 15 '21 at 18:22
  • @chux-ReinstateMonica I think you missed my point: the ABI may require actual `int` variables to have a 4-byte alignment while still permitting the forced loading or storing of `int` values from/to misaligned addresses. "You must put actual things declared as `int` on 4-byte boundaries, but we'll let you abuse things anyway" vs "`int` variables can have any alignment, we just make 4-byte-aligned `int` accesses faster". If it's the former, forcing an `int` to a `0x...FFF` address likely results in undefined behavior, if it's the latter this could very well be a compiler bug. – Andrew Henle Jul 15 '21 at 18:38
  • (cont) I *suspect* it's the former, but I don't know. – Andrew Henle Jul 15 '21 at 18:38

4 Answers4

4

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

Lrnt Gr
  • 369
  • 2
  • 9
  • 3
    The compiler doesn't "guess", it *knows* the ABI rules, and thus knows that `0xFFF` isn't a valid `int` address and can optimize away the check. Just like [Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?](https://stackoverflow.com/q/47510783) and [Does the C++ standard allow for an uninitialized bool to crash a program?](https://stackoverflow.com/q/54120862), if you create a situation that violates the ABI then the resulting behaviour is undefined (and therefore potentially exciting). – Peter Cordes Jul 15 '21 at 19:16
  • If you really did need to access an unaligned `int`, you can wrap it in a `struct` with `__attribute__((packed))`. – Nate Eldredge Jul 15 '21 at 21:02
  • 1
    @NateEldredge: Or `extern int foo __attribute__((aligned(1)));`. (Or typedef an `unaligned_aliasing_int` with `__attribute__((aligned(1),may_alias))`, useful for pointers instead of memcpy into an int for various things, as in [Why does unaligned access to mmap'ed memory sometimes segfault on AMD64?](https://stackoverflow.com/q/47510783) or [Why does glibc's strlen need to be so complicated to run quickly?](https://stackoverflow.com/a/57676035)) – Peter Cordes Jul 15 '21 at 23:15
3

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

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • 1
    I suspect this is the correct answer, but I'm personally not ready to rule out a compiler bug, given [the anwser @supercat posted](https://stackoverflow.com/a/68397328/4756299). Even though his answer is also touching on undefined behavior for different reasons. Yes, this is deep-diving into language-lawyer territory... – Andrew Henle Jul 15 '21 at 18:53
1

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.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • 1
    That's very interesting! Do you happen to have links to bugreports? – SergeyA Jul 15 '21 at 16:15
  • I don't think this is the right answer. This is about pointer aliasing and optimizing out reloads. But the question at hand is about comparing the addresses themselves. I think that https://stackoverflow.com/a/68397700/634919 has the correct answer - the compiler assumes that every `int` variable is aligned to 4 bytes and thus the low bits can never be set. – Nate Eldredge Jul 15 '21 at 17:10
  • @NateEldredge: Both gcc and clang treat as UB (without any justification in the Standard) situations where two pointers that they observe to be equal happen to be differently based. As such, a comparison between differently-based objects that yields false is not observably different from one that yields true, causing the compiler to jump the rails because the pointers are differently based. The mechanism of failure in my example is different from in the OP's, but even if the generated code had noticed the pointers were equal, that would merely set things up for possible downstream failure. – supercat Jul 15 '21 at 17:58
1

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};
Craig Estey
  • 30,627
  • 4
  • 24
  • 48