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.
#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/5ifunc_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/5ifunc_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/5ifunc_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/5ifunc_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))));
}