Fixture 159

ifunc resolver

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

4 of 4 lanes have a function that returns a different result after decompilation: clang-O0 (4/5), clang-O2 (4/5), gcc-O0 (4/5), gcc-O2 (4/5).

An STT_GNU_IFUNC symbol is a function whose ADDRESS is computed at load time. The symbol's st_value does not point at a body; it points at a resolver, and the loader calls that resolver during relocation processing and writes the returned address into the GOT slot (R_X86_64_IRELATIVE, applied after all other relocations, before any user code runs). This is how glibc dispatches memcpy/strlen to an AVX2 or SSE2 body, and it is the single most confusing shape in a real .so for a decompiler:

- the call site is an ordinary PLT call, but nothing static points at the code that will actually run; - the resolver looks like a leaf function returning a pointer and is reachable only from a relocation, so a CFG built from call edges leaves it and both implementations unreferenced (candidate dead code); - the ifunc alias carries no DWARF subprogram at all - gcc and clang emit debug info for the resolver and the implementations, never for the alias - so a DWARF-driven recovery has a name with no body and two bodies with no callers; - the resolver runs before the C runtime is initialised, so it may not call into libc, which is why real ones are tiny and read cpu feature words.

Determinism note: BOTH implementations here compute exactly the same value by different instruction sequences (shift versus self-addition), so whichever one the resolver picks, the observable result is identical. Nothing in this fixture can vary between two loads of the same code.

The second half is the hand-rolled equivalent: a static function pointer resolved once on first use. Same dispatch semantics, no loader involvement - the contrast is the point.

tests/decompiler_fixtures/src/159_ifunc_resolver.c source
#include <stdint.h>

/* An STT_GNU_IFUNC symbol is a function whose ADDRESS is computed at load time.
 * The symbol's st_value does not point at a body; it points at a resolver, and
 * the loader calls that resolver during relocation processing and writes the
 * returned address into the GOT slot (R_X86_64_IRELATIVE, applied after all
 * other relocations, before any user code runs). This is how glibc dispatches
 * memcpy/strlen to an AVX2 or SSE2 body, and it is the single most confusing
 * shape in a real .so for a decompiler:
 *
 *   - the call site is an ordinary PLT call, but nothing static points at the
 *     code that will actually run;
 *   - the resolver looks like a leaf function returning a pointer and is
 *     reachable only from a relocation, so a CFG built from call edges leaves
 *     it and both implementations unreferenced (candidate dead code);
 *   - the ifunc alias carries no DWARF subprogram at all - gcc and clang emit
 *     debug info for the resolver and the implementations, never for the alias
 *     - so a DWARF-driven recovery has a name with no body and two bodies with
 *     no callers;
 *   - the resolver runs before the C runtime is initialised, so it may not call
 *     into libc, which is why real ones are tiny and read cpu feature words.
 *
 * Determinism note: BOTH implementations here compute exactly the same value by
 * different instruction sequences (shift versus self-addition), so whichever
 * one the resolver picks, the observable result is identical. Nothing in this
 * fixture can vary between two loads of the same code.
 *
 * The second half is the hand-rolled equivalent: a static function pointer
 * resolved once on first use. Same dispatch semantics, no loader involvement -
 * the contrast is the point.
 */

typedef int32_t (*Ifn159Fn)(int32_t);

static int32_t ifn159_impl_shift(int32_t value) {
    return (int32_t)((uint32_t)value << 1);
}

static int32_t ifn159_impl_add(int32_t value) {
    return (int32_t)((uint32_t)value + (uint32_t)value);
}

/* Selector kept fixed rather than read from cpu feature bits: the harness
 * compares two loads of the same code, so the choice must not depend on the
 * host. A real resolver differs only in where this word comes from. */
static const uint32_t IFN159_FEATURE_WORD = 0x1u;

static Ifn159Fn ifn159_resolve(void);

/* The alias itself: exported as STT_GNU_IFUNC, no DWARF, no body. */
int32_t ifunc_double(int32_t value) __attribute__((ifunc("ifn159_resolve")));

static Ifn159Fn ifn159_resolve(void) {
    if ((IFN159_FEATURE_WORD & 1u) != 0u) {
        return ifn159_impl_shift;
    }
    return ifn159_impl_add;
}

