Fixture 08

indirect dispatch

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

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

Indirect-call / target-recovery fixture. A dispatcher selects a handler from an OPERATIONS TABLE (array of function pointers) indexed by a tag argument and returns the handler's result. Every handler returns a UNIQUE combination of its inputs, so an execution-differential test (original vs. recompiled decompilation) catches a mis-recovered call target the instant dispatch routes a tag to the wrong handler.

Targets review #8 (indirect / target-call recovery). The property under test: a DIRECT call must resolve to its named callee, while a genuinely INDIRECT call must stay an explicit indirect call whose computed target expression is a real table lookup (ops[tag]), never a fabricated/guessed direct callee. The table and all handlers live in this translation unit so it links standalone.

Differential vs. structural: - dispatch(), dispatch_switch(), tail_dispatch(): DIFFERENTIAL. Pure int functions with unique per-path constants; drivable by an int-diff gate. - apply(): STRUCTURAL. It takes a caller-supplied function pointer, so it cannot be driven by scalar ints alone; the assertion is that the callback parameter is preserved as an indirect call through the parameter, not inlined or bound to a fabricated callee.

Keep every handler pure (no globals, no memory) and deterministic.

tests/decompiler_fixtures/src/08_indirect_dispatch.c source
/* 08_indirect_dispatch.c
 *
 * Indirect-call / target-recovery fixture. A dispatcher selects a handler from
 * an OPERATIONS TABLE (array of function pointers) indexed by a tag argument and
 * returns the handler's result. Every handler returns a UNIQUE combination of
 * its inputs, so an execution-differential test (original vs. recompiled
 * decompilation) catches a mis-recovered call target the instant `dispatch`
 * routes a tag to the wrong handler.
 *
 * Targets review #8 (indirect / target-call recovery). The property under test:
 * a DIRECT call must resolve to its named callee, while a genuinely INDIRECT
 * call must stay an explicit indirect call whose computed target expression is a
 * real table lookup (`ops[tag]`), never a fabricated/guessed direct callee. The
 * table and all handlers live in this translation unit so it links standalone.
 *
 * Differential vs. structural:
 *   - dispatch(), dispatch_switch(), tail_dispatch(): DIFFERENTIAL. Pure int
 *     functions with unique per-path constants; drivable by an int-diff gate.
 *   - apply(): STRUCTURAL. It takes a caller-supplied function pointer, so it
 *     cannot be driven by scalar ints alone; the assertion is that the callback
 *     parameter is preserved as an indirect call through the parameter, not
 *     inlined or bound to a fabricated callee.
 *
 * Keep every handler pure (no globals, no memory) and deterministic.
 */
#include <stdint.h>

/* Function-pointer typedef for a binary integer handler. */
typedef int (*binop_fn)(int, int);

/* --- handlers: each returns a distinct combination of a and b ----------- */

static int h_add(int a, int b) { return a + b + 100; }
static int h_sub(int a, int b) { return a - b + 200; }
static int h_mul(int a, int b) { return a * b + 300; }
static int h_xor(int a, int b) { return (a ^ b) + 400; }
static int h_max(int a, int b) { return (a > b ? a : b) + 500; }

/* The OPERATIONS TABLE: an array of function pointers indexed by tag. A correct
 * decompilation recovers `ops[tag]` as the indirect target expression. */
static binop_fn ops[5] = { h_add, h_sub, h_mul, h_xor, h_max };

/* Table-driven dispatch. `int dispatch(int tag, int a, int b)` selects a handler
 * via an indirect call through ops[tag] and returns its result. The bounds guard
 * returns a unique sentinel so an out-of-range tag stays deterministic. */
int dispatch(int tag, int a, int b) {
    if (tag < 0 || tag >= 5)
        return -1;                 /* unique out-of-range sentinel */
    return ops[tag](a, b);         /* genuine indirect call: target is ops[tag] */
}

/* The same selection expressed as a switch over direct calls. A compiler may or
 * may not lower this to a jump table; either way each arm is a DIRECT call to a
 * named callee, and the returns must match dispatch() arm-for-arm. */
