2

I stumbled on some code in the project I'm working on and I wanted to be sure to understand it correctly. So here it is:

uint16_t* tmp;

tmp = (uint16_t*) ((uint8_t*)getVariableAddress(variable) + offset);

tmp = (uint16_t*)((uint8_t*)tmp + otherOffset);
Set_Register((unsigned long) tmp[0]);
Set_OtherRegister((unsigned long) tmp[2]);

At first I got a bit lost between all the casts but the way I see it the uint8_t* are being used to move byte per byte and add the offset values to the base address we place in tmp, this was the first part that got me troubled. The second part was the use of [] on a uint16_t* , for this one I'm not sure at all on the result, anyone care to explain this in detail ?

Thanks

M Nicolaes
  • 51
  • 5
  • If the offset is in bytes but the type is 16-bit, casting ensures that the pointer arithmetic is correct. If the type is 16-bit, then `pointer + 1` adds `2` to the pointer value. As for `tmp[2]` this is the same as `*(tmp + 2)` and the cast is applied to the data value, not the pointer type. – Weather Vane Mar 08 '21 at 14:33
  • 1
    Notably, unless what's actually stored at that address is indeed `uint16_t`, this code invokes undefined behavior bugs. What does `getVariableAddress` do and where does the pointer returned from it point at? There's a pretty good chance that this code is incorrectly written. – Lundin Mar 08 '21 at 14:40
  • 1
    The writer of that code in the project might want to get informed that casting an `uint8_t` pointer to an `uint16_t` pointer violates strict aliasing rules. – Pedro Mar 08 '21 at 14:44
  • The code is likely fundamentally broken. As @Pedro noted, it's a violation of [strict aliasing](https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule) and therefore undefined behavior. It can also be undefined behavior because of [alignment restrictions](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p7). And no, [it doesn't "work"](https://stackoverflow.com/questions/47510783/why-does-unaligned-access-to-mmaped-memory-sometimes-segfault-on-amd64/47512025#47512025). It just hasn't failed ***yet***. – Andrew Henle Mar 08 '21 at 14:54
  • @AndrewHenle: Implementations may, as a form of "conforming language extension", define the behavior of more actions than mandated by the Standard. The question of when to extend the language in this fashion is a quality-of-implementation issue outside the Standard's jurisdiction. Code which relies upon such extensions will not be *strictly* conforming, nor 100% portable, but that doesn't imply that it is non-conforming nor broken. It will work 100% reliably on implementations that extend the language in the required fashion. – supercat Mar 09 '21 at 00:10
  • @supercat Can you provide documentation for a C implementation that allows for breaking strict aliasing and alignment requirements? The linked question in my previous comment demonstrates that GCC does not successfully extend C to allow for the breaking of strict aliasing or alignment restrictions. Without documented extensions for an implementation that also guarantee future existence of those extensions, you're talking about unicorns. I prefer to write code that doesn't require unicorns for it to always work. – Andrew Henle Mar 09 '21 at 13:54
  • @AndrewHenle: What is the purpose of `-fno-strict-aliasing`? So far as I can tell, gcc and clang do extend the language so as to behave as though N1570 6.5p7 didn't exist when that flag is used, and do not reliably process even the cases the Standard clearly intends to mandate when the flag is not used. I can't tell for sure which failures to behave as directed by the Standard are by design, and which are bugs, but the fact that the maintainers of gcc have gone years without fixing them suggests they're intentional. – supercat Mar 09 '21 at 15:50
  • @AndrewHenle: Bear in mind that the Standard was written not to create a language, but *to describe an already-existing langauge*, and the pre-existing language had no such restrictions. If you haven't done so, read the published Rationale at http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf to understand what the authors of the Standard intended. The first 14 numbered pages (some of which are blank or nearly so) make clear that the Standard makes no effort to require that all implementations be suitable for all purposes, nor to imply that all programs should be designed... – supercat Mar 09 '21 at 15:55
  • ...to run on all implementations. Under the One Program Rule, the ability of an implementation to run one contrived and useless program can suffice for conformance, even if the implementation behaves in nonsensical fashion when given any other source text. The Standard was written merely to describe a common core language which implementations for various platforms and purposes would extend in ways appropriate for those platforms and purposes. It makes no effort to mandate everything necessary to make an implementation suitable for any particular purpose (or even any purpose whatsoever). – supercat Mar 09 '21 at 16:05
  • @supercat *gcc and clang do extend the language so as to behave as though N1570 6.5p7 didn't exist when that flag is used* It's not just about [6.5.p7](https://port70.net/~nsz/c/c11/n1570.html#6.5p7). [6.2.3.2p7](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p7) also applies. Merely creating a misaligned pointer is undefined behavior: "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." NB no dereference of the misaligned pointer is needed for UB. – Andrew Henle Mar 09 '21 at 16:52
  • @supercat And as the [link I'll provide again demonstrates](https://stackoverflow.com/questions/47510783/why-does-unaligned-access-to-mmaped-memory-sometimes-segfault-on-amd64/47512025#47512025), code doing that can and does fail even on x86 systems where there's this naive belief that all access are safe no matter how they're aligned. ***They're not***. But hey, if you want to write code susceptible to failures like that, that's on you. – Andrew Henle Mar 09 '21 at 16:52
  • @AndrewHenle: I wasn't talking about unaligned accesses. Implementations intended for low-level programming typically have options to control things like whether they will do things like replace a pair of `ldr` instructions (which on Cortex-M3 will tolerate misaligned addresses) with `ldrm` or `ldrd` (which is not thus tolerant) but generally subscribe to the philosophy that pointers which don't identify aligned objects should be kept as `void*` rather than a particular object type, and that potentially-unaligned objects identified by such pointers should be accessed via special-purpose code. – supercat Mar 09 '21 at 17:09
  • @AndrewHenle: A simpler example of the effect of pointer alignment can be found with clang when targeting a Cortex-M0. Using `memcpy` with a size of 4 and source and destination arguments of a 32-bit type will cause clang to generate a LDR/STR pair rather than a sequence of LDRB/STRB. I have no beef with that optimization, since the Standard lacks any other efficient type-agnostic way of copying objects that are known to be word-aligned, and code which is working with unaligned pointers can easily use `char*` or `void*` for that purpose. – supercat Mar 09 '21 at 17:18
  • @AndrewHenle: BTW, the most fundamental defect in the Standard is the last word of the section now known as N1570 4.2p2, which recursively states that behavior which the Standard characterizes as "undefined" is "undefined", when it would be more accurate and useful to say "outside the Standard's jurisdiction". The Standard's failure to mandate that implementations not intended for low-level programming need not support certain constructs is not intended as a prohibition on the use of such constructs in low-level code that's not intended to be portable. – supercat Mar 09 '21 at 17:28

3 Answers3

1

I think it is easier to see what happens if we rewrite the code a bit:

// We keep this as a `uint8_t *` so we can add offsets correctly.
uint8_t *base_address = (uint8_t *)getVariableAddress(variable);

// Add the offsets to the base address.
uint8_t *offsetted_address = base_address + offset + otherOffset;

// We want it as a uint16_t.
uint16_t *as_u16 = (uint16_t *)offsetted_address;

// We want to write the first register with the first `uint16_t` at `offsetted_address`, but `Set_Register` takes the value as `unsigned long`.
unsigned long first_u16 = as_u16[0];
Set_Register(first_u16);

// We want to write the other register with the third `uint16_t` at `offsetted_address`, but `Set_OtherRegister` takes the value as `unsigned long`.
unsigned long third_u16 = as_u16[2];
Set_OtherRegister(third_u16 );

We know that the values that interest us are at an offset relative to the address returned by getVariableAddress. In order to properly compute that address we cast the address to an uint8_t. If we keep as uint16_t our arithmetic would be wrong. Consider this:

uint8_t *p = (uint8_t *)0x100;
printf("%p\n", p + 1); // prints 0x101

uint16_t *q = (uint16_t *)0x200;
printf("%p\n", q + 1); // prints 0x202

We then want to read the first and third 16-bit unsigned integer from the address we just computed, so we cast it to an uint16_t * and get the first ([0]) and third ([2]) integers.

icebp
  • 1,608
  • 1
  • 14
  • 24
0

When you use something such as:

int var[5];
var[3] = 1;

When declaring var, it allocates 5 integers contiguously in memory, and var gets just a pointer to the first of those 5 memory slots, essentially an int*.

Then when you access it with var[3] you are telling it to access that first memory address with an offset of 3 times sizeof(int).

In your example it's doing the same thing, you are getting the pointer to the first memory position that tmp is pointing to, and then adding an offset of a value to it

0

otherOffset is in units of bytes, but tmp is a uint16_t *. If the address were calculated as tmp + otherOffset, the sum would be calculated by treating otherOffset as a number of uint16_t objects rather than a number of bytes. So, to do the desired calculation, tmp is converted to uint8_t *. Then (uint8_t *) tmp + otherOffset does the calculation in units of bytes.

Similarly, offset is in units of bytes, and the type of getVariableAddress(variable) might not be a pointer to a byte/character type (we do not know since you have not shown us its declaration), so the cast ensures the arithmetic is done in the desired units, bytes.

The casts to uint16_t * merely convert the results of arithmetic to the desired pointer type for further use.

tmp[0] and tmp[2] are ordinary uses of subscript. In effect, they say, “There is an array of uint16_t starting at the location tmp points to. Give me the element with index 0 or 2 from that array.” Formally, tmp[i] is defined as *(tmp + i), which says to add the offset i to tmp and then dereference the resulting address.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312