Every control transfer in a method, an existing GOTO, an
IF*'s two branches, ordinary fall-through between blocks, is rewritten to set a
synthetic state local and jump to one shared dispatcher table at the top of the
method, without physically reordering the underlying blocks. Every branch ends up looking
like "jump to the dispatcher, which picks the next block from a table", enough to defeat a
structured decompiler's usual if/while/for recovery. Block order is not shuffled, which
keeps the output easier to verify.
Excluded, to avoid emitting incorrect bytecode: constructors; any method
that already has an exception handler; any method already containing a
TABLESWITCH/LOOKUPSWITCH/JSR/RET; any
method where a non-parameter local isn't provably int-typed everywhere it's
stored (a long/float/double/reference store anywhere
excludes the whole method, since the type is not guessed); and, checked with a
dataflow analysis pass before flattening is attempted, any method where a
value crosses a block boundary on the operand stack instead of through a local variable.
That last case is common: a ternary expression fed directly into another
call's argument list joins its two branches on the stack, not in a local.
Raises the cost of reconstructing readable control flow from decompiled output, a structured decompiler's normal loop/branch recovery has nothing to latch onto once every transfer routes through one dispatcher. It doesn't change what the method actually computes, and it doesn't claim to defeat an attacker willing to trace the dispatcher table by hand.
TABLESWITCH dispatchers are present, per
javap, in at least ten classes of a protected Apache Commons Exec build,
with every method that was eligible for nativization still nativized alongside
them. A ternary-joins-via-the-stack method from that same codebase is
excluded, not crashed, after the dataflow check was added, the fix for a
verifier crash found during development.