Cast volatile array to non volatile array
Asked Answered
T

4

16

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?

Thankless answered 25/6, 2019 at 8:7 Comment(7)
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?Ephemeral
Possible duplicate of Is `memcpy((void *)dest, src, n)` with a `volatile` array safe?Madoc
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.Madoc
@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. TYThankless
@CacahueteFrito your linked question is very similar to mine! I do not quite think, that it is a duplicate, but i will edit it into my answer. I think the keywords are different, but I will accept the duplicate if said otherwise. Regarding your question: I stated in the question, that the function will handle the first three elements. This is purely an example and the focus does not lie here. TYThankless
@Thankless I also had doubts about the duplicate because of that: the main point, which is the discarding of volatile is the same, but the keywords are a little different; we'll see what others think. And sorry, I didn't read that of the 3 elements :)Madoc
For the implications of volatile see also electronics.stackexchange.com/q/409545/6383Benzene
G
14

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).

Gravel answered 25/6, 2019 at 8:16 Comment(3)
thanks for yout input. I wrote a helper function to copy the contents of the volatile array to a non-volatile array. I used *memcpy as a template, i just changed the parameter types. The question linked in my question does something similar.Thankless
Wait what? What you actually said by that cast is "I guarantee this value won't change out from under the code I'm passing this pointer to." He probably has a terrible memory model problem waiting for him but this isn't immediately undefined.Rapp
@Rapp The cast itself is not undefined. However, dereferencing the pointer obtained by the cast is. That's precisely what the standard quoted in the question says. If an object is volatile, all access to it must be through volatile-qualified lvalues.Gravel
E
6

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*.

Encounter answered 25/6, 2019 at 8:22 Comment(4)
with "hardware" I meant something like an EEPROM, i added that to my question. Thank you for your elaborate explanation! This was an intersting read.Thankless
@Thankless The answer then might very well be to copy the volatile buffer into a non-volatile one first. Because you really don't want data changing on the fly in the middle of EEPROM programming. And you might have interrupts disabled, which would block the data from getting updated anyway. Etc. Inside the EEPROM driver, the pointer to the memory-mapped area to be written has to be declared volatile though, but that's another story.Encounter
This is what I did, see my comment on Angew's answer. The EEPROM has some logic itself, it is controlled with I2C, I think I do not have to worry about pointers :)Thankless
@Thankless Ok then it isn't memory-mapped and less sensitive to deal with. (But I2C registers in your MCU will be volatile qualified)Encounter
W
3
  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.

Woundwort answered 25/6, 2019 at 8:29 Comment(2)
1.: in my case, the function is called at a defined moment, when no data is written to the buffer. 2.: Is this a defined behaviour?Thankless
2. Yes it is very well defined but you need to care about the atomicity of the accessWoundwort
B
1

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.

Benzene answered 25/6, 2019 at 9:11 Comment(3)
One example of how it can go wrong: suppose the first item in the array is some status flag. while(array[0]) { do_stuff(array[i]); }. Without volatile, this will likely get translated into a single read, after which the compiler can decide to leave the function or hang in an eternal loop.Encounter
@Encounter Yes, of course. However, as the function parameter is declared non-voltaile, why should the function implementation be relying on it behaving as a volatile?Benzene
@Benzene I think your answer is very similar to DavidSchwartz's comment. I now made a helper function, as suggested by Angew, also see my comment on his answer.Thankless

© 2022 - 2024 — McMap. All rights reserved.