Fixture 208

flag register roundtrip

C · 4 functions · 4 lanes · 16 of 16 function-lanes behave identically

All 4 lanes recompile and return the same results as the original.

An instruction the lifter does not model, whose destination register is nevertheless READ afterwards.

THE DEFECT. Op::opaque builds Op::Intrinsic { ins: [], outs: [], reads_mem: true, writes_mem: true } and its doc calls it "maximally-conservative". That is true of memory and FALSE of registers: outs: [] is not an absence of information, it is a positive claim that no register is written. use_def::defs_uses returns no definition, so ssa::write_regs places no phi and never bumps the destination's version, and the value live BEFORE the instruction flows past it into every later reader. memory_ssa maps the same op to unknown_effects(true, true) and clobbers every region — assume-everything for memory, assume-nothing for registers, from one op.

The consequence is worse than a wrong value. A destination that nothing defines is read as an INCOMING ARGUMENT, so the recovered prototype grows parameters the function never had. A one-argument function is reported with three. That moves type_match (argument correspondence), byte_match (the recompiled signature) and the def-use census simultaneously.

WHY THE FLAG REGISTER. The obvious reproduction is rdtsc or cpuid, and both are unusable here: a timestamp is not deterministic and CPUID is not portable. Saving and restoring the flag register is unmodelled in exactly the same way, is deterministic, and exists on every architecture we test — pushfq/popfq on x86-64, mrs/msr nzcv on AArch64, mrs/msr cpsr on ARM32 — with a portable fallback so no lane loses the fixture.

205_x87_long_double covers the same root cause through the x87 stack, which is 99.997% of the corpus-wide occurrences. This covers it through a GENERAL-PURPOSE register, which is where the phantom-parameter symptom appears, and no other fixture reaches it.

Every function returns a value derived only from its arguments, so a correct recovery is checkable by execution; the arity is checkable by reading the recovered prototype.

tests/decompiler_fixtures/src/208_flag_register_roundtrip.c source
#include <stdint.h>

/* An instruction the lifter does not model, whose destination register is
 * nevertheless READ afterwards.
 *
 * THE DEFECT. `Op::opaque` builds `Op::Intrinsic { ins: [], outs: [],
 * reads_mem: true, writes_mem: true }` and its doc calls it
 * "maximally-conservative". That is true of memory and FALSE of registers:
 * `outs: []` is not an absence of information, it is a positive claim that no
 * register is written. `use_def::defs_uses` returns no definition, so
 * `ssa::write_regs` places no phi and never bumps the destination's version,
 * and the value live BEFORE the instruction flows past it into every later
 * reader. `memory_ssa` maps the same op to `unknown_effects(true, true)` and
 * clobbers every region — assume-everything for memory, assume-nothing for
 * registers, from one op.
 *
 * The consequence is worse than a wrong value. A destination that nothing
 * defines is read as an INCOMING ARGUMENT, so the recovered prototype grows
 * parameters the function never had. A one-argument function is reported with
 * three. That moves `type_match` (argument correspondence), `byte_match` (the
 * recompiled signature) and the def-use census simultaneously.
 *
 * WHY THE FLAG REGISTER. The obvious reproduction is `rdtsc` or `cpuid`, and
 * both are unusable here: a timestamp is not deterministic and CPUID is not
 * portable. Saving and restoring the flag register is unmodelled in exactly the
 * same way, is deterministic, and exists on every architecture we test —
 * `pushfq`/`popfq` on x86-64, `mrs`/`msr nzcv` on AArch64, `mrs`/`msr cpsr` on
 * ARM32 — with a portable fallback so no lane loses the fixture.
 *
 * `205_x87_long_double` covers the same root cause through the x87 stack, which
 * is 99.997% of the corpus-wide occurrences. This covers it through a
 * GENERAL-PURPOSE register, which is where the phantom-parameter symptom
 * appears, and no other fixture reaches it.
 *
 * Every function returns a value derived only from its arguments, so a correct
 * recovery is checkable by execution; the arity is checkable by reading the
 * recovered prototype.
 */

static inline uint64_t save_flags(void) {
#if defined(__x86_64__)
    uint64_t flags;
    __asm__ __volatile__("pushfq\n\tpopq %0" : "=r"(flags)::"memory");
    return flags;
#elif defined(__i386__)
    uint32_t flags;
    __asm__ __volatile__("pushfl\n\tpopl %0" : "=r"(flags)::"memory");
    return flags;
#elif defined(__aarch64__)
    uint64_t flags;
    __asm__ __volatile__("mrs %0, nzcv" : "=r"(flags));
    return flags;
#elif defined(__arm__)
    uint32_t flags;
    __asm__ __volatile__("mrs %0, cpsr" : "=r"(flags));
    return flags;
#else
    return 0;
#endif
}

