Table of Contents
Unaligned Access
Unaligned memory access is one of the most common reasons software fails when first built for Alpha. Every Alpha load and store except LDQ_U and STQ_U traps when the address is not a multiple of the operand size, and no implementation corrects the access in hardware.
The construct responsible is almost always a pointer cast such as *(uint32_t *)(buf + 1). It is undefined behavior in C on every target, including x86, where the hardware ordinarily tolerates it; the sections below include an x86-64 program that fails because of it. Alpha differs from such targets only in reporting the error immediately.
The fix is to load and store through memcpy, which compilers reduce to the right instruction sequence for the target:
uint32_t val; memcpy(&val, buf + 1, sizeof val); /* not: val = *(uint32_t *)(buf + 1); */
Kernel code uses get_unaligned() and put_unaligned() instead; see In kernel code.
See also Byte and Word Access and Memory Model.
What counts as aligned
An access is aligned when the address is a multiple of the operand size.
| Instruction | Operand | Required alignment |
|---|---|---|
LDBU / STB | byte | any address |
LDWU / STW | word, 16-bit | address % 2 == 0 |
LDL / STL | longword, 32-bit | address % 4 == 0 |
LDQ / STQ | quadword, 64-bit | address % 8 == 0 |
LDS / STS | S_floating, 32-bit | address % 4 == 0 |
LDT / STT | T_floating, 64-bit | address % 8 == 0 |
LDL_L / STL_C | longword, 32-bit | address % 4 == 0 |
LDQ_L / STQ_C | quadword, 64-bit | address % 8 == 0 |
LDQ_U / STQ_U | quadword | none; address bits <2:0> are ignored |
LDBU, LDWU, STB and STW are the byte/word extension (BWX, amask bit 0), introduced with the EV56 (21164A). No Alpha has an LDB or LDW instruction. Earlier parts have no byte or word memory instructions at all; see Byte and Word Access.
Every load and store except LDQ_U/STQ_U faults on an unaligned address. From the Architecture Reference Manual, on LDS:
If the data is not naturally aligned, an alignment exception is generated.
The same sentence appears for LDT, STS and STT.
No Alpha implementation fixes unaligned accesses up in hardware. The unaligned data reference is an architecturally defined trap on every part from the 21064 to the 21364.
BWX does not change this. It adds byte and word instructions; it does not make misaligned longword or quadword access work.
What Alpha does at runtime
An unaligned load or store traps to PALcode, which reflects the trap to the kernel. User-space accesses are handled by do_entUnaUser() in arch/alpha/kernel/traps.c; kernel-mode accesses go to do_entUna() in the same file, which fixes up only accesses covered by an exception-table entry and otherwise panics.
By default the kernel emulates the access, logs a message, and lets the process continue. It emulates the integer loads and stores LDWU, LDL, LDQ, STW, STL, and STQ, and the floating-point LDS, LDT, STS, and STT. Any other instruction that traps on a misaligned address, in particular the load-locked and store-conditional instructions LDL_L, LDQ_L, STL_C, and STQ_C, is never emulated and always results in SIGBUS. A misaligned atomic operation, for example on a member of a packed structure, therefore crashes the program regardless of the settings below. 1)
The behavior is controlled per process by three bits:
#define UAC_BITMASK 7 #define UAC_NOPRINT 1 #define UAC_NOFIX 2 #define UAC_SIGBUS 4
The log message
printk("%s(%d): unaligned trap at %016lx: %p %lx %ld\n", current->comm, task_pid_nr(current), regs->pc - 4, va, opcode, reg);
| Field | Meaning |
|---|---|
%s(%d) | process name and PID |
%016lx | address of the faulting instruction (the trap PC points past it, hence the - 4) |
%p | the unaligned virtual address |
%lx | the memory-format opcode, in hex: 0c=LDWU, 0d=STW, 22=LDS, 23=LDT, 26=STS, 27=STT, 28=LDL, 29=LDQ, 2c=STL, 2d=STQ |
%ld | the instruction's Ra register number, in decimal. This is the destination of a load or the source of a store. It is not a cause code. |
Two properties of the message limit its use:
- The
%paddress is hashed on any modern kernel unless it is booted withno_hash_pointers. The instruction address is real; the data address is not. - The message is rate limited to 5 per 5 seconds. A hot loop loses almost all of them, so
dmesgoutput cannot be used to count occurrences.
Counting them properly
Alpha exports cumulative, un-rate-limited, system-wide counters:
$ grep unaligned /proc/cpuinfo kernel unaligned acc : 0 (pc=0,va=0) user unaligned acc : 1174 (pc=120001234,va=11ffff8e5)
Sampling these counters before and after a run shows whether any unaligned access occurred during the run. The kernel keeps one counter for all user processes, so on a busy system other processes also increment it.
Making it fatal
The portable interface works on Alpha:
#include <sys/prctl.h> /* SIGBUS on any unaligned access in this process */ prctl(PR_SET_UNALIGN, PR_UNALIGN_SIGBUS, 0, 0, 0); /* fix up silently: no log message, no signal */ prctl(PR_SET_UNALIGN, PR_UNALIGN_NOPRINT, 0, 0, 0);
PR_SET_UNALIGN replaces the mask rather than OR-ing into it. Alpha also accepts a third bit, bare value 4, mapping to TS_UAC_NOFIX ("do not fix up at all"), which has no PR_UNALIGN_* name.
The Alpha-specific interface inherited from Digital UNIX is osf_setsysinfo(). glibc does not wrap it, so it must be invoked directly:
#include <asm/sysinfo.h> #include <sys/syscall.h> #include <unistd.h> unsigned int buf[2] = { SSIN_UACPROC, UAC_SIGBUS }; syscall(__NR_osf_setsysinfo, SSI_NVPAIRS, buf, 1, 0, 0);
The third argument is the number of name/value pairs in the buffer; the kernel processes that many pairs, so it must be given. 2)
The two interfaces assign different meanings to the same bit values: the value 2 is UAC_NOFIX to osf_setsysinfo() and PR_UNALIGN_SIGBUS to prctl().
The interface is modeled on Digital UNIX's setsysinfo(2), 3) but only the per-process SSIN_UACPROC setting is implemented, not Digital UNIX's system-wide SSIN_UACSYS or per-parent SSIN_UACPARNT. 4)
Setting one of these at the top of main() in a test harness turns every unaligned access into a core dump with a usable backtrace instead of a line in dmesg.
Absence of a sysctl
The command echo 2 > /proc/sys/kernel/unaligned-trap does not work on Alpha. The unaligned-trap sysctl exists only on architectures that opt in to it (parisc, arc, loongarch and riscv), takes only the values 0 and 1, and is not available on Alpha, which registers no sysctls of its own. Alpha's only controls are the two per-process interfaces described above.
Other operating systems
The other Alpha operating systems, NetBSD, FreeBSD, and Tru64 UNIX, also fix up user unaligned accesses by default and print a warning, with system-wide or per-process controls of their own. 5) 6)
What an unaligned access costs
Each fixed-up access costs a trap into the kernel, and silencing the log message does not remove that cost. 7) Reliable per-instruction figures are not available, but the total can be large: rendering a single web page with Ghostscript once incurred over 200 million unaligned accesses, and fixing them reduced the render time from more than 30 seconds to one or two. 8)
The cost has the following structure. A fixed-up unaligned access pays for, at minimum: the PALcode unaligned trap; entry to do_entUnaUser(); decode of the faulting instruction; two LDQ_U plus extract and merge for a load, or two LDQ_U, mask, insert and two STQ_U for a store; a write back into the saved register file; and the return to user mode. On top of that come the instruction-cache and branch-predictor damage of the round trip, and a rate-limited printk if logging is still enabled.
The sequence is orders of magnitude more expensive than the single instruction it replaces, being a trap into the operating system rather than a pipeline stall.
A figure for particular hardware can be obtained with an rpcc-timed loop comparing aligned against +1-offset accesses, with UAC_NOPRINT set so that logging falls outside the measurement.
What the compiler emits instead
Alpha is a STRICT_ALIGNMENT target. When GCC cannot prove that a pointer is aligned, which is the default for a char * or a memcpy source, it emits the extract sequence rather than a faulting load. The Architecture Handbook gives the canonical form:
LDQ_U R1, X(R11) ; Ignores va<2:0>, R1 = CBAx xxxx LDQ_U R2, X+3(R11) ; Ignores va<2:0>, R2 = yyyy yyyD LDA R3, X(R11) ; R3<2:0> = (X mod 8) = 5 EXTLL R1, R3, R1 ; R1 = 0000 0CBA EXTLH R2, R3, R2 ; R2 = 0000 D000 OR R2, R1, R1 ; R1 = 0000 DCBA
In practice, for uint32_t v; memcpy(&v, p, sizeof v); return v;, alpha-unknown-linux-gnu-gcc -O2 produces the following, whatever the -mcpu setting:
ldq_u $0,0($16) ldq_u $1,3($16) extll $0,$16,$0 extlh $1,$16,$1 bis $0,$1,$0 addl $31,$0,$0 /* sign-extend to canonical 64-bit form */
Six instructions instead of one. The aligned version, return *(const uint32_t *)p;, is a single ldl. An unaligned 32-bit store is worse still: two LDQ_U, MSKLL/MSKLH, INSLL/INSLH, two BIS and two STQ_U.
Six instructions is the cheaper case, in which the compiler treated the pointer as possibly misaligned. The expensive case is a plain LDL, emitted because the pointer was declared uint32_t *, which the hardware then traps.
In an optimized build memcpy of a scalar is not a function call on any target: on x86-64 it is a single movl, and on Alpha it is the sequence above. memcpy(&v, p, sizeof v) is the portable spelling of a load performed however the machine requires, and unlike the cast it is well-defined C.
Finding unaligned accesses
UBSan on a non-Alpha host
UBSan detects most alignment defects without Alpha hardware.
gcc -g -fsanitize=undefined -fno-sanitize-recover=alignment myfile.c -o mybinary ./mybinary
u.c:4:55: runtime error: store to misaligned address 0x561495a5f121 for type 'uint32_t', which requires 4 byte alignment
0x561495a5f121: note: pointer points here
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
^
-fsanitize=alignment is already part of -fsanitize=undefined. -fno-sanitize-recover=alignment makes the first occurrence abort; by default UBSan prints a diagnostic and continues, which is unsuitable for a test suite.
-Wcast-align and -Wcast-align=strict
-Wcast-align warns only on targets that require alignment, and is a silent no-op on x86, so on an x86 build host it reports nothing. From gcc/doc/invoke.texi:
@item -Wcast-align
Warn whenever a pointer is cast such that the required alignment of the
target is increased. For example, warn if a @code{char *} is cast to
an @code{int *} on machines where integers can only be accessed at
two- or four-byte boundaries.
@opindex Wcast-align=strict
Warn whenever a pointer is cast such that the required alignment of the
target is increased. For example, warn if a @code{char *} is cast to
an @code{int *} regardless of the target machine.
The difference is target sensitivity. Neither variant inspects the offset; both fire on the cast, which means =strict will also warn about casts that happen to be fine. It is a lint, not a proof.
char buf[64]; uint32_t *p(void) { return (uint32_t *)(buf + 1); }
$ gcc -c -Wcast-align x.c # x86-64: nothing $ gcc -c -Wcast-align=strict x.c x.c:3:27: warning: cast increases required alignment of target type [-Wcast-align] $ alpha-unknown-linux-gnu-gcc -c -Wcast-align x.c x.c:3:27: warning: cast increases required alignment of target type [-Wcast-align]
GDB
gdb ./mybinary (gdb) handle SIGBUS stop print (gdb) run
(gdb) bt (gdb) x/i $pc # the faulting instruction (gdb) p $_siginfo.si_code # 1 == BUS_ADRALN (gdb) p/x $_siginfo._sifields._sigfault.si_addr # the misaligned address
The kernel rewinds $pc to the faulting instruction before delivering the signal, and sets si_addr to the unaligned address:
give_sigbus: regs->pc -= 4; send_sig_fault(SIGBUS, BUS_ADRALN, va, current); return;
$_siginfo is populated when GDB intercepts a live signal under handle SIGBUS stop, and also when it opens a core file, which carries the signal information in an NT_SIGINFO note.
Kernel log and addr2line
This method is limited by the hashing and rate limiting described above. The faulting PC is the field ending in a colon:
dmesg -t | sed -n 's/.*unaligned trap at \([0-9a-f]*\):.*/0x\1/p' |
sort -u | while read -r pc; do
addr2line -e ./mybinary -f -C "$pc"
done
addr2line expects a link-time address, so for a PIE or a shared library the mapping base must be subtracted first. Function names come from the symbol table; file names and line numbers require a binary built with -g.
Detection workflow
The steps below combine the tools described above.
- Build with
-g -fsanitize=undefined -fno-sanitize-recover=alignment -Wcast-align=strict. The=strictform is required, since the plain form does nothing on x86. - Run the test suite on x86. UBSan finds the majority of alignment defects without Alpha hardware.
- On Alpha, call
prctl(PR_SET_UNALIGN, PR_UNALIGN_SIGBUS, 0, 0, 0)at the top ofmain()and run undergdbwithhandle SIGBUS stop print. - Compare
grep unaligned /proc/cpuinfobefore and after a run as a regression check, on an otherwise idle system, since the counters are system-wide. - Repair the access with
memcpy, or withget_unaligned()in kernel code. Casting throughvoid *silences the warning without removing the undefined behavior.
Common causes
Packed structures
struct __attribute__((packed)) header { uint8_t type; uint32_t length; /* offset 1 */ uint64_t checksum; /* offset 5 */ };
Reading h→length through a struct header * is fine, because __packed tells the compiler the field is unaligned and it emits the extract sequence. The defect is taking the address of a packed field and passing it on as an ordinary pointer, which discards the alignment information. GCC's -Waddress-of-packed-member diagnoses this.
Pointer arithmetic on byte buffers
/* WRONG */ uint32_t val = *(uint32_t *)(buf + 3); /* RIGHT */ uint32_t val; memcpy(&val, buf + 3, sizeof val);
Network and file format parsing
Wire and file formats pack fields without padding, and a receive buffer offset by a header length is aligned only by chance. IP headers, DOS partition tables, and DWARF exception frame information all have unaligned fields. Casting a pointer into such a buffer to a pointer to a larger type creates a potential alignment defect, and, unless the original type is a character type, a strict aliasing defect as well; the two are separate problems in the same line of code (see It is undefined behavior in C). 9)
The same problem is the reason some network drivers copy received packets on Alpha and other strict-alignment architectures: a card that can only DMA to a 32-bit boundary leaves the IP header misaligned, so the driver copies each packet to an aligned buffer (rx_copybreak set to the full MTU) rather than let the TCP/IP code trap on every access. 10)
/* WRONG on Alpha, undefined everywhere */ struct ip_header *ip = (struct ip_header *)packet; uint16_t len = ntohs(ip->total_length); /* RIGHT */ struct ip_header ip; memcpy(&ip, packet, sizeof ip); uint16_t len = ntohs(ip.total_length);
Type punning through a union
union { char bytes[8]; uint64_t qword; } u;
This is safe, and is a type-punning idiom that C (but not C++) explicitly permits. The union carries the alignment of its strictest member. Problems arise only when the union is embedded in a packed structure or placed at a computed offset.
In kernel code
Kernel code uses get_unaligned() and put_unaligned():
#include <linux/unaligned.h>
asm/unaligned.h and asm-generic/unaligned.h no longer exist; they were consolidated into linux/unaligned.h.
The helpers were once implemented with a packed structure. The current implementation is based on memcpy, and its comment gives the reason:
/**
* __get_unaligned_t - read an unaligned value from memory.
* …
* Use memcpy to affect an unaligned type sized load avoiding undefined behavior
* from approaches like type punning that require -fno-strict-aliasing in order
* to be correct. …
*/
It is undefined behavior in C
The relevant rule is about the cast, not the dereference. C11/C17 §6.3.2.3 paragraph 7:
A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.
and Annex J.2, in the list of undefined behaviors:
Conversion between two pointer types produces a result that is incorrectly aligned (6.3.2.3).
Three consequences follow:
- The undefined behavior occurs at the cast.
(uint32_t *)(buf + 1)is undefined even if the pointer is never dereferenced, which is why-Wcast-align=strictdiagnoses the cast and never examines the offset. - There is no exception for hardware that tolerates the access. x86 guarantees that the hardware will not trap on an ordinary integer load. It guarantees nothing about what the compiler may assume or emit.
- The rule is distinct from strict aliasing, which is §6.5 paragraph 7, about effective types.
*(uint32_t *)(buf + 1)on achararray violates both rules, and-fno-strict-aliasingdisables exploitation of the aliasing rule only. It has no effect on §6.3.2.3p7.
memcpy satisfies both rules at once. It accesses through character-type lvalues, which §6.5p7 explicitly permits, and it performs no misaligned pointer conversion, so §6.3.2.3p7 never engages. It is therefore the recommended construct, and compilers reduce it to the same instructions as a direct load.
Effects on x86
A misaligned pointer in plain C
The following program uses no intrinsics, no inline assembly and no -ffast-math. It uses a type that the ABI requires to be 16-byte aligned, and a pointer that is not.
/* v.c */ typedef int v4si __attribute__((vector_size(16))); void vcopy(v4si *d, const v4si *s) { *d = *s; }
/* main.c */ #include <stdio.h> typedef int v4si __attribute__((vector_size(16))); extern void vcopy(v4si *d, const v4si *s); static char dst[64], src[64]; int main(void) { vcopy((v4si *)(dst + 1), (v4si *)(src + 1)); printf("survived\n"); }
gcc -O2 compiles vcopy to aligned moves, because it is entitled to assume the pointer is aligned:
vcopy: movdqa (%rsi), %xmm0 movaps %xmm0, (%rdi) ret
$ gcc -O2 -c v.c && gcc -O2 main.c v.o -o t && ./t; echo "exit=$?" exit=139
Program received signal SIGSEGV, Segmentation fault. 0x0000555555555184 in vcopy () (gdb) x/i $pc => 0x555555555184 <vcopy+4>: movdqa xmm0,XMMWORD PTR [rsi]
Exit status 139 is SIGSEGV. The #GP fault from MOVDQA on a misaligned operand is delivered as SIGSEGV, which is why si_addr is 0 rather than the offending address. The failure is the ordinary consequence of writing through a pointer that is not aligned as its type requires, on an architecture usually regarded as tolerant of misalignment.
x86 can also trap every misaligned access: with the EFLAGS.AC flag set, it delivers SIGBUS with BUS_ADRALN, the same signal and si_code as Alpha. x86 software tolerates misaligned access only because the flag is normally clear.
Summary of build flags
| Flag | Purpose |
|---|---|
-fsanitize=undefined | includes the alignment check; works on any architecture |
-fno-sanitize-recover=alignment | abort on the first misaligned access instead of printing and continuing |
-Wcast-align=strict | diagnose alignment-increasing casts regardless of target; the form to use on x86 hosts |
-Wcast-align | same, but only on strict-alignment targets; a no-op on x86 |
-Waddress-of-packed-member | catch pointers taken to packed fields |
-g | required for addr2line and useful backtraces |
References
- Alpha Architecture Reference Manual, First Edition, §1.6.5 (aligned and unaligned data), §4.6.2 (the extract, insert and mask instructions, with the canonical unaligned load sequence), §4.8 (the load and store instructions).
- Alpha Architecture Handbook, Version 4, §4.6.2, which reproduces the same sequence in cleaner typesetting.
- ISO/IEC 9899:2011 §6.3.2.3p7, §6.5p7, Annex J.2. The free N1570 draft is textually identical in these clauses.
- ''Documentation/core-api/unaligned-memory-access.rst'', Linux kernel. It argues entirely in machine terms and does not mention undefined behavior; for that, see the
__get_unaligned_tcomment ininclude/vdso/unaligned.h. - Richard Henderson, "Re: Unaligned traps on kernel startup", axp-list, 31 August 1998, on the compiler merging two byte loads in
msdos_partition()and theattribute1)fix.
See Mailing Lists for archive locations.
