Jeremy Friesner
Jeremy Friesner

Reputation: 73209

Is it well-defined to hold a misaligned pointer, as long as you don't ever dereference it?

I have some C code that parses packed/unpadded binary data that comes in from the network.

This code was/is working fine under Intel/x86, but when I compiled it under ARM it would often crash.

The culprit, as you might have guessed, was unaligned pointers -- in particular, the parsing code would do questionable things like this:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]);  // misaligned access -- can crash under ARM!

... that's obviously not going to fly in ARM-land, so I modified it to look more like this:

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord));  // slower but ARM-safe

My question (from a language-lawyer perspective) is: is my "ARM-fixed" approach well-defined under the C language rules?

My worry is that maybe even just having a misaligned-int32_t-pointer might be enough to invoke undefined behavior, even if I never actually dereference it directly. (If my concern is valid, I think I could fix the problem by changing pNextWord's type from (const int32_t *) to (const char *), but I'd rather not do that unless it's actually necessary to do so, since it would mean doing some pointer-stride arithmetic by hand)

Upvotes: 39

Views: 3573

Answers (4)

No, the new code still has undefined behaviour. C11 6.3.2.3p7:

  1. 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 68) for the referenced type, the behavior is undefined. [...]

It doesn't say anything about dereferencing the pointer - even the conversion has undefined behaviour.


Indeed, the modified code that you assume is ARM-safe might not be even Intel-safe. Compilers are known to generate code for Intel that can crash on unaligned access. While not in the linked case, it might just be that a clever compiler can take the conversion as a proof that the address is indeed aligned and use a specialized code for memcpy.


Alignment aside, your first excerpt also suffers from strict aliasing violation. C11 6.5p7:

  1. An object shall have its stored value accessed only by an lvalue expression that has one of the following types:88)
    • a type compatible with the effective type of the object,
    • a qualified version of a type compatible with the effective type of the object,
    • a type that is the signed or unsigned type corresponding to the effective type of the object,
    • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
    • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
    • a character type.

Since the array buf[2048] is statically typed, each element being char, and therefore the effective types of the elements are char; you may access the contents of the array only as characters, not as int32_ts.

I.e., even

int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);

has undefined behaviour.

Upvotes: 23

supercat
supercat

Reputation: 81247

Some compilers may assume no pointer will ever hold a value that is not properly aligned for its type, and perform optimizations that rely upon that. As a simple example, consider:

void copy_uint32(uint32_t *dest, uint32_t *src)
{
  memcpy(dest, src, sizeof (uint32_t));
}

If both dest and src hold 32-bit aligned addresses, the above function could be optimized to one load and one store even in platforms that don't support unaligned accesses. If the function had been declared to accept arguments of type void*, however, such an optimization would not be allowed on platforms where unaligned 32-bit accesses would behave differently from a sequence of byte accesses, shifts, and bit-wise operations.

Upvotes: 5

dbush
dbush

Reputation: 224437

As mentioned in Antti Haapala's answer, simply converting a pointer to another type when the resulting pointer is not properly aligned invokes undefined behavior as per section 6.3.2.3p7 of the C standard.

Your modified code only uses pNextWord to pass to memcpy, where it gets converted into a void *, so you don't even need a variable of type uint32_t *. Just pass the address of the first byte in the buffer you want to read from to memcpy. Then you don't need to worry about alignment at all.

uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));

Upvotes: 2

lee qiaoping
lee qiaoping

Reputation: 155

To safely parse multi-byte integer across compilers/platforms, you can extract each byte, and assemble them to integer according to the endian. For example, to read 4-byte integer from big-endian buffer:

uint8_t* buf = any address;

uint32_t val = 0;
uint32_t  b0 = buf[0];
uint32_t  b1 = buf[1];
uint32_t  b2 = buf[2];
uint32_t  b3 = buf[3];

val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;

Upvotes: 8

Related Questions