Course outline

C / Advanced: Language Boundaries and Systems / Lesson 13

Undefined Behavior, Alignment, and Access Rules

Classify language guarantees precisely and replace representation tricks with operations whose contracts can be justified.

About 5 min readContent reviewed
On this page

Lesson goals

  • Distinguish undefined, unspecified, and implementation-defined behavior.
  • Review alignment, effective type, and sequencing.
  • Use byte access and memcpy for justified representation operations.

Different freedoms require different responses

Undefined behavior imposes no requirements on an execution that encounters it; a crash, a plausible answer, or an optimization-dependent result cannot be promoted to its expected output. Unspecified behavior chooses among permitted alternatives without requiring documentation. Implementation-defined behavior requires the implementation to document its choice. These categories are not interchangeable labels for “it depends.”

For unspecified evaluation order, write code correct for every allowed order. For an implementation-defined representation choice, either avoid dependence or document and verify the target requirement. For undefined behavior, repair the program’s violated precondition. Testing cannot turn an invalid operation into a portable feature merely because its observed result seems stable.

A cast does not create alignment

Every complete object type has an alignment requirement. _Alignof queries that requirement, and _Alignas can request supported alignment for an object. Converting an arbitrary byte pointer to int * does not ensure the address is suitably aligned, does not establish sufficient storage, and does not by itself make access through int valid.

The example requests alignment suitable for max_align_t and checks only relationships, so it does not claim a universal number of bytes. malloc supplies alignment suitable for object types with fundamental alignment; extended over-aligned types may need a supported aligned-allocation facility. For aligned_alloc, obey supported alignment and size-multiple constraints, and check allocation failure.

c Request alignment without inventing a width
#include <stddef.h>
#include <stdio.h>

int main(void)
{
    _Alignas(max_align_t) unsigned char storage[32];
    storage[0] = 1;
    if (_Alignof(max_align_t) < _Alignof(int)) return 1;
    puts(storage[0] == 1 ? "alignment contract checked" : "unexpected");
    return 0;
}
Expected output
alignment contract checked

The object has the requested supported alignment; the program does not cast it into an unrelated object type.

Storage and access type must agree

C17 restricts which lvalue types may access an object’s stored value. Compatible types, corresponding signed or unsigned types, relevant aggregates, and character types have specific allowances; an arbitrary pointer cast does not bypass them. Allocated storage has additional effective-type rules associated with writes and copying. Avoid summarizing this complex rule as “all pointers are interchangeable.”

Character access can inspect an object representation. The next program copies an int representation into unsigned char storage and back into another int of the same type. It does not invent a portable byte order or reinterpret arbitrary bytes as a valid float. memcpy expresses a byte copy without requiring a misaligned typed dereference.

c Copy a representation back to its original type
#include <stdio.h>
#include <string.h>

int main(void)
{
    int source = 123;
    int restored = 0;
    unsigned char representation[sizeof source];
    memcpy(representation, &source, sizeof source);
    memcpy(&restored, representation, sizeof restored);
    printf("restored=%d\n", restored);
    return 0;
}
Expected output
restored=123

The bytes came from a valid int and are restored into another int of the same implementation.

Optimization assumes valid operations

An unsequenced modification and conflicting access to the same scalar object can cause undefined behavior. Expressions that both increment a counter and use it elsewhere are therefore poor places to guess a result. Split the updates and derive the trace from full statements. Function-argument evaluation order is not generally left to right in C17.

Signed overflow and out-of-range shift counts are other frequent violations. A post-overflow comparison is too late to establish safety. Compiler transformations may rely on the fact that well-defined executions do not perform these operations. Turning optimization off can change symptoms, but does not repair the language-level error.

Review obligations before experiments

For a suspicious low-level operation, list the target object, lifetime, bounds, alignment, permitted access type, initialization, and sequencing. Establish each obligation from the surrounding contract. If one is unknown, reduce the code or redesign the interface instead of filling the gap with an address observed in a debugger.

Use warnings, static analysis, and selected sanitizers to collect additional evidence. UBSan does not cover every undefined operation, and some effective-type defects remain difficult to detect dynamically. Maintain separate tests for the repaired defined behavior; never store an undefined program’s observed output as a regression oracle.

Common errors and fixes

A byte-buffer cast works only on one architecture.

Why it happens
Alignment or access-type rules were violated.
How to fix it
Copy into a properly declared object or decode fields explicitly.
Prevent it next time
Require representation and alignment proofs at interface boundaries.

Disabling optimization appears to repair the result.

Why it happens
Undefined behavior changed its symptom.
How to fix it
Find and remove the invalid operation.
Prevent it next time
Run defined regression cases under multiple optimization levels.

A sanitizer accepts the program, so a cast is declared safe.

Why it happens
Instrument coverage was mistaken for a language proof.
How to fix it
Check the applicable lifetime, alignment, and access rules directly.
Prevent it next time
Document obligations even when dynamic checks are silent.

Check your understanding

1. A network field begins at byte offset one. Why should a decoder avoid simply casting its address to a 32-bit integer pointer?

Show worked solution

The address may be insufficiently aligned, the storage may not contain an object of the required effective type, and the network byte order may differ from the host. Decode the documented bytes with unsigned arithmetic, or copy into an appropriate object only when its representation and byte-order requirements are explicitly satisfied. Bounds and available integer width still need validation.

2. Explain what evidence is needed before accepting a claimed output from an expression with two updates of the same scalar.

Show worked solution

First identify whether the updates are sequenced relative to each other under C17. If conflicting modifications are unsequenced, there is no required output and running more compilers does not supply one. Rewrite into separate full statements with an explicit order, then derive and test the new trace. The rewritten program has its own defined contract.

Key takeaways

  • Undefined behavior has no portable expected output.
  • A pointer cast does not establish an object, alignment, or access permission.
  • Use representation copies only under explicit validity and format contracts.

Official references