Lower Type

 

The integer obfuscators — EncodeArithmetic, EncodeData — cannot see floating-point code. LowerType makes it visible to them: its float2fixed converter rewrites a function's float and double computation as fixed-point integer arithmetic in a chosen Q-format. The function's parameters and return value stay floating point, converted at entry and at the return, so its callers are unchanged.

OptionArgumentsDescription
--Transform LowerType Type-directed lowering of a value's representation. The float2fixed converter replaces the floating-point locals and parameters of a function with fixed-point integers (a Q-format), rewriting the floating-point arithmetic as integer operations and routing transcendental calls (sqrt, log10, exp, pow, hypot, fabs, fmin, fmax, floor, ceil, round, ...); a lowered value cast to an integer (e.g. (int)floor(x) for an index) becomes its integer part to the embedded libFixed library. The result is integer code that the integer obfuscators (EncodeArithmetic, EncodeData) can then protect. A function's parameters and return value stay floating-point (they are converted at entry and return -- the ABI boundary), and lowered variables are declared with a distinctive typedef PREFIX_FIXED_Q<m>p<n> so a reviewer can see which values were converted. Fixed point is a bounded-range, approximate representation; the transform refuses rather than mis-lower on constructs it does not yet handle (written floating-point globals, escaping local float buffers). A call to an un-routed callee crosses the value boundary (its float arguments are decoded and any lowered result re-encoded), so lowered code composes with printf, helpers, and libm. Local float arrays are lowered in place when their address does not escape. Floating-point pointer parameters (double *buf) are lowered at the value boundary -- loads encode / stores decode -- so the interior algorithm is protected while the pointee stays IEEE.
--LowerTypeConvert float2fixed Which conversion to apply. Default=float2fixed.
  • float2fixed = Lower floating-point values to fixed-point integers.
--LowerTypeType float, double, all Which source floating-point type(s) to lower. Under the same-width rule a float lowers to a 32-bit container and a double to a 64-bit container. Default=all.
  • float = Lower only float variables.
  • double = Lower only double variables.
  • all = Lower every floating-point variable.
