Accessing members of a structure that is not aligned to the natural alignment of its members can lead to unaligned memory accesses. This can cause performance issues and even runtime errors on some architectures like ARM that do not support unaligned accesses natively. However, there are techniques to safely use the address of an unaligned structure member.
What is Structure Alignment
Structure alignment refers to how the members of a struct are arranged in memory. For optimal performance, compilers will insert padding between struct members to align each member to its natural alignment boundary. For example, a 4-byte integer would be aligned to a 4-byte boundary. This enables the CPU to access the data efficiently in a single load/store instruction.
The natural alignment of a data type is usually its size (in bytes). So a 4 byte int would have an alignment of 4, an 8 byte double would have an alignment of 8, etc. Compilers ensure each struct member is aligned properly, which may require padding between members.
Issues with Unaligned Accesses
If a structure is not aligned properly, then accessing its members will cause unaligned accesses. This can lead to several issues:
- Performance degradation – Unaligned accesses may require multiple memory transactions instead of just one. This can lower performance.
- Unaligned access errors – Some architectures like ARM do not support unaligned accesses natively in hardware. This will cause a runtime error or fault.
- Undefined behavior – C/C++ standards allow compilers to assume aligned accesses. Unaligned access can lead to unexpected results.
Aligning Structures
To avoid issues with unaligned accesses, structures should be aligned properly. There are a few ways to do this in C/C++ code:
- Let the compiler handle alignment automatically by default. Compilers will align members to prevent unaligned accesses.
- Use #pragma pack to specify packing rules for a struct declaration.
- Use alignas specifier if available in C++11 and later to force a struct to be aligned to a certain boundary.
- Manually insert padding fields to align members.
Ensuring the struct has proper alignment prevents unaligned accesses to members later when the struct is used.
Accessing Unaligned Structure Members
There are times when a structure may be unaligned, for example when:
- Interfacing with a hardware register interface that requires specific fixed layout.
- Reading network packets or files formats that have fixed unaligned layouts.
- Casting an unaligned buffer to a structure type.
In these cases, we cannot change the definition of the structure itself to be aligned properly. However, we can still safely access unaligned structure members in a few different ways:
1. Use uint8_t* Pointer
Cast the structure pointer to a uint8_t* pointer. Then use pointer arithmetic to access each unaligned member. This treats the struct as a byte array rather than a properly aligned struct. struct unaligned_struct { uint32_t a; uint8_t b; uint16_t c; }; void func(struct unaligned_struct* s) { uint8_t* bytes = (uint8_t*) s; uint32_t a = *(uint32_t*)bytes; bytes += 4; uint8_t b = *bytes; bytes++; uint16_t c = *(uint16_t*)bytes; }
2. Use memcpy
Use memcpy to copy each unaligned member into a local aligned variable. The memcpy will handle the unaligned access. void func(struct unaligned_struct* s) { uint32_t a; memcpy(&a, &s->a, sizeof(a)); uint8_t b; memcpy(&b, &s->b, sizeof(b)); uint16_t c; memcpy(&c, &s->c, sizeof(c)); }
3. Load Entire Structure then Access Members
Use memcpy to load the entire unaligned structure into an aligned local stack variable. Then access the aligned copy. void func(struct unaligned_struct* s) { struct aligned_struct { uint32_t a; uint8_t b; uint16_t c; } temp; memcpy(&temp, s, sizeof(temp)); uint32_t a = temp.a; uint8_t b = temp.b; uint16_t c = temp.c; }
Using Address of Unaligned Members
Sometimes we need a pointer directly to an unaligned structure member. This should be avoided on architectures like ARM. But can be done carefully using a few methods:
1. Pointer Cast to uint8_t*
Cast the address to a uint8_t* and offset to the member address: uint8_t* ptr = (uint8_t*)&s->b;
2. memcpy Address to Aligned Local
Use memcpy to copy the member address into an aligned local pointer variable: uint32_t* aligned_ptr; memcpy(&aligned_ptr, &s->a, sizeof(aligned_ptr));
3. Union Pointer
Use a union to store the address in an aligned way: union { uint32_t align; uint32_t* ptr; } u; u.ptr = &s->a;
Mitigations on ARM
The ARM architecture generally does not support unaligned accesses in hardware. But there are some mitigations and techniques to allow unaligned access on ARM:
- Some ARM cores implicitly support unaligned accesses using multiple memory transactions. This hurts performance.
- The ARMv6 architecture added support for halfword unaligned accesses using the LDRH/STRH instructions.
- The __packed qualifier tells the compiler a struct does not require alignment. This enables unaligned access to those structs.
- The __attribute__((packed)) attribute can be used on individual struct declarations to ignore alignment.
- Some compilers provide options like -munaligned-access to allow unaligned accesses by emulating them in software.
However, unaligned accesses on ARM still remain slower than naturally aligned access. Avoid whenever possible by properly aligning structures.
Conclusion
Unaligned structure members can be safely accessed in C/C++ using pointer casts, memcpy, or temporary aligned copies. This prevents unaligned access issues. Pointers to unaligned members can also be obtained with careful casting or type punning. Overall, maintaining proper structure alignment is ideal, but unaligned structures can be supported with some extra care on architectures like ARM.