16
votes

I have a global volatile unsigned char array volatile unsigned char buffer[10] to which data is written in an interupt. I have a function that takes an unsigned char * and stores that value to hardware (EEPROM) void storeArray(unsigned char *array), in this example the first three values. Is it safe to cast the volatile array to a non-volatile array like so?

store_array((unsigned char *) buffer);

I read the following, which I do not quite understand, but which concerns me:

6.7.3:5 If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.

Does this affect my code?

Then I have this followup question: The buffer array only has a section of data I want to store (can't change that), for this example beginning with the third value. Is it legit to do the following?

store_array((unsigned char *) buffer + 3);

If it is, how is the cast affected, if 3 is added to the array? BR and thank you!

EDIT: @Cacahuete Frito linked a very similar question: Is `memcpy((void *)dest, src, n)` with a `volatile` array safe?

4
IMO, since this is platform-specific code anyway, if you can determine that volatile does what you want on your platform, then it's okay for you to use it. You're relying on non-portable aspects of volatile anyway, and ... in for a penny, in for a pound. If your compiler's treatment of volatile changes, your code can break anyway. So what do you have to lose?David Schwartz
Regarding storeArray((unsigned char *) buffer + 3);: How does the function know where the array ends? If it has the size hardcoded, the pointer arithmetics may force the function to read 3 bytes beyond the buffer limits, and therefore UB.alx
@DavidSchwartz the focus of this question was aimend to give a better understanding of this issue. However I like your point! I think I can determine the behaviour, as the code is quite stringent. This might be the "slimest" way to go. TYearthling
For the implications of volatile see also electronics.stackexchange.com/q/409545/6383JimmyB

4 Answers

14
votes

Yes, the standard quote you've posted covers precisely what you're trying to do. By doing the cast, you're pretending the objects in the array are unsigned char when they're actually volatile unsigned char, so inside the function, you're referring to volatile object through an lvalue without a volatile qualifier. Undefined Behaviour.

If you cannot change the function storeArray, you will have to copy the data from the volatile array to a non-volatile one before passing it to the function.

Regarding the second question: the pointer arithmetic is fine, it will simply convert buffer to an unsigned char* and then add 3 to the resulting pointer, pointing to buffer[3] (but with the wrong qualification).

6
votes

You have found the correct section of the standard, this code leads to undefined behavior.

A function writing something "to hardware" should probably have a volatile-qualifier parameter, depending on what "hardware" is. If it is a memory-mapped register, a DMA buffer or non-volatile memory, then the parameter should definitely have been volatile unsigned char* (or optionally, volatile uint8_t* which also is to be regarded as a character type).


Details: C allows us to iterate through any chunk of data using a character pointer, C17 6.3.2.3/7:

When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.

The part you quote about accessing a "lvalue" refers to accesing data through a different pointer type than what's actually stored in that location. Plainly: no matter how much you cast various pointers pointing at it, the actual data retains its original type.

Accessing the data through the wrong pointer type is normally not even allowed, but again character access is a special exception to the "strict aliasing rule", C17 6.5/7:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
...
- a character type.

So you can access any kind of data through a character pointer, but if that pointer is not volatile-qualified, you invoke undefined behavior as per the part you quoted, C17 6.7.3/5.

In practice, using a non-volatile pointer type could cause the compiler to optimize the access in unexpected ways. So this isn't just theoretical "language-lawyering", you could in practice get very strange code generated with optimizations enabled. Lots of very hard to find bugs in embedded systems originate from such a missing volatile.


Regarding your follow-up question, the cast and the buffer + 3 changes nothing: you are still dealing with a character pointer without volatile qualifier - same type. The actual data remains of type volatile unsigned char, so you can't access it from the function through a unsigned char*.

3
votes
  1. If the array is changes in interrupt you need to provide a mechanism to acces and modify it atomic way. If you don't any RW or RMW operation may be unsuccessful and the data inconsistent.

  2. You access volatile data make the f=unction parameters volatile as well. storeArray(volatile unsigned char *) and no cast will be needed. The cast only removes the warning. Even you pass non-volatile data to it, it will work as well.

1
votes

As you found, you're relying on "undefined behavior". However, depending among other things on the separation of compilation units (and things like "whole-program-optimization" (WPO)) it will probably work. In most cases, the compiler (at least gcc) is not "smart enough" to optimize array accesses across functions in different compilation units. That said, the clean, safe and portable way would be to copy the array, making the dependency of the non-volatile array's values on the volatile ones visible to the compiler.