Programming
Is gccs attributepacked pragma pack unsafe
In the complex world of C and C++ programming, optimizing memory layout for structures is a frequent concern, especially in embedded systems or when interfacing with hardware. Two common compiler directives, GCC’s __attribute__((packed)) and the more widely supported pragma pack, allow developers to control how data members are aligned within structures, effectively removing padding bytes. This capability, while powerful for minimizing memory footprint or matching external data formats, often raises a critical question: is using __attribute__((packed)) or pragma pack inherently unsafe? The answer, like many things in low-level programming, is nuanced. While these directives offer significant advantages, their improper application can lead to serious performance penalties, undefined behavior, and portability nightmares. Understanding the underlying mechanisms and potential pitfalls is crucial for any developer considering their use.
Understanding Data Alignment and Padding
Before diving into the specifics of packed structures, it’s essential to grasp the concept of data alignment. Modern computer architectures prefer, and in some cases require, that data be aligned on specific memory boundaries. For instance, a 4-byte integer might perform best, or even only work, if its memory address is a multiple of 4. This is because CPUs often fetch data in chunks (e.g., 4, 8, or 16 bytes), and unaligned access can force the CPU to perform multiple memory accesses, significantly slowing down operations or even leading to hardware exceptions on certain architectures.
Compilers, by default, insert “padding bytes” into structures to ensure that each member is naturally aligned according to its type and the system’s architecture. For example, in a structure containing a char and an int, the compiler might add three padding bytes after the char to ensure the subsequent int starts on a 4-byte boundary. While this increases the structure’s overall size, it guarantees efficient and safe memory access for its members. This automatic alignment is a core optimization that compilers perform to ensure program stability and performance.
Consider a typical scenario where a developer defines a structure to represent a network packet header. If the protocol specifies a tightly packed sequence of bytes without regard for CPU alignment preferences, the default compiler padding would break compatibility. In such cases, the developer needs a mechanism to override the compiler’s default behavior, and this is where packing directives come into play. However, overriding default behavior without a deep understanding of its implications can introduce subtle bugs that are difficult to diagnose.
How __attribute__((packed)) and pragma pack Work
Both __attribute__((packed)) and pragma pack serve the same fundamental purpose: to instruct the compiler to lay out structure members as tightly as possible, eliminating or reducing padding bytes. The key difference lies in their syntax, scope, and portability. __attribute__((packed)) is a GCC-specific extension, applied directly to a structure definition, like so: struct MyPackedStruct { char a; int b; } __attribute__((packed)); This attribute tells the GCC compiler to use the smallest possible alignment for each member, typically 1 byte, effectively removing all internal padding.
pragma pack, on the other hand, is a more widely adopted and portable directive, supported by many compilers beyond GCC (e.g., MSVC, Clang, Intel C++). It operates by setting a packing alignment boundary. For example, pragma pack(1) sets the packing boundary to 1 byte, meaning members will be aligned on 1-byte boundaries, similar to __attribute__((packed)). The pragma can also be pushed and popped to control its scope, ensuring that only specific sections of code are affected, as in:
pragma pack(push, 1) // Save current packing, set to 1-byte alignment struct MyPackedStruct { char a; int b; }; pragma pack(pop) // Restore previous packing alignment
When you declare a structure as packed, the compiler no longer adds padding bytes between members to meet the natural alignment requirements of the underlying architecture. This results in structures that occupy less memory, which can be advantageous for memory-constrained environments or for serializing data structures to disk or network without needing an explicit serialization step to remove padding. However, this memory efficiency comes at the cost of potential performance degradation and other issues, as the CPU may encounter unaligned memory accesses when reading or writing to these packed members.
The “Unsafe” Aspects: Risks and Pitfalls
The primary reason __attribute__((packed)) and pragma pack are often labeled “unsafe” stems from their direct interaction with memory alignment and processor architecture. The most significant risk is unaligned memory access. When a program attempts to read or write a multi-byte value (like an int or a long) from an address that is not a multiple of its size, it constitutes an unaligned access. While some architectures (like x86) handle unaligned accesses by performing multiple, slower memory operations, others (like many ARM or MIPS processors) may trigger a hardware exception or trap, leading to program crashes or unexpected behavior.
Beyond potential crashes, unaligned accesses almost universally incur a performance penalty. The CPU has to perform additional work, such as multiple memory fetches and bit shifts, to assemble the requested value. This overhead can be substantial, particularly in loops or data-intensive operations, negating any memory savings. For instance, accessing an unaligned 4-byte integer might take two memory bus cycles instead of one, effectively halving the memory bandwidth for that operation. This issue is often overlooked during development but can become a critical bottleneck in production.
Using __attribute__((packed)) or pragma pack directly impacts data alignment, often leading to unaligned memory accesses. While some CPUs tolerate unaligned access at a performance cost, others, particularly RISC architectures, can trigger bus errors or exceptions, causing program crashes. This is the primary reason these directives are considered “unsafe” without careful consideration of the target hardware.
Portability is another major concern. Code relying on specific packing behavior might compile and run correctly on one system but fail spectacularly on another with a different architecture, compiler Question & Answer :
In C, the compiler will lay out members of a struct in the order in which they’re declared, with possible padding bytes inserted between members, or after the last member, to ensure that each member is aligned properly.
gcc provides a language extension, __attribute__((packed)), which tells the compiler not to insert padding, allowing struct members to be misaligned. For example, if the system normally requires all int objects to have 4-byte alignment, __attribute__((packed)) can cause int struct members to be allocated at odd offsets.
Quoting the gcc documentation:
The
packed' attribute specifies that a variable or structure field should have the smallest possible alignment--one byte for a variable, and one bit for a field, unless you specify a larger value with thealigned’ attribute.
Obviously the use of this extension can result in smaller data requirements but slower code, as the compiler must (on some platforms) generate code to access a misaligned member a byte at a time.
But are there any cases where this is unsafe? Does the compiler always generate correct (though slower) code to access misaligned members of packed structs? Is it even possible for it to do so in all cases?
Yes, __attribute__((packed)) is potentially unsafe on some systems. The symptom probably won’t show up on an x86, which just makes the problem more insidious; testing on x86 systems won’t reveal the problem. (On the x86, misaligned accesses are handled in hardware; if you dereference an int* pointer that points to an odd address, it will be a little slower than if it were properly aligned, but you’ll get the correct result.)
On some other systems, such as SPARC, attempting to access a misaligned int object causes a bus error, crashing the program.
There have also been systems where a misaligned access quietly ignores the low-order bits of the address, causing it to access the wrong chunk of memory.
Consider the following program:
#include <stdio.h> #include <stddef.h> int main(void) { struct foo { char c; int x; } __attribute__((packed)); struct foo arr[2] = { { 'a', 10 }, {'b', 20 } }; int *p0 = &arr[0].x; int *p1 = &arr[1].x; printf("sizeof(struct foo) = %d\n", (int)sizeof(struct foo)); printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c)); printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x)); printf("arr[0].x = %d\n", arr[0].x); printf("arr[1].x = %d\n", arr[1].x); printf("p0 = %p\n", (void*)p0); printf("p1 = %p\n", (void*)p1); printf("*p0 = %d\n", *p0); printf("*p1 = %d\n", *p1); return 0; }
On x86 Ubuntu with gcc 4.5.2, it produces the following output:
sizeof(struct foo) = 5 offsetof(struct foo, c) = 0 offsetof(struct foo, x) = 1 arr[0].x = 10 arr[1].x = 20 p0 = 0xbffc104f p1 = 0xbffc1054 *p0 = 10 *p1 = 20
On SPARC Solaris 9 with gcc 4.5.1, it produces the following:
sizeof(struct foo) = 5 offsetof(struct foo, c) = 0 offsetof(struct foo, x) = 1 arr[0].x = 10 arr[1].x = 20 p0 = ffbff317 p1 = ffbff31c Bus error
In both cases, the program is compiled with no extra options, just gcc packed.c -o packed.
(A program that uses a single struct rather than array doesn’t reliably exhibit the problem, since the compiler can allocate the struct on an odd address so the x member is properly aligned. With an array of two struct foo objects, at least one or the other will have a misaligned x member.)
(In this case, p0 points to a misaligned address, because it points to a packed int member following a char member. p1 happens to be correctly aligned, since it points to the same member in the second element of the array, so there are two char objects preceding it – and on SPARC Solaris the array arr appears to be allocated at an address that is even, but not a multiple of 4.)
When referring to the member x of a struct foo by name, the compiler knows that x is potentially misaligned, and will generate additional code to access it correctly.
Once the address of arr[0].x or arr[1].x has been stored in a pointer object, neither the compiler nor the running program knows that it points to a misaligned int object. It just assumes that it’s properly aligned, resulting (on some systems) in a bus error or similar other failure.
Fixing this in gcc would, I believe, be impractical. A general solution would require, for each attempt to dereference a pointer to any type with non-trivial alignment requirements either (a) proving at compile time that the pointer doesn’t point to a misaligned member of a packed struct, or (b) generating bulkier and slower code that can handle either aligned or misaligned objects.
I’ve submitted a gcc bug report. As I said, I don’t believe it’s practical to fix it, but the documentation should mention it (it currently doesn’t).
UPDATE: As of 2018-12-20, this bug is marked as FIXED. The patch will appear in gcc 9 with the addition of a new -Waddress-of-packed-member option, enabled by default.
When address of packed member of struct or union is taken, it may result in an unaligned pointer value. This patch adds -Waddress-of-packed-member to check alignment at pointer assignment and warn unaligned address as well as unaligned pointer
I’ve just built that version of gcc from source. For the above program, it produces these diagnostics:
c.c: In function ‘main’: c.c:10:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member] 10 | int *p0 = &arr[0].x; | ^~~~~~~~~ c.c:11:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member] 11 | int *p1 = &arr[1].x; | ^~~~~~~~~