/* Ordinary exported wrapper so the ifunc is exercised through a function that
 * does have DWARF and can carry an execution contract. */
__attribute__((noinline)) int32_t ifunc_call_double(int32_t value) {
    return ifunc_double(value);
}

/* The ifunc must agree with the arithmetic it stands for, for every input. */
__attribute__((noinline)) int32_t ifunc_matches_reference(int32_t value) {
    uint32_t dispatched = (uint32_t)ifunc_double(value);
    uint32_t reference = (uint32_t)value * 2u;
    return (int32_t)(dispatched - reference);
}

/* Repeated dispatch in a bounded loop: every iteration is a PLT call whose
 * target slot was written by the loader, not by the linker. */
__attribute__((noinline)) int32_t
ifunc_fold(const int32_t *values, int32_t count) {
    uint32_t accumulator = 0u;
    int32_t index;
    if (values == 0 || count < 0 || count > 16) {
        return -1;
    }
    for (index = 0; index < count; ++index) {
        accumulator += (uint32_t)ifunc_double(values[index]);
    }
    return (int32_t)accumulator;
}

/* Hand-rolled dispatch: a static function pointer, null until the first call,
 * then latched. Cheap to recover in principle - the table is one slot - but the
 * "already resolved?" test is a load-modify-branch that a decompiler tends to
 * render as an unrelated global flag. Latching is idempotent and both targets
 * agree, so repeated calls return identical values. */
static Ifn159Fn ifn159_cached = 0;

__attribute__((noinline)) int32_t ifunc_lazy_double(int32_t value) {
    Ifn159Fn chosen = ifn159_cached;
    if (chosen == 0) {
        chosen = ifn159_resolve();
        ifn159_cached = chosen;
    }
    return chosen(value);
}

/* Loader-resolved and hand-resolved dispatch must agree: 0 for every input. */
__attribute__((noinline)) int32_t ifunc_paths_agree(int32_t value) {
    uint32_t loader_side = (uint32_t)ifunc_double(value);
    uint32_t manual_side = (uint32_t)ifunc_lazy_double(value);
    return (int32_t)(loader_side - manual_side);
}

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/5
ifunc_call_double pass 9 lines
// glaurung: ifunc_call_double @ 0x1130
int32_t ifunc_call_double(int32_t arg0) {
    extern int ifunc_double(long);
    int ret;
    // x86-64 prologue: save rbp, frame 16 bytes
    ret = ifunc_double((unsigned long)((unsigned int)(arg0)));
    // x86-64 epilogue: restore rbp
    return ret;
}
ifunc_fold pass 32 lines
// glaurung: ifunc_fold @ 0x1180
int32_t ifunc_fold(const int32_t * arg0, int32_t arg1) {
    extern long ifunc_double(long);
    unsigned int accumulator;
    int index;
    int local_4;
    long var3;
    // x86-64 prologue: save rbp, frame 32 bytes
    accumulator = 0;
    if ((arg0 == 0)) {
        local_4 = -1;
        // x86-64 epilogue: restore rbp
        return (unsigned int)(local_4);
    }
    if (((long)(arg1) < 0)) {
        local_4 = -1;
        // x86-64 epilogue: restore rbp
        return (unsigned int)(local_4);
    }
    if (((((unsigned long)((unsigned int)(arg1)) == 16) | ((long)(arg1) < 16)) == 0)) {
        local_4 = -1;
        // x86-64 epilogue: restore rbp
        return (unsigned int)(local_4);
    }
    for (index = 0; (index < arg1); index++) {
        var3 = ifunc_double((unsigned long)((unsigned int)(arg0[(long)(index)])));
        accumulator = (var3 + accumulator);
    }
    local_4 = accumulator;
    // x86-64 epilogue: restore rbp
    return (unsigned int)(local_4);
}
ifunc_lazy_double fail 19 lines
// glaurung: ifunc_lazy_double @ 0x1210
static unsigned char glaurung_global_4038[16] __attribute__((aligned(16)));
int32_t ifunc_lazy_double(int32_t arg0) {
    extern long ifn159_resolve(void);
    extern unsigned char glaurung_global_4038[16];
    int local_10;
    int ret;
    long var1;
    // x86-64 prologue: save rbp, frame 16 bytes
    local_10 = *(long *)(&glaurung_global_4038[0]);
    if ((local_10 == 0)) {
        var1 = ifn159_resolve();
        local_10 = var1;
        *(long *)(&glaurung_global_4038[0]) = (unsigned long)((unsigned int)(local_10));
    }
    ret = ((int (*)(long))(local_10))((unsigned long)((unsigned int)(arg0)));
    // x86-64 epilogue: restore rbp
    return ret;
}
ifunc_matches_reference pass 13 lines
// glaurung: ifunc_matches_reference @ 0x1150
int32_t ifunc_matches_reference(int32_t arg0) {
    extern long ifunc_double(long);
    unsigned int dispatched;
    unsigned int reference;
    long var0;
    // x86-64 prologue: save rbp, frame 16 bytes
    var0 = ifunc_double((unsigned long)((unsigned int)(arg0)));
    dispatched = var0;
    reference = ((unsigned long)((unsigned int)(arg0)) << 1);
    // x86-64 epilogue: restore rbp
    return (unsigned int)(((unsigned long)(dispatched) - reference));
}
ifunc_paths_agree pass 16 lines
// glaurung: ifunc_paths_agree @ 0x1260
int32_t ifunc_paths_agree(int32_t arg0) {
    extern long ifunc_double(long);
    extern int ifunc_lazy_double(int);
    unsigned int loader_side;
    unsigned int manual_side;
    long var0;
    int var2;
    // x86-64 prologue: save rbp, frame 16 bytes
    var0 = ifunc_double((unsigned long)((unsigned int)(arg0)));
    loader_side = var0;
    var2 = ifunc_lazy_double((unsigned long)((unsigned int)(arg0)));
    manual_side = var2;
    // x86-64 epilogue: restore rbp
    return (unsigned int)(((unsigned long)(loader_side) - manual_side));
}