/* The measurement. `seed * K` lands in the same register class the flag save
 * writes; if that write is invisible to dataflow, the multiply's result flows
 * past it and the mask below reads a value the machine never produced. The
 * flags themselves are masked away entirely, so the ANSWER is deterministic on
 * every target while the instruction remains unmodelled. */
__attribute__((noinline)) int64_t flags_do_not_leak(int64_t seed) {
    int64_t product = seed * 2654435761LL;
    uint64_t flags = save_flags();
    /* Discard every architecturally-defined bit: the result depends only on
     * `seed`, but only if the flag read did not clobber the product. */
    return product + (int64_t)(flags & 0);
}

/* One argument in, one argument out, with an unmodelled instruction between
 * them. A recovered prototype with more than one parameter is the phantom-
 * parameter defect. */
__attribute__((noinline)) int64_t single_argument_survives(int64_t value) {
    (void)save_flags();
    return value + 1;
}

/* The unmodelled instruction sits inside a loop, so a stale value propagates
 * across iterations rather than once. */
__attribute__((noinline)) int64_t accumulate_across_barrier(int32_t count) {
    int64_t total = 0;
    if (count < 0 || count > 32) {
        return -1;
    }
    for (int32_t i = 0; i < count; i++) {
        total += (int64_t)i * 3;
        (void)save_flags();
        total ^= 0x11;
    }
    return total;
}

/* CONTROL: the identical shape with the unmodelled instruction removed. If this
 * fails, the defect is in the arithmetic and not in the effect model. */
__attribute__((noinline)) int64_t control_without_barrier(int64_t seed) {
    int64_t product = seed * 2654435761LL;
    return product + 0;
}

Recovered C

Generated by glaurung decompile --style decbench at b47f6b43. baseline.json records the result after recompiling the C and calling it beside the original with seeded inputs.

clang -O0

4/4
accumulate_across_barrier pass 28 lines
// glaurung: accumulate_across_barrier @ 0x1170
int64_t accumulate_across_barrier(int32_t arg0) {
    extern unsigned long save_flags(void);
    long total;
    int i;
    long local_8;
    unsigned long var4;
    // x86-64 prologue: save rbp, frame 32 bytes
    total = 0;
    if (((long)(arg0) < 0)) {
        local_8 = -1;
        // x86-64 epilogue: restore rbp
        return local_8;
    }
    if (((((unsigned long)((unsigned int)(arg0)) == 32) | ((long)(arg0) < 32)) == 0)) {
        local_8 = -1;
        // x86-64 epilogue: restore rbp
        return local_8;
    }
    for (i = 0; (i < arg0); i++) {
        total = (((long)(i) * 3) + total);
        var4 = save_flags();
        total = (total ^ 17);
    }
    local_8 = total;
    // x86-64 epilogue: restore rbp
    return local_8;
}
control_without_barrier pass 8 lines
// glaurung: control_without_barrier @ 0x1200
int64_t control_without_barrier(int64_t arg0) {
    long product;
    // x86-64 prologue: save rbp
    product = (0x9e3779b1 * arg0);
    // x86-64 epilogue: restore rbp
    return product;
}
flags_do_not_leak pass 13 lines
// glaurung: flags_do_not_leak @ 0x1100
int64_t flags_do_not_leak(int64_t arg0) {
    extern unsigned long save_flags(void);
    long product;
    unsigned long flags;
    unsigned long var2;
    // x86-64 prologue: save rbp, frame 32 bytes
    product = (0x9e3779b1 * arg0);
    var2 = save_flags();
    flags = var2;
    // x86-64 epilogue: restore rbp
    return product;
}
single_argument_survives pass 9 lines
// glaurung: single_argument_survives @ 0x1150
int64_t single_argument_survives(int64_t arg0) {
    extern unsigned long save_flags(void);
    unsigned long var0;
    // x86-64 prologue: save rbp, frame 16 bytes
    var0 = save_flags();
    // x86-64 epilogue: restore rbp
    return (arg0 + 1);
}

clang -O2

