使用硬件时,有时需要从特定寄存器中进行读取以丢弃实际值(例如,清除一些标志)。一种方法是显式读取和丢弃该值,例如:
int temp = *(volatile int*)0x1234; // 0x1234 is the register address
(void)temp; // To silence the "unused" warning
似乎可行的另一种方法是:
*(volatile int*)0x1234;
但是,这似乎并不明显意味着 read 访问,但是它似乎可以转换为我检查过的编译器。这是标准所保证的吗?
带有-O3
的ARM GCC的示例:
https://arm.godbolt.org/z/9Vmt6n
void test(void)
{
*(volatile int *)0x1234;
}
翻译成
test():
mov r3, #4096
ldr r3, [r3, #564]
bx lr
答案 0 :(得分:7)
C 2018 6.7.3 8说:
具有volatile限定类型的对象可以以实现方式未知的方式修改或具有其他未知的副作用。因此,必须严格按照抽象机器的规则评估引用该对象的任何表达式,如5.1.2.3中所述。……
由于*(volatile int*)0x1234;
是一个表达式,它引用具有volatile限定类型的对象,因此评估它必须访问该对象。 (当然,这假设0x1234
代表对C实现中某个对象的有效引用。)
每C 2018 5.1.2.3 4:
在抽象机中,所有表达式均按语义指定的方式求值。如果实际实现可以推断出未使用表达式的值并且没有产生所需的副作用(包括由调用函数或访问易失性对象引起的副作用),则无需评估表达式的一部分。
每C 2018 6.5 1:
表达式是一个运算符和操作数的序列,它们指定值的计算,或者指定对象或函数,或者产生副作用,或者执行它们的组合。
因此,表达式指定了值的计算。 5.1.2.3 4告诉我们该评估是由抽象机执行的,而6.7.3 8告诉我们实际的实现是由抽象机执行的评估。
一个警告是构成“访问”的是实现定义的。 C标准定义的“访问”包括读取和写入(C 3.1 1),但是C标准无法指定这意味着读取或写入某些特定硬件。
要进一步进入语言律师领域,C 6.3.2.1 2告诉我们:
除非它是
sizeof
运算符的操作数,一元&
运算符,++
运算符,--
运算符或{的左运算数{1}}运算符或赋值运算符,将没有数组类型的左值转换为存储在指定对象中的值(不再是左值);这称为左值转换。
因此,由于.
是左值,因此*(volatile int*)0x1234;
运算符不是列表运算符的操作数,因此它将转换为存储在对象中的值。因此,此表达式指定存储在对象中的值的计算。
答案 1 :(得分:3)
volatile上的gcc文档告诉我们实现定义是构成易失性访问的原因:
C具有易失性对象的概念。这些通常由指针访问,并用于访问硬件或线程间通信。该标准鼓励编译器避免对易失对象的访问进行优化,但将实现定义为易失访问的定义。最低要求是,在一个顺序点上,之前对易失对象的所有访问都已稳定,并且没有后续访问发生。因此,实现可以自由地重新排序和组合发生在序列点之间的易失性访问,但是对于跨序列点的访问则不能这样做。使用volatile不允许违反两个序列点之间多次更新对象的限制。
这由C11部分6.7.3 Type qualifiers p7备份:
具有volatile限定类型的对象可能会以未知方式修改 实施或有其他未知的副作用。因此,任何表达指 必须严格按照抽象机的规则对此类对象进行评估, 如5.1.2.3中所述。此外,在每个序列点,最后存储在 对象应与抽象机器所规定的一致,除非由 134)什么构成对访问以下对象的对象的访问 具有volatile限定类型的是实现定义的。
gcc文档继续指定volatile对gcc的工作方式,类似于您所说的情况:
在空中访问标量易失对象时,将读取它 上下文:
volatile int *src = somevalue; *src;
此类表达式为右值,而GCC则将其实现为对 指向易失性对象。