clang -O2

4/5
ifunc_call_double pass 7 lines
// glaurung: ifunc_call_double @ 0x1130
int32_t ifunc_call_double(int32_t arg0) {
    extern int ifunc_double(int);
    int ret;
    ret = ifunc_double(arg0);
    return ret;
}
ifunc_fold pass 38 lines
// glaurung: ifunc_fold @ 0x1150
int32_t ifunc_fold(const int32_t * arg0, int32_t arg1) {
    extern long ifunc_double(long);
    int index;
    unsigned int accumulator;
    long var0;
    long var1;
    long var10;
    long var12;
    long var4;
    long var5;
    // x86-64 prologue: save callee registers, frame 40 bytes
    var0 = 0xffffffff;
    var1 = 0xffffffff;
    if ((arg0 == 0)) {
        // x86-64 epilogue: restore callee registers
        return (unsigned int)(var1);
    }
    var1 = var0;
    if (((unsigned long)(16) < (unsigned long)((unsigned long)((unsigned int)(arg1))))) {
        // x86-64 epilogue: restore callee registers
        return (unsigned int)(var1);
    }
    if (((unsigned long)((unsigned int)(arg1)) == 0)) {
        return 0;
    } else {
        var4 = (long)arg0;
        var5 = (unsigned long)((unsigned int)(arg1));
        var10 = 0;
        index = 0;
        do {
            var12 = ifunc_double((unsigned long)((unsigned int)(*(int *)((var4 + index * 4)))));
            var10 = (unsigned long)((unsigned int)((var10 + var12)));
            index = (index + 1);
        } while ((var5 != index));
        return (unsigned int)(var10);
    }
}
ifunc_lazy_double fail 12 lines
// glaurung: ifunc_lazy_double @ 0x11b0
static unsigned char glaurung_global_4038[16] __attribute__((aligned(16)));
int32_t ifunc_lazy_double(int32_t arg0) {
    extern unsigned char glaurung_global_4038[16];
    long var0;
    var0 = *(long *)(&glaurung_global_4038[0]);
    if ((var0 == 0)) {
        *(long *)(&glaurung_global_4038[0]) = (long)((long)(0x11f0));
        /* unrecovered indirect jump through 0x11f0 */
    }
    /* unrecovered indirect jump through var0 */
}
ifunc_matches_reference pass 11 lines
// glaurung: ifunc_matches_reference @ 0x1140
int32_t ifunc_matches_reference(int32_t arg0) {
    extern long ifunc_double(void);
    unsigned int dispatched;
    long var0;
    long var1;
    var0 = (unsigned long)((unsigned int)(arg0));
    var1 = ifunc_double();
    // x86-64 epilogue: tear down frame
    return (unsigned int)((var1 - (unsigned long)((unsigned int)((var0 + var0)))));
}
ifunc_paths_agree pass 18 lines
// glaurung: ifunc_paths_agree @ 0x11d0
int32_t ifunc_paths_agree(int32_t arg0) {
    extern long ifunc_double(void);
    extern int ifunc_lazy_double(int);
    unsigned int loader_side;
    unsigned int manual_side;
    long var0;
    long var1;
    long var3;
    int var4;
    // x86-64 prologue: save callee registers, frame 24 bytes
    var0 = (unsigned long)((unsigned int)(arg0));
    var1 = ifunc_double();
    var3 = (unsigned long)((unsigned int)(var1));
    var4 = ifunc_lazy_double((unsigned long)((unsigned int)(var0)));
    // x86-64 epilogue: restore callee registers
    return (unsigned int)((var3 - var4));
}