int dispatch_switch(int tag, int a, int b) {
    switch (tag) {
        case 0:  return h_add(a, b);
        case 1:  return h_sub(a, b);
        case 2:  return h_mul(a, b);
        case 3:  return h_xor(a, b);
        case 4:  return h_max(a, b);
        default: return -1;
    }
}

/* Indirect tail call: the selected handler's result is returned directly, giving
 * the exact tail-call shape (jmp through a table slot) that a decompiler must
 * render as an indirect call in return position, not as a fabricated direct one. */
int tail_dispatch(int tag, int a, int b) {
    if (tag < 0 || tag >= 5)
        return -1;
    return ops[tag](a, b);         /* tail position: often lowered to an indirect jmp */
}

/* Callback-parameter function. STRUCTURAL: `cb` is supplied by the caller, so
 * this cannot be exercised by scalar ints through the differential gate. The
 * assertion is that `cb(x)` stays an indirect call through the parameter. */
int apply(int (*cb)(int), int x) {
    return cb(x) + 1;
}

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

3/4
apply structural 8 lines
// glaurung: apply @ 0x1330
int apply(void * arg0, int arg1) {
    long var1;
    // x86-64 prologue: save rbp, frame 16 bytes
    var1 = ((long (*)(long))((long)arg0))((unsigned long)((unsigned int)(arg1)));
    // x86-64 epilogue: restore rbp
    return (unsigned int)((var1 + 1));
}
dispatch pass 32 lines
// glaurung: dispatch @ 0x1100
int dispatch(int arg0, int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    int local_4;
    long var3;
    // x86-64 prologue: save rbp, frame 16 bytes
    if (((long)(arg0) < 0)) {
        local_4 = -1;
        // x86-64 epilogue: restore rbp
        return (unsigned int)(local_4);
    }
    if ((5 <= (long)(arg0))) {
        local_4 = -1;
        // x86-64 epilogue: restore rbp
        return (unsigned int)(local_4);
    }
    var3 = ((long (*)(long, long))(ops[(long)(arg0)]))((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
    local_4 = var3;
    // x86-64 epilogue: restore rbp
    return (unsigned int)(local_4);
}
dispatch_switch pass 34 lines
// glaurung: dispatch_switch @ 0x1160
int dispatch_switch(int arg0, int arg1, int arg2) {
    extern int h_add(int, int);
    extern int h_max(int, int);
    extern int h_mul(int, int);
    extern int h_sub(int, int);
    extern int h_xor(int, int);
    long var10;
    long var12;
    long var14;
    long var6;
    long var8;
    // x86-64 prologue: save rbp, frame 32 bytes
    switch ((unsigned int)(arg0)) {
        case 0:
            var6 = ((long (*)(long, long))h_add)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return (unsigned int)(var6);
        case 1:
            var8 = ((long (*)(long, long))h_sub)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return (unsigned int)(var8);
        case 2:
            var10 = ((long (*)(long, long))h_mul)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return (unsigned int)(var10);
        case 3:
            var12 = ((long (*)(long, long))h_xor)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return (unsigned int)(var12);
        case 4:
            var14 = ((long (*)(long, long))h_max)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return (unsigned int)(var14);
        default:
            return (unsigned int)(-1);
    }
    // x86-64 epilogue: restore rbp
}
tail_dispatch pass 32 lines
// glaurung: tail_dispatch @ 0x12d0
int tail_dispatch(int arg0, int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    int local_4;
    long var3;
    // x86-64 prologue: save rbp, frame 16 bytes
    if (((long)(arg0) < 0)) {
        local_4 = -1;
        // x86-64 epilogue: restore rbp
        return (unsigned int)(local_4);
    }
    if ((5 <= (long)(arg0))) {
        local_4 = -1;
        // x86-64 epilogue: restore rbp
        return (unsigned int)(local_4);
    }
    var3 = ((long (*)(long, long))(ops[(long)(arg0)]))((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
    local_4 = var3;
    // x86-64 epilogue: restore rbp
    return (unsigned int)(local_4);
}

clang -O2

3/4
apply structural 13 lines
// glaurung: apply @ 0x11f0
int apply(void * arg0, int arg1) {
    long local_8;
    long ret;
    long var1;
    int var3;
    local_8 = ret;
    var1 = ((long (*)(long))((long)arg0))((unsigned long)((unsigned int)(arg1)));
    var3 = (var1 + 1);
    ret = (unsigned long)((unsigned int)(var3));
    // x86-64 epilogue: tear down frame
    return (unsigned int)(var3);
}
dispatch pass 21 lines
// glaurung: dispatch @ 0x1100
int dispatch(int arg0, int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    int ret;
    if (((unsigned long)((unsigned long)((unsigned int)(arg0))) <= (unsigned long)(4))) {
        ret = ((int (*)(long, long))(ops[(unsigned long)((unsigned int)(arg0))]))((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
        return ret;
    }
    return 0xffffffff;
}
dispatch_switch pass 17 lines
// glaurung: dispatch_switch @ 0x1120
int dispatch_switch(int arg0, int arg1, int arg2) {
    switch ((unsigned long)((unsigned int)(arg0))) {
        case 0:
            return (unsigned int)(((unsigned long)((unsigned int)(((unsigned long)((unsigned int)(arg1)) + arg2))) + 100));
        case 1:
            return (unsigned int)(((unsigned long)((unsigned int)(((unsigned long)((unsigned int)(arg1)) - arg2))) + 200));
        case 2:
            return (unsigned int)(((unsigned long)((unsigned int)((arg2 * (unsigned long)((unsigned int)(arg1))))) + 300));
        case 3:
            return (unsigned int)(((unsigned long)((unsigned int)((arg2 ^ (unsigned long)((unsigned int)(arg1))))) + 400));
        case 4:
            return (unsigned int)(((((((unsigned int)(arg1) == (unsigned int)(arg2)) | (arg1 < arg2)) == 0) ? (unsigned long)((unsigned int)(arg1)) : arg2) + 500));
        default:
            return 0xffffffff;
    }
}
tail_dispatch pass 21 lines
// glaurung: tail_dispatch @ 0x11d0
int tail_dispatch(int arg0, int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    int ret;
    if (((unsigned long)((unsigned long)((unsigned int)(arg0))) <= (unsigned long)(4))) {
        ret = ((int (*)(long, long))(ops[(unsigned long)((unsigned int)(arg0))]))((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
        return ret;
    }
    return 0xffffffff;
}

gcc -O0

3/4
apply structural 8 lines
// glaurung: apply @ 0x12bd
int apply(void * arg0, int arg1) {
    long var2;
    // x86-64 prologue: save rbp, frame 16 bytes
    var2 = ((long (*)(long))((long)arg0))((unsigned long)((unsigned int)(arg1)));
    // x86-64 epilogue: restore rbp
    return (unsigned int)((var2 + 1));
}
dispatch pass 28 lines
// glaurung: dispatch @ 0x1186
int dispatch(int arg0, int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    int ret;
    // x86-64 prologue: save rbp, frame 16 bytes
    if (((long)(arg0) < 0)) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    if (((((unsigned long)((unsigned int)(arg0)) == 4) | ((long)(arg0) < 4)) == 0)) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    ret = ((int (*)(long, long))(ops[(long)((int)((unsigned long)((unsigned int)(arg0))))]))((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
    // x86-64 epilogue: restore rbp
    return ret;
}
dispatch_switch pass 34 lines
// glaurung: dispatch_switch @ 0x11d4
int dispatch_switch(int arg0, int arg1, int arg2) {
    extern int h_add(int, int);
    extern int h_max(int, int);
    extern int h_mul(int, int);
    extern int h_sub(int, int);
    extern int h_xor(int, int);
    long var12;
    long var15;
    long var18;
    long var21;
    long var9;
    // x86-64 prologue: save rbp, frame 16 bytes
    switch ((unsigned long)((unsigned int)(arg0))) {
        case 0:
            var9 = ((long (*)(long, long))h_add)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return var9;
        case 1:
            var12 = ((long (*)(long, long))h_sub)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return var12;
        case 2:
            var15 = ((long (*)(long, long))h_mul)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return var15;
        case 3:
            var18 = ((long (*)(long, long))h_xor)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return var18;
        case 4:
            var21 = ((long (*)(long, long))h_max)((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
            return var21;
        default:
            return 0xffffffff;
    }
    // x86-64 epilogue: restore rbp
}
tail_dispatch pass 28 lines
// glaurung: tail_dispatch @ 0x126f
int tail_dispatch(int arg0, int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    int ret;
    // x86-64 prologue: save rbp, frame 16 bytes
    if (((long)(arg0) < 0)) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    if (((((unsigned long)((unsigned int)(arg0)) == 4) | ((long)(arg0) < 4)) == 0)) {
        // x86-64 epilogue: restore rbp
        return 0xffffffff;
    }
    ret = ((int (*)(long, long))(ops[(long)((int)((unsigned long)((unsigned int)(arg0))))]))((unsigned long)((unsigned int)(arg1)), (unsigned long)((unsigned int)(arg2)));
    // x86-64 epilogue: restore rbp
    return ret;
}

gcc -O2

3/4
apply structural 6 lines
// glaurung: apply @ 0x1230
int apply(void * arg0, int arg1) {
    long var1;
    var1 = ((long (*)(long))((long)arg0))((unsigned long)((unsigned int)(arg1)));
    return (unsigned int)((var1 + 1));
}
dispatch pass 27 lines
// glaurung: dispatch @ 0x1160
int dispatch(int arg0, int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    int ret;
    long var0;
    long var1;
    long var2;
    var0 = (long)(arg0);
    var1 = (unsigned long)((unsigned int)(arg1));
    var2 = (unsigned long)((unsigned int)(arg2));
    if (((unsigned long)((unsigned long)((unsigned int)(arg0))) <= (unsigned long)(4))) {
        ret = ((int (*)(long, long))(ops[var0]))(var1, var2);
        return ret;
    }
    return 0xffffffff;
}
dispatch_switch pass 17 lines
// glaurung: dispatch_switch @ 0x1190
int dispatch_switch(int arg0, int arg1, int arg2) {
    switch ((unsigned long)((unsigned int)(arg0))) {
        case 0:
            return (unsigned int)(((arg1 + arg2) + 100));
        case 1:
            return (unsigned int)(((unsigned long)((unsigned int)((arg1 - arg2))) + 200));
        case 2:
            return (unsigned int)(((unsigned long)((unsigned int)((arg1 * arg2))) + 300));
        case 3:
            return (unsigned int)(((unsigned long)((unsigned int)((arg1 ^ arg2))) + 400));
        case 4:
            return (unsigned int)((((arg1 < arg2) ? arg2 : arg1) + 500));
        default:
            return (unsigned int)(-1);
    }
}
tail_dispatch pass 27 lines
// glaurung: tail_dispatch @ 0x1200
long tail_dispatch(unsigned int arg0, unsigned int arg1, int arg2) {
    extern void h_add(void);
    extern void h_max(void);
    extern void h_mul(void);
    extern void h_sub(void);
    extern void h_xor(void);
    static void (*ops[5])(void) = {
        (void (*)(void))h_add,
        (void (*)(void))h_sub,
        (void (*)(void))h_mul,
        (void (*)(void))h_xor,
        (void (*)(void))h_max,
    };
    long ret;
    long var0;
    long var1;
    long var2;
    var0 = (long)((int)(arg0));
    var1 = (unsigned long)(arg1);
    var2 = (unsigned long)((unsigned int)(arg2));
    if (((unsigned long)(arg0) <= (unsigned long)(4))) {
        ret = ((long (*)(long, long))(ops[var0]))(var1, var2);
        return ret;
    }
    return 0xffffffff;
}

← 213 fixtures