--LowerTypeFixedFraction INTSPEC The number of fraction bits n for the float2fixed converter; the Q-format is Q(width-n)p(n) (e.g. 24 gives Q40p24 in a 64-bit container: range about +/-2^15, resolution 2^-24). Larger n is more precise but narrows the representable range. The default (24, Q40p24) keeps multiply/divide within int64 so no 128-bit widening is needed (see below). Default=24.
--LowerTypeFixedBits INTSPEC The container width in bits for the float2fixed converter: 64 (an int64 container, the default) or 32 (an int32 container). A 32-bit container is the same size as a C float, so lowering float storage (locals, buffers, struct fields) keeps its layout size-safe -- a float[N] becomes an int32[N] of the same footprint. It halves the memory of lowered data and is friendlier to 32-bit targets, at a narrower range/precision: with Q16.16 (--LowerTypeFixedFraction=16) the range is about +/-2^15 with resolution 2^-16. A 32-bit container always widens multiply/divide through int64 (so --Allow128BitInts does not apply), and it cannot host the Q32.32 libFixed transcendental tier, so any routed sqrt/exp/... falls back to the value boundary (a cleartext libm call on the decoded double) instead. The fraction must satisfy 1 <= n < bits. Default=64.
--LowerTypeBoundary refuse, convert What to do at a frontier that cannot be lowered in place -- a written shared global, or a local whose address escapes. It does not affect truly un-representable constructs (infinity/NaN, a :region qualifier, an unsupported expression), which are refused under either policy. Default=convert.
  • refuse = Fail with a diagnostic naming the site (strict).
  • convert = Leave the object IEEE and route its accesses through the value boundary (best-effort: the program still lowers and runs, the arithmetic is still fixed-point, only that object's storage stays a true double).
--LowerTypeSpecials refuse, exclude, sentinel, saturate How a lowered function that constructs or classifies a special value (infinity / NaN via INFINITY, NAN, isnan, isinf, ...) is handled. Fixed-point has no infinity or NaN, so this is the dedicated policy axis for them (independent of --LowerTypeBoundary, which governs the memory/ABI frontiers). A system inf/NaN helper the function set sweeps in is never lowered regardless of this setting. Default=exclude.
  • refuse = Fail with a diagnostic (LT-SPECIAL) naming the site.
  • exclude = Leave the function in IEEE floating point (excluded from the lowered set); its callees still lower, reaching it through the value boundary. Behaviour-preserving.
  • sentinel = Rewrite the common idioms so the function lowers at zero runtime cost -- an INFINITY min/max sentinel becomes the largest finite Q value, a NaN constructor becomes 0, and a defensive isnan/isinf/isfinite classifier folds to a constant (a Q value is always finite); anything unmatched falls back to exclude. Exact for ordinary inputs, but a genuine infinity/NaN INPUT is treated as finite.
  • saturate = Represent infinity/NaN with reserved Q sentinel values and propagate them through every operation (branch-free), so the function lowers faithfully; costs a few integer ops per operation and a few values at the range extremes.
--LowerTypeInline BOOLSPEC Inline the embedded libFixed transcendental helpers (and their integer dependencies) into the caller, transitively, so the fixed-point approximation's integer arithmetic (shifts, multiplies, the polynomial evaluation) is exposed rather than hidden behind a call. This lets a following EncodeArithmetic obfuscate it and the C compiler optimise it; the now-unused library definitions can be dropped with --Transform=CleanUp --CleanUpKinds=removeUnusedFunctions. Turn it off to keep the transcendentals as calls. Default=true.

Example

Before:

double diagonal(double w, double h) {
   double area;

   area = w * h;
   return sqrt(area) / 2.0;
}

After --LowerTypeConvert=float2fixed --LowerTypeFixedFraction=24. The arguments are converted to Q40.24 on entry, the multiply and divide carry the Q-format scaling, sqrt becomes a call into the embedded libFixed, and the result is converted back:

typedef long long PREFIX_FIXED_Q40p24;

double diagonal(double w, double h) {
   PREFIX_FIXED_Q40p24 area, t, w_fx, h_fx;

   w_fx = (long long)(w * 16777216.);
   h_fx = (long long)(h * 16777216.);
   area = w_fx * h_fx >> 24;
   t    = fixed__sqrt__q32p32__relerr_0p000001(area << 8) >> 8;
   return (double)((t << 24) / 33554432LL) / 16777216.;
}

Lowered variables get the distinctive typedef PREFIX_FIXED_Q<m>p<n>, so a reviewer can see which values were converted and in what format.


What it covers

  • Locals, parameter shadows, and local float arrays, whose element type becomes the container.
  • Arithmetic: + and - are plain integer ops, * and / apply the Q-format scaling, and comparisons are direct integer compares (a Q-format is linear and monotone).
  • Transcendentalssqrt, hypot, log, exp, pow, sin, cos, floor, round, and friends — are routed to the embedded libFixed. (int)floor(x) becomes the integer part.
  • Other calls cross the value boundary: float arguments are decoded, a result assigned to a lowered variable is re-encoded. So lowered code composes with printf, un-lowered helpers, and non-routed libm.
  • Pointer parameters are lowered at the value boundary too: a load encodes, a store decodes. The algorithm is protected in Q-space while the ABI and the buffer stay IEEE — sound whatever the aliasing.
  • Globals and struct fields are retyped in memory when the lowered set is self-contained with respect to them, and otherwise handled at the value boundary.

Q-format and targets

The Q-format is Q(width-n)p(n), chosen with --LowerTypeFixedFraction=n: more fraction bits buy resolution (2^-n) and cost range. Multiply and divide need a widening intermediate, and whether that may be a 128-bit integer is a property of the target, not of this transform — so it comes from the global --Allow128BitInts, which defaults to what the machine model says the target has. Where a 128-bit integer is available the widening is exact for any in-range value; where it is not, the arithmetic stays in 64 bits and the fraction must be chosen so the products fit in int64.

Two cases are worth setting the flag off by hand even though the target does have the type: WebAssembly, where clang does provide __int128 but lowers a multiply or divide to a __multi3 / __udivti3 library call, and any function you intend to --Transform=Jit or Virtualize afterwards.


Composing

Run LowerType before an integer obfuscator, which can then protect the now-integer computation:

tigress --Transform=LowerType --LowerTypeConvert=float2fixed \
          --LowerTypeFixedFraction=24 --Functions=diagonal \
        --Transform=EncodeArithmetic --Functions=diagonal \
        in.c --out=out.c


Limitations

Fixed point is a bounded-range, approximate representation, so LowerType suits tolerant code — measurement, thresholds, geometry, simulation — not bit-exact IEEE work. Rather than mis-lower it refuses what it cannot yet handle: a written float global still reachable from un-lowered code, escaping float struct fields, a local float buffer whose address escapes (external code would read the container integers as doubles), and unsupported callees. Pointer-referenced memory is a boundary, not an escape — but the pointee values themselves are not obfuscated; that needs the whole-program mode.

LowerType preserves the atomic-region and barrier annotations other transforms leave behind, and inserts its own entry conversions through the same mechanism, so it composes safely in a pipeline. Because lowering is type-directed and function-granular — a variable's type spans the whole function — a --Functions=f:region qualifier cannot be honoured and is refused rather than silently widened.