gcc -O0

4/5
ifunc_call_double pass 9 lines
// glaurung: ifunc_call_double @ 0x1183
int32_t ifunc_call_double(int32_t arg0) {
    extern int ifunc_double(long);
    int ret;
    // x86-64 prologue: save rbp, frame 16 bytes
    ret = ifunc_double((unsigned long)((unsigned int)(arg0)));
    // x86-64 epilogue: restore rbp
    return ret;
}
ifunc_fold pass 27 lines
// glaurung: ifunc_fold @ 0x11ca
int32_t ifunc_fold(const int32_t * arg0, int32_t arg1) {
    extern long ifunc_double(long);
    unsigned int accumulator;
    int index;
    long var7;
    // x86-64 prologue: save rbp, frame 32 bytes
    accumulator = 0;
    if ((arg0 == 0)) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    if (((long)(arg1) < 0)) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    if (((((unsigned long)((unsigned int)(arg1)) == 16) | ((long)(arg1) < 16)) == 0)) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    for (index = 0; (index < arg1); index++) {
        var7 = ifunc_double((unsigned long)((unsigned int)(arg0[(long)(index)])));
        accumulator = (accumulator + var7);
    }
    // x86-64 epilogue: restore rbp
    return accumulator;
}
ifunc_lazy_double fail 19 lines
// glaurung: ifunc_lazy_double @ 0x1238
static unsigned char glaurung_global_4038[16] __attribute__((aligned(16)));
int32_t ifunc_lazy_double(int32_t arg0) {
    extern long ifn159_resolve(void);
    extern unsigned char glaurung_global_4038[16];
    long local_8;
    int ret;
    long var1;
    // x86-64 prologue: save rbp, frame 32 bytes
    local_8 = *(long *)(&glaurung_global_4038[0]);
    if ((local_8 == 0)) {
        var1 = ifn159_resolve();
        local_8 = var1;
        *(long *)(&glaurung_global_4038[0]) = local_8;
    }
    ret = ((int (*)(long))(local_8))((unsigned long)((unsigned int)(arg0)));
    // x86-64 epilogue: restore rbp
    return ret;
}
ifunc_matches_reference pass 13 lines
// glaurung: ifunc_matches_reference @ 0x119e
int32_t ifunc_matches_reference(int32_t arg0) {
    extern long ifunc_double(long);
    unsigned int dispatched;
    unsigned int reference;
    long var1;
    // x86-64 prologue: save rbp, frame 32 bytes
    var1 = ifunc_double((unsigned long)((unsigned int)(arg0)));
    dispatched = var1;
    reference = ((unsigned int)(arg0) + (unsigned int)(arg0));
    // x86-64 epilogue: restore rbp
    return (unsigned int)(((unsigned long)(dispatched) - reference));
}
ifunc_paths_agree pass 16 lines
// glaurung: ifunc_paths_agree @ 0x127a
int32_t ifunc_paths_agree(int32_t arg0) {
    extern long ifunc_double(long);
    extern int ifunc_lazy_double(int);
    unsigned int loader_side;
    unsigned int manual_side;
    long var1;
    int var4;
    // x86-64 prologue: save rbp, frame 32 bytes
    var1 = ifunc_double((unsigned long)((unsigned int)(arg0)));
    loader_side = var1;
    var4 = ifunc_lazy_double((unsigned long)((unsigned int)(arg0)));
    manual_side = var4;
    // x86-64 epilogue: restore rbp
    return (unsigned int)(((unsigned long)(loader_side) - manual_side));
}