4/4
accumulate_across_barrier pass 24 lines
// glaurung: accumulate_across_barrier @ 0x1120
int64_t accumulate_across_barrier(int32_t arg0) {
    long total;
    int i;
    long ret;
    long var3;
    long var5;
    ret = -1;
    if (((unsigned long)((unsigned long)((unsigned int)(arg0))) <= (unsigned long)(32))) {
        if (((unsigned long)((unsigned int)(arg0)) == 0)) {
            return 0;
        }
        var3 = ((unsigned long)((unsigned int)(arg0)) + ((unsigned long)((unsigned int)(arg0)) * 2));
        var5 = 0;
        total = 0;
        do {
            /* asm: pushfq */
            total = ((total + var5) ^ 17);
            var5 = (var5 + 3);
            ret = total;
        } while ((var3 != var5));
    }
    return ret;
}
control_without_barrier pass 5 lines
// glaurung: control_without_barrier @ 0x1160
int64_t control_without_barrier(int64_t arg0) {
    long product;
    return (0x9e3779b1 * arg0);
}
flags_do_not_leak pass 7 lines
// glaurung: flags_do_not_leak @ 0x1100
int64_t flags_do_not_leak(int64_t arg0) {
    long product;
    /* asm: pushfq */
    // x86-64 epilogue: tear down frame
    return (0x9e3779b1 * arg0);
}
single_argument_survives pass 6 lines
// glaurung: single_argument_survives @ 0x1110
int64_t single_argument_survives(int64_t arg0) {
    /* asm: pushfq */
    // x86-64 epilogue: tear down frame
    return (arg0 + 1);
}

gcc -O0

4/4
accumulate_across_barrier pass 24 lines
// glaurung: accumulate_across_barrier @ 0x1158
int64_t accumulate_across_barrier(int32_t arg0) {
    extern unsigned long save_flags(void);
    long total;
    int i;
    unsigned long var6;
    // x86-64 prologue: save rbp, frame 24 bytes
    total = 0;
    if (((long)(arg0) < 0)) {
        // x86-64 epilogue: restore rbp
        return -1;
    }
    if (((((unsigned long)((unsigned int)(arg0)) == 32) | ((long)(arg0) < 32)) == 0)) {
        // x86-64 epilogue: restore rbp
        return -1;
    }
    for (i = 0; (i < arg0); i++) {
        total = (total + (((long)(i) + (long)(i)) + (long)(i)));
        var6 = save_flags();
        total = (total ^ 17);
    }
    // x86-64 epilogue: restore rbp
    return total;
}
control_without_barrier pass 8 lines
// glaurung: control_without_barrier @ 0x11bc
int64_t control_without_barrier(int64_t arg0) {
    long product;
    // x86-64 prologue: save rbp
    product = (arg0 * 0x9e3779b1);
    // x86-64 epilogue: restore rbp
    return product;
}
flags_do_not_leak pass 13 lines
// glaurung: flags_do_not_leak @ 0x1109
int64_t flags_do_not_leak(int64_t arg0) {
    extern unsigned long save_flags(void);
    long product;
    unsigned long flags;
    unsigned long var3;
    // x86-64 prologue: save rbp, frame 24 bytes
    product = (arg0 * 0x9e3779b1);
    var3 = save_flags();
    flags = var3;
    // x86-64 epilogue: restore rbp
    return product;
}
single_argument_survives pass 9 lines
// glaurung: single_argument_survives @ 0x1139
int64_t single_argument_survives(int64_t arg0) {
    extern unsigned long save_flags(void);
    unsigned long var0;
    // x86-64 prologue: save rbp, frame 8 bytes
    var0 = save_flags();
    // x86-64 epilogue: restore rbp
    return (arg0 + 1);
}

gcc -O2

4/4
accumulate_across_barrier pass 26 lines
// glaurung: accumulate_across_barrier @ 0x1130
int64_t accumulate_across_barrier(int32_t arg0) {
    long total;
    int i;
    long var1;
    long var3;
    long var4;
    long var7;
    if (((unsigned long)(32) < (unsigned long)((unsigned long)((unsigned int)(arg0))))) {
        return -1;
    }
    if (((unsigned long)((unsigned int)(arg0)) == 0)) {
        return 0;
    }
    var1 = (unsigned long)((unsigned int)((arg0 - 1)));
    var3 = 0;
    var4 = ((var1 + (var1 * 2)) + 3);
    total = 0;
    do {
        var7 = (total + var3);
        /* asm: pushfq */
        var3 = (var3 + 3);
        total = (var7 ^ 17);
    } while ((var3 != var4));
    return total;
}
control_without_barrier pass 5 lines
// glaurung: control_without_barrier @ 0x1180
int64_t control_without_barrier(int64_t arg0) {
    long product;
    return (0x9e3779b1 * arg0);
}
flags_do_not_leak pass 6 lines
// glaurung: flags_do_not_leak @ 0x1100
int64_t flags_do_not_leak(int64_t arg0) {
    long product;
    /* asm: pushfq */
    return (arg0 * 0x9e3779b1);
}
single_argument_survives pass 6 lines
// glaurung: single_argument_survives @ 0x1120
int64_t single_argument_survives(int64_t arg0) {
    /* asm: pushfq */
    // x86-64 epilogue: tear down frame
    return (arg0 + 1);
}

← 213 fixtures