Optimized MCU Code and Strict Aliasing: Replace Unsafe Type-Punning at Binary Boundaries

When a binary conversion works without optimization and changes under an optimized build, inspect the object-access types before blaming the compiler. Casting a pointer does not grant permission to read the same storage through an unrelated type. For an object-representation copy, use a defined byte-copy operation into a correctly declared destination, then separately verify size, representation and byte-order requirements.

This guide covers C11 code compiled with GCC 14.2.0. The executed probes ran on an x86-64 host, not an MCU. They demonstrate an aliasing failure pattern and a defined replacement; they do not establish a target’s floating-point encoding, unaligned-access behavior or generated instruction sequence. C++ object-lifetime rules require their own review.

Separate numeric conversion from representation copying

A numeric cast from floating point to integer computes an integer value under the language’s conversion rules. Copying a floating object’s bytes into an unsigned integer preserves a representation instead. These operations answer different questions. First decide whether the application needs a numeric quantity, local representation bits or a protocol-defined byte sequence.

The public WG14 N1570 C11 committee draft, section 6.5, specifies permitted access types and includes character-type access to object representations. Its section 7.24.2.1 defines memcpy() as a byte-copy operation with nonoverlapping objects. Those rules support a copy into a separately declared destination without inventing an incompatible typed pointer into the source.

Binary-boundary decision separates numeric conversion, local object representation copied with memcpy, and protocol bytes decoded explicitly; each path has size representation alignment and lifetime checks.
Choose the intended meaning first. A defined representation copy removes the aliasing shortcut but does not define a wire format.

Recognize the incompatible-pointer pattern

The following original function is intentionally invalid when the two arguments designate overlapping integer and floating storage. It is included only as a diagnostic counterexample, not as production code:

int bad(int *p, float *q) {
    *p = 1;
    *q = 0.0f;
    return *p;
}

An optimizer can reason from the permitted aliasing relationships of the parameter types. The GCC 14.2.0 optimization manual documents strict-aliasing optimization and enables it at -O2, -O3 and -Os. A program that violates the underlying access rules has no reliable result to preserve.

The host fixture passed addresses of overlapping union members to a no-inline version of this function. It printed zero with -O0 and one with -O2. Those are observations of this build, not required outcomes of undefined behavior. They do not show a compiler defect or promise that another compiler will fail in the same visible way.

GCC documents specific type-punning behavior when access occurs through a union type. That does not make every pointer obtained from a union safe for unrelated typed access. Nor does adding an intermediate void * change the type of the object ultimately accessed. Review the final dereference, not only the cast syntax.

Use a declared destination for representation bits

This original replacement was compiled and executed in the fixture:

uint32_t representation(float value) {
    uint32_t bits;
    _Static_assert(sizeof bits == sizeof value,
                   "equal object sizes required");
    memcpy(&bits, &value, sizeof bits);
    return bits;
}

The translation unit includes stdint.h and string.h. Its separately declared uint32_t destination avoids an incompatible integer dereference into the float object. The size assertion prevents an accidental over-read or partial representation copy when the assumed sizes differ.

On the tested host, copying 1.0f produced hexadecimal 3f800000 at both optimization levels. The optimized representation function used a register move and return, with no external memcpy call. This demonstrates that the defined source form need not imply a costly library call in that build; it is not a performance guarantee for other targets.

Keep the remaining binary-boundary checks

  • Representation: equal size does not prove that a target uses the floating-point format expected by a protocol. Copying arbitrary incoming bytes into a float can create an unsupported or problematic representation.
  • Byte order: a local representation integer is not automatically a serialized network field. Decode and encode the protocol’s byte order explicitly.
  • Alignment: a byte buffer may not be aligned for a typed pointer cast. Copying into a properly declared local object avoids requiring the source byte array to have that destination alignment.
  • Extent and overlap: validate source and destination lengths. Use the appropriate operation if regions overlap; ordinary memcpy does not permit overlap.
  • Lifetime and concurrency: the source must still exist and remain stable during copying. Memcpy does not make concurrent modification safe.

For structured packets, do not replace every cast with one memcpy into a native structure and declare the format solved. Padding, field layout and representation can remain incompatible. A hypothetical device receiving a compact status frame should decode fields according to the frame specification into application objects, rather than depend on the compiler’s structure layout.

Diagnose an optimization-sensitive failure systematically

  1. Reduce the failing path to the exact reads and writes around the binary boundary. Preserve the original types and optimization flags.
  2. Identify the declared or effective type of each object and every final access through a pointer.
  3. Check alignment, bounds and lifetime independently; several undefined behaviors can coexist.
  4. Replace representation reinterpretation with a defined copy or explicit field decoding, without changing the intended value semantics.
  5. Compare optimized behavior and inspect generated code for the corrected form. Keep test vectors for boundary values and malformed inputs.

Disabling strict-aliasing optimization can be a diagnostic experiment, but it does not repair alignment, lifetime or data races. Treat a changed symptom as evidence to investigate, not a complete fix. The executed host probes used GCC 14.2.0 with C11 and separate O0/O2 builds; no MCU code or sanitizer run is claimed.

The acceptance criterion is a defined access path whose representation assumptions are documented and tested on the target. Once that is established, optimization becomes a code-generation and performance question rather than an accidental test of whether invalid pointer reinterpretation happens to work.

Similar Posts