gcc -O2

4/5
ifunc_call_double pass 7 lines
// glaurung: ifunc_call_double @ 0x1160
int32_t ifunc_call_double(int32_t arg0) {
    extern int ifunc_double(int);
    int ret;
    ret = ifunc_double(arg0);
    return ret;
}
ifunc_fold pass 31 lines
// glaurung: ifunc_fold @ 0x1190
int32_t ifunc_fold(const int32_t * arg0, int32_t arg1) {
    extern long ifunc_double(long);
    unsigned int accumulator;
    int index;
    long var2;
    long var5;
    long var6;
    long var7;
    long var8;
    if ((arg0 == 0)) {
        return 0xffffffff;
    }
    if (((unsigned long)(16) < (unsigned long)((unsigned long)((unsigned int)(arg1))))) {
        return 0xffffffff;
    }
    if (((unsigned long)((unsigned int)(arg1)) == 0)) {
        return 0;
    }
    var2 = (long)((((long)arg0 + ((unsigned long)((unsigned int)((arg1 - 1))) * 4)) + 4));
    var5 = (long)arg0;
    var6 = 0;
    do {
        var7 = (unsigned long)((unsigned int)(*(int *)((var5))));
        var5 = (var5 + 4);
        var8 = ifunc_double(var7);
        var6 = (unsigned long)((unsigned int)((var6 + var8)));
    } while ((var5 != var2));
    // x86-64 epilogue: tear down frame
    return (unsigned int)(var6);
}
ifunc_lazy_double fail 12 lines
// glaurung: ifunc_lazy_double @ 0x11f0
static unsigned char glaurung_global_4038[16] __attribute__((aligned(16)));
int32_t ifunc_lazy_double(int32_t arg0) {
    extern unsigned char glaurung_global_4038[16];
    long var0;
    var0 = *(long *)(&glaurung_global_4038[0]);
    if ((var0 == 0)) {
        *(long *)(&glaurung_global_4038[0]) = (long)((long)(0x1140));
        /* unrecovered indirect jump through 0x1140 */
    }
    /* unrecovered indirect jump through var0 */
}
ifunc_matches_reference pass 11 lines
// glaurung: ifunc_matches_reference @ 0x1170
int32_t ifunc_matches_reference(int32_t arg0) {
    extern long ifunc_double(void);
    unsigned int dispatched;
    long var2;
    long var3;
    var2 = (unsigned long)((unsigned int)(((unsigned long)((unsigned int)(arg0)) + (unsigned long)((unsigned int)(arg0)))));
    var3 = ifunc_double();
    // x86-64 epilogue: tear down frame
    return (unsigned int)((var3 - var2));
}
ifunc_paths_agree pass 16 lines
// glaurung: ifunc_paths_agree @ 0x1220
int32_t ifunc_paths_agree(int32_t arg0) {
    extern long ifunc_double(void);
    extern int ifunc_lazy_double(int);
    unsigned int loader_side;
    unsigned int manual_side;
    long var0;
    long var1;
    long var3;
    int var4;
    var0 = (unsigned long)((unsigned int)(arg0));
    var1 = ifunc_double();
    var3 = (unsigned long)((unsigned int)(var1));
    var4 = ifunc_lazy_double((unsigned long)((unsigned int)(var0)));
    return (unsigned int)(((unsigned long)((unsigned int)(var3)) - (unsigned long)((unsigned int)(var4))));
}

← 213 fixtures