C Part II · Module 14 · C Through a Hardware Engineer's Eyes
Alignment, Pointer Casts and Unaligned Access
Read a 32-bit value out of a byte buffer at an odd offset by casting the pointer. On this machine it returns the right answer, exits cleanly, and produces no warning at any level — and UndefinedBehaviorSanitizer calls it a misaligned load. This chapter measures all three facts together, then measures the one that settles the argument: the defined way of doing it compiles to exactly the same single instruction, so the undefined version buys nothing at all. It also records a finding worth more than the rest of the chapter — the void-pointer cast that people add to silence the alignment warning removes the only diagnostic and leaves the undefined behaviour untouched.
Advanced23 min readCPart IIHardwareCorrectness
Part II · Module 14 · Chapter 14.4 · Alignment, Pointer Casts and Unaligned Access
1. What an Alignment Requirement Is
Every object type in C has an alignment requirement: a number, always a power of two, such that the address of any object of that type must be a multiple of it. Chapter 10.5 met this indirectly, as the reason a structure acquires padding. Here it is directly, through the C11 operator that reports it:
#include <stdio.h>
int main(void) {
printf("_Alignof: int %zu long %zu double %zu\n",
_Alignof(int), _Alignof(long), _Alignof(double));
return 0;
}_Alignof: int 4 long 8 double 8_Alignof is C11 and available in C17; C23 spells it alignof without a header, and <stdalign.h> provides that spelling in C11 and C17. The companion _Alignas — used later in this chapter — specifies an alignment rather than reporting one.
So on this machine a uint32_t object must live at an address divisible by 4. That is not a performance preference. It is a property of the type, and the standard states the consequence plainly: converting a pointer to a type with a stricter alignment requirement than the object actually has produces undefined behaviour if the resulting pointer is not correctly aligned (C17 §6.3.2.3 paragraph 7).
A unsigned char array has an alignment requirement of 1, so it may start anywhere and any offset into it is a valid unsigned char *. Casting such a pointer to uint32_t * asserts something about the address that the array never promised.
2. The Measurement
#include <stdio.h>
#include <stdint.h>
#include <string.h>
int main(void) {
_Alignas(8) unsigned char buf[16] = {
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
};
printf("buf address %% 8 = %lu (aligned to 8)\n",
(unsigned long)((uintptr_t)buf % 8));
uint32_t v;
memcpy(&v, buf + 1, sizeof v); /* defined */
printf("memcpy from buf+1 : 0x%08x\n", v);
const uint32_t *p = (const uint32_t *)(const void *)(buf + 1); /* not */
printf("cast from buf+1 : 0x%08x\n", *p);
return 0;
}The buffer itself is deliberately aligned to 8, so buf + 1 is at an odd address by construction rather than by luck.
$ cc -std=c17 -Wall -Wextra -Wpedantic -O2 ch144.c -o ch144 && ./ch144
buf address % 8 = 0 (aligned to 8)
memcpy from buf+1 : 0x44332211
cast from buf+1 : 0x44332211
exit=0Both produced the same value. The program exited normally. No warning was emitted at any level.
Now the same binary built with UndefinedBehaviorSanitizer, at the -O1 -g that Chapter 13.10 recommends:
ch144.c:19:44: runtime error: load of misaligned address 0x00016b6fa711
for type 'const uint32_t' (aka 'const unsigned int'),
which requires 4 byte alignment
0x00016b6fa711: note: pointer points here
00 00 00 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ch144.c:19:44Three facts hold simultaneously, and holding all three at once is the skill this chapter is teaching:
- the cast version gave the correct answer
- the cast version did not fault
- the cast version is undefined behaviour, named precisely by the sanitizer
Chapter 13.2 spent an entire chapter establishing that the third fact is not refuted by the first two. This is that lesson, in the place where hardware engineers are most tempted to forget it.
3. What It Costs: Nothing, Measured
The usual defence of the cast is performance. memcpy looks like a function call; the cast looks like a load. Compile all three plausible ways to read four bytes and read what the compiler actually emits:
#include <stdint.h>
#include <string.h>
uint32_t via_memcpy(const unsigned char *b) { uint32_t v; memcpy(&v, b, 4); return v; }
uint32_t via_cast (const unsigned char *b) { return *(const uint32_t *)(const void *)b; }
uint32_t via_shifts(const unsigned char *b) {
return (uint32_t)b[0] | (uint32_t)b[1] << 8
| (uint32_t)b[2] << 16 | (uint32_t)b[3] << 24;
}$ cc -std=c17 -O2 -S gen.c -o -
_via_memcpy:
ldr w0, [x0]
ret
_via_cast:
ldr w0, [x0]
ret
_via_shifts:
ldr w0, [x0]
retIdentical. One instruction each.
memcpy with a constant size is not a function call — the compiler recognises it and emits the load directly. The undefined version is not faster, because there is nothing to be faster than. The shift version from Chapter 14.3, which additionally does not care about byte order, is also the same instruction here.
4. The Finding: Silencing the Warning Keeps the Bug
This is the most practically useful measurement in the chapter.
There is a flag that diagnoses the cast. It is -Wcast-align, and almost nobody has it switched on: it is not in -Wall, not in -Wextra, and — checking honestly — not in Chapter 13.7's recommended set either, which has -Wcast-qual for a different purpose and does not list this one. So a project following this curriculum's own advice to the letter has still never seen this diagnostic. Add it.
#include <stdint.h>
uint32_t direct (const unsigned char *b) { return *(const uint32_t *)b; }
uint32_t viavoid(const unsigned char *b) { return *(const uint32_t *)(const void *)b; }$ cc -std=c17 -Wall -Wextra -Wcast-align -c castalign.c
castalign.c:2:51: warning: cast from 'const unsigned char *' to
'const uint32_t *' (aka 'const unsigned int *')
increases required alignment from 1 to 4 [-Wcast-align]
2 | uint32_t direct(const unsigned char *b) { return *(const uint32_t *)b; }
1 warning generated.One warning. On direct. Nothing on viavoid.
The two functions are the same program. They have the same undefined behaviour at the same offsets. The only difference is an intermediate cast to void * — and a cast to void * is always allowed, so the compiler has nothing left to object to by the time it reaches the second cast.
That matters because of how the void * gets there. Nobody writes it first. It gets added after -Wcast-align fires, by someone making the warning go away. The sequence is: the compiler diagnoses a real defect, a developer silences the diagnosis without changing the defect, and the code now looks cleaner than the version that was honest about what it did.
Chapter 13.7's instruction was to read warnings rather than count them. This is the cost of the other habit, measured: a build that went from one warning to zero and from one undefined behaviour to one undefined behaviour.
The fix is memcpy, which §3 measured as the same instruction, is defined at every offset, and gives the compiler nothing to warn about because there is nothing wrong.
5. Why the Conversion Is Implementation-Defined, Not Merely Risky
Chapter 14.6 deals with address maps properly, but the type rule belongs here because it is an alignment rule.
Converting an integer to a pointer, or a pointer to an integer, is implementation-defined — Chapter 13.3's middle category. The standard (C17 §6.3.2.3 paragraphs 5 and 6) says the result is implementation-defined, might not be correctly aligned, might not point to an object, and may be a trap representation. It does not say the conversion is meaningless; it says the implementation must document what it does, and you may rely on that documentation.
This is a genuinely different status from undefined behaviour, and Chapter 13.3 argued that the difference is the whole basis of Part II. Writing a device address as an integer and converting it to a pointer is not a hack — it is the standard-sanctioned mechanism, usable precisely because the implementation documents it. What stays undefined is dereferencing the result when it is misaligned for the type, or when it does not denote an object at all.
So the three things come apart cleanly:
| The operation | Status |
|---|---|
(uintptr_t)p and back again | implementation-defined; round trip preserved here |
(uint32_t *)some_integer | implementation-defined; the implementation documents it |
| dereferencing it when misaligned | undefined |
The practical rule follows directly: get the alignment right, and the conversion is a documented platform mechanism rather than a gamble. A register at 0x40021000 is 4-byte aligned because the hardware designer made it so; a byte offset into a packet buffer is not aligned to anything, which is why packets get memcpy and registers get a typed pointer.
6. What It Breaks, and an Honest Non-Result on What It Costs
What it breaks is three things, in increasing order of how hard they are to debug.
It can fault. Many processors raise an alignment exception on a misaligned wide access. Not this one, for ordinary loads — §2 measured that — but the architecture does not promise it, the operating system's trap-and-emulate behaviour is configurable on some targets, and other ARM implementations differ.
It can be silently slow. Where a misaligned access is permitted, it may be implemented as two bus transactions plus a merge. On a processor that is permissive this is invisible except in a profile.
It licenses the optimiser. This is the one that actually bites. If a pointer has type uint32_t *, the compiler is entitled to assume its target is 4-byte aligned, and it may use that assumption — to vectorise a loop, to choose a wider instruction, to combine two accesses. The code that breaks is then not the line with the cast but a loop three functions away that was compiled under an assumption the cast quietly falsified. That failure survives code review indefinitely, because the line that is wrong looks fine.
7. The Misconception: "This Is an ARM Problem"
"ARM faults on unaligned access and x86 does not, so this matters on embedded targets and not on a PC."
Both halves are measurably wrong, and the second half is the dangerous one.
The first half: this is an ARM machine, and §2's misaligned load did not fault. ARMv8 permits unaligned access for ordinary loads and stores to normal memory. The folklore is a memory of ARMv5 and earlier, where it would silently return a rotated value rather than faulting — which is worse than a fault, and is where the reputation came from.
The second half is the real error, because it frames the hazard as "will it crash?" The hazard is §6's third item: the compiler is entitled to assume alignment and to optimise on that assumption. That entitlement is identical on x86-64, where nothing faults and everything still breaks when the optimiser acts on a promise your cast made falsely. A program that has worked on x86 for ten years has demonstrated that its particular compiler versions never happened to exploit the assumption. It has not demonstrated that the next one will not.
The correct frame is not which processors fault. It is:
A pointer's type is a promise about its target's alignment. The compiler believes the promise. Make it only when it is true.
And since §3 measured that keeping the promise costs zero instructions, there is nothing on the other side of the trade.
8. Doing It Properly
Four rules cover essentially all of it.
Read and write unaligned data with memcpy. It is defined at every offset, compiles to the same instruction, and needs no flag to protect it. For a packet buffer or a file record, this is the default and the other options are not improvements.
uint32_t v;
memcpy(&v, buf + offset, sizeof v);Declare the alignment you need rather than hoping for it. If a buffer will be accessed as wider objects, say so where it is defined:
_Alignas(4) unsigned char rx_buffer[1024];Now rx_buffer + 0, + 4, + 8 are genuinely aligned for uint32_t, as a stated property rather than an accident of where the linker put it. _Alignas is C11/C17; <stdalign.h> provides the alignas spelling, which C23 makes a keyword.
Add -Wcast-align to your build and never silence it with void *. It is the one flag in this module that Chapter 13.7's set does not already give you. §4 measured what that costs. If a cast is genuinely correct, it is correct because you have established the alignment — in which case say so, with an assertion that records the reasoning:
assert((uintptr_t)p % _Alignof(uint32_t) == 0);Chapter 13.8 covered what assert is for: recording a belief so that being wrong is loud. This is exactly that, and it is compiled out of the shipping build.
Run the sanitizers on your tests. §2's diagnostic came from UBSan and from nothing else. No warning flag found it, the program ran correctly, and the only reason this chapter can state the fact is that something was watching at runtime. Chapter 13.10's build — -fsanitize=address,undefined -fno-sanitize-recover=all -g -O1 — is where misalignment turns up.
9. What You Should Be Able to Say Now
- Every type has an alignment requirement, a power of two that its address must be a multiple of.
_Alignofreports it (C11/C17;alignofin C23). Measured here:int4,long8,double8. - A
unsigned chararray is aligned to 1, so casting any offset into it to a wider pointer asserts something the array never promised. C17 §6.3.2.3 ¶7 makes the misaligned result undefined. - Measured: the cast at an odd offset returned the right value, did not fault, and warned about nothing — and UBSan named it a misaligned load. All three are true at once.
- It costs nothing to be correct.
memcpy, the cast, and the byte-shift reconstruction all compile toldr w0, [x0]— one identical instruction. - The
void *cast removes the diagnostic and keeps the bug.-Wcast-alignwarns on a direct cast and is silent on the laundered one.-Wcast-alignis in neither-Wall,-Wextra, nor 13.7's set — you have to add it. - Integer-to-pointer conversion is implementation-defined, not undefined (§6.3.2.3 ¶5–6). The implementation documents it and you may rely on that. Dereferencing a misaligned result is what is undefined.
- The real hazard is the optimiser, not the fault. A
uint32_t *is a promise the compiler may act on three functions away. This is identical on x86-64. - A failed measurement is reported as failed. No cost ratio is given for misaligned loads here, because two attempts produced an impossible number and then a faster-when-misaligned number whose sanity check contradicted it.
10. Exercises
- Print
_Alignofforchar,short,int,long,double, a pointer, and a structure containing acharand adouble. Explain the last one from the rule in §1. - Reproduce §2 on your machine: the plain run, then the UBSan run. If yours faults instead of returning a value, say which of §6's three failure modes you observed.
- Compile the three functions from §3 at
-O2 -Sand count instructions. If your target is not arm64, report what you got and whether the three were still identical. - Write
directandviavoidfrom §4 and confirm the one-warning, zero-warning result. Then write thememcpyversion and confirm it needs no flag. - Declare a 1024-byte receive buffer with
_Alignas(4), then write a function that reads auint32_tfrom a caller-supplied offset. Decide whether it should usememcpyor a cast, and defend the choice in one sentence. - Add the
assertfrom §8 to a function that casts a pointer, then call it with a deliberately misaligned pointer in a debug build. Compare the message with UBSan's for the same program. - Attempt §6's timing measurement yourself. Whatever you get, apply the sanity check: halve the buffer and confirm the time roughly halves. Report your result including whether the check passed — a failed check is the expected outcome and is the point of the exercise.
11. Summary
- Alignment requirement: a power of two the address must divide by.
_Alignofreports it;_Alignasdemands it. Measuredint4,long8,double8. - A
unsigned chararray is aligned to 1; casting an offset into it to a wider pointer is UB when misaligned (C17 §6.3.2.3 ¶7). - Measured together: right answer, no fault, no warning — and UBSan calls it a misaligned load. All three at once.
memcpy, the cast and the shift form all compile toldr w0, [x0]. Correctness costs zero instructions.- The
void *intermediate deletes the-Wcast-aligndiagnostic and leaves the undefined behaviour. And-Wcast-alignis in neither-Wall,-Wextra, nor 13.7's set — it is the one flag this module adds. - Integer↔pointer conversion is implementation-defined, documented, and usable. Only the misaligned dereference is undefined.
- The hazard is the optimiser's entitlement to believe the pointer type, not the fault — and that is the same on x86-64.
- No cost ratio is published: two attempts failed their own sanity checks, and a failed measurement is reported as failed.
Every example so far has read one value out of a buffer at a time, and memcpy handled each in isolation. Real formats do not arrive one field at a time — they arrive as a header with half a dozen fields at fixed offsets, and C has a construct that looks purpose-built for describing exactly that. Whether it may be pointed at the bytes is a question of padding and alignment together, and the answer is not the one the construct's appearance suggests.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
