通过 volatile reference/pointer 访问声明的非 volatile 对象是否会为所述访问赋予 volatile 规则?

Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?

这将是一篇很长的文章,为了将其置于上下文中并提供尽可能多的信息,我必须浏览各种链接和引用 - 这通常是我们进入 C/C 后的唯一方法++ 标准兔子洞。如果您对此 post 有更好的引用或任何其他改进,请告诉我。但总结一下, 目的是从两个命题中找出真相:

无论哪种方式,如果(看起来)措辞与意图相比有些模棱两可 - 我们能否在标准本身中明确

第一种相互排斥的解释更为普遍,这并非完全没有根据。但是,我希望表明有大量 "reasonable doubt" 支持第二个 - 特别是当我们回到基本原理和工作组论文中的一些先前段落时。


公认的智慧:引用的对象本身必须已经声明volatile

昨天的热门问题 假设 volatile 引用会赋予非 volatile 引用对象 volatile 行为 - 但发现它没有,或者以不同程度和不可预测的方式做了。

接受的答案最初得出结论,只有声明的引用类型才重要。这个和大多数评论似乎都同意,正如我们所熟知的 const 一样,等效原则正在发挥作用:如果引用具有相同的 [=120=,则行为只会是 volatile(或根本没有定义) ]cv-被引用对象资格:

The key word in that passage is object. volatile sig_atomic_t flag; is a volatile object. *(volatile char *)foo is merely an access through a volatile-qualified lvalue and the standard does not require that to have any special effects. – zwol

这种解释似乎得到了广泛的支持,从对这个类似但希望不重复的问题的回答中可以看出:Requirements for behavior of pointer-to-volatile pointing to non-volatile object但即使在那里也存在不确定性:就在回答说 'no',然后显示 'maybe'!无论如何......让我们检查一下标准,看看 'no' 是基于什么的。


标准说的...或不说的

C11,N1548,§6.7.3:很明显,访问一个对象 定义为 volatileconst 通过不共享所述限定符的指针键入...

6 If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined. 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.(133)

...标准似乎没有明确提到相反的情况,即 volatile。此外,在总结 volatile 及其上的操作时,它现在谈论一个 具有 volatile 限定类型的对象:

7 An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously.(134) What constitutes an access to an object that has volatile-qualified type is implementation-defined.

我们是否假设 "has" 等同于 "was defined with"?或 can "has" 引用对象和引用限定符的组合?

一位评论者用这样的措辞很好地总结了这个问题:

From n1548 §6.7.3 ¶6 the standard uses the phrase "object defined with a volatile-qualified type" to distinguish it from "lvalue with volatile-qualified type". It's unfortunate that this "object defined with" versus "lvalue" distinction does not carry forward, and the standard then uses "object that has volatile-qualified type", and says that "what constitutes access to an object that has volatile-qualified type is implementation-defined" (which could have said "lvalue" or "object defined with" for clarity). Oh well. – Dietrich Epp

同一部分的第 4 段似乎不太常被引用,但可能很相关,我们将在下一节中看到。


合理怀疑:Is/Was volatile pointer/reference 旨在赋予其取消引用 volatile 语义?

上述回答有一条评论,其中作者引用了委员会早些时候的声明,对 'reference must match referent' 的想法提出了质疑:

Interestingly, there is one sentence in there [C99 Rationale for volatile] that implies that the committee meant for *(volatile T*)x to force that one access to x to be treated as volatile; but the actual wording of the standard does not achieve this. – zwol

我们可以从前面提到的第二个线程中找到关于这一点的更多信息:Requirements for behavior of pointer-to-volatile pointing to non-volatile object

On the other hand, this post quotes from the 6.7.3 of the Rationale for International Standard--Programming Languages--C:

A cast of a value to a qualified type has no effect; the qualification (volatile, say) can have no effect on the access since it has occurred prior to the case. If it is necessary to access a non-volatile object using volatile semantics, the technique is to cast the address of the object to the appropriate pointer-to-qualified type, then dereference that pointer.

philipxy

that Bytes thread 开始,我们参考了 C99 s6.7.3 p3 - a.k.a。 C11 的 p4 - 以及这个分析:

The paragraph in question is just before section 6.7.3.1 in the rationale document. If you also need to quote from the standard document itself, cite 6.7.3 p3:

The properties associated with qualified types are meaningful only for expressions that are lvalues.

The expression (volatile WHATEVER) non_volatile_object_identifier is not an lvalue, hence the 'volatile' qualifier is meaningless.

Conversely, the expression * (volatile WHATEVER *) & non_volatile_object_identifier is an lvalue (it may be placed on the left side of an assignment statement), so the property of the 'volatile' qualifier has its intended meaning in this case.

Tim Rentsch

WG Paper N1381 中有一个 非常 支持这个想法的具体演示,具体针对第一个链接问题。这引入了 Annexed memset_s() 来做 OP 想要的 - 保证内存的非遗漏填充。在讨论可能的实现时,它似乎支持这样的想法 - 通过省略说明任何要求 - 使用 volatile 指针来改变非 volatile 对象 应该 根据指针的 限定符 生成代码,而不管引用对象的限定符...

  1. Platform-independent ' secure-memset' solution:
void *secure_memset(void *v, int c , size_t n) {
    volatile unsigned char *p = v;
    while (n--) *p++ = c;
    return v;
}

This approach will prevent the clearing of memory from being optimized away, and it should work on any standard-compliant platform.

...并且注意到没有这样做的编译器...

There has been recent notice that some compilers violate the standard by not always respecting the volatile qualifier.


谁说的对?

太累了。这里肯定有很大的解释空间,具体取决于您碰巧阅读了哪些文档,哪些没有阅读,以及您选择如何解释很多不够具体的词。似乎很明显有些不对劲:要么:

我希望我们能比过去似乎围绕着这个问题的所有模棱两可和猜测做得更好 - 并得到一份更有说服力的声明并记录在案。为此,我们非常欢迎专家提供任何进一步的资源和想法。

转换为回答,因为我认为深思熟虑的不回答可能有助于发现这里的真相。

I guess the underlying question is "how abstract do we expect the memory model to be?". By qualifying a non-vol pointer as volatile, we seem to be asking the compiler to "write to the I/O or memory directly". That's ok, but if the compiler has previously deduced that the "memory" need not exist, what should it do? Backtrack and create the memory, or ignore you?

在我看来,以下两个案例的意图非常不同:

内存映射I/O

volatile unsigned char * const uart_base = (volatile unsigned char *)0x10000;

这显然是为了通知编译器在地址 0x10000 处有一个 uart 内存映射。

正在清除密码哈希值

void *secure_memset(void *v, int c , size_t n) {
    volatile unsigned char *p = v;
    while (n--) *p++ = c;
    return v;
} 

这显然是为了保证v到(int*)v + n处的内存实际上是在函数returns之前修改的。

但是,如果推断 v 处的内存从不需要,是否可以省略对该函数的调用尚不清楚。

我会争辩说,如果之前在程序中,内存被推断为根本不需要存在,那么如果调用被省略,我不会感到惊讶,无论转换为 volatile。

Thanks. Because the address is taken, isn't the object required to occupy memory?

gcc 似乎同意你的观点:

#include <cstdint>
#include <cstring>

void * clearmem(void* p, std::size_t len)
{
  auto vp = reinterpret_cast<volatile char*>(p);
  while (len--) {
    *vp++ = 0;
  }
  return p;
}

struct A
{
  char sensitive[100];
  
  A(const char* p)
  {
    std::strcpy(sensitive, p);
  }
  
  ~A() {
    clearmem(&sensitive[0], 100);
  }
};

void use_privacy(A a)
{
  auto b = a;
}


int main()
{
  A a("very private");
  use_privacy(a);
}

产量:

clearmem(void*, unsigned long):
        leaq    (%rdi,%rsi), %rax
        testq   %rsi, %rsi
        je      .L4
.L5:
        movb    [=12=], (%rdi)
        addq    , %rdi
        cmpq    %rax, %rdi
        jne     .L5
.L4:
        xorl    %eax, %eax
        ret
use_privacy(A):
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L10:
        movb    [=12=], (%rax)
        addq    , %rax
        cmpq    %rdx, %rax
        jne     .L10
        ret
main:
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L13:
        movb    [=12=], (%rax)
        addq    , %rax
        cmpq    %rdx, %rax
        jne     .L13
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L14:
        movb    [=12=], (%rax)
        addq    , %rax
        cmpq    %rdx, %rax
        jne     .L14
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L15:
        movb    [=12=], (%rax)
        addq    , %rax
        cmpq    %rdx, %rax
        jne     .L15
        xorl    %eax, %eax
        ret

clang 并没有省略私有数组的构造,所以我不能在那里得出任何结论。

一个对象要么是易变的,要么不是。使用 volatile 引用引用对象将生成正确的代码,无论该对象是否为 volatile。

该标准并未尝试定义有用实现中所需的所有行为。基本原理明确承认实现可以同时符合要求但几乎完全无用的可能性。

标准将 volatile 访问的语义分类为实现定义的,并且不以任何方式、形状或形式要求实现必须有用地定义它们。因此,可以说,只要记录在案的行为和实际行为一致,像 gcc 那样的 volatile 语义的实现会使实现不符合要求,但只会使其无法用于原本可能具有的目的合适。

请注意,gcc 通常用于可以配置任意地址区域的平台 space 表现得有点像 I/O 设备,然后表现得像普通 RAM。因此可能有必要确保某些操作的顺序非常精确,即使大多数其他操作的顺序无关紧要;要求必须将对某事物的所有操作都视为 volatile 以便对任何操作进行如此处理似乎不是优化的好方法。

我觉得奇怪的是,在过去的几年里,人们对标准是否允许编译器为某些结构实现无用语义以提高不需要该结构的代码的性能变得如此感兴趣很有用,因为这种方法几乎在所有方面都不如默认实现有用的语义,但为不需要这种语义的程序员提供了使用命令行开关或#pragma 指令放弃它们的方法。如果程序包含 [假设] #pragma gcc_loose_volatile 指令,gcc 可以做任何它喜欢的事情,不管人们如何解释标准关于 volatile 的要求,如果它不包含这样的指令,则无用无论标准是否禁止,语义都是无用的。

Does accessing a declared non-volatile object through a volatile reference/pointer confer volatile rules upon said accesses?

volatile 在 C 和 C++ 中的含义不同。 C++ 标准使通过易失性左值的访问变得可观察。 [1] 它说它打算与 C 行为相同。这就是 C 基本原理中描述的行为。尽管如此,C 标准表示对 volatile-declared 对象的访问是可观察的。 (请注意,通过 non-volatile 左值访问 volatile-declared 对象是未定义的。)

但是。 有一个缺陷报告,基本上有委员会同意(尽管仍然开放),标准应该说,并且意图一直是,并且实施已经总是反映出,重要的不是对象的波动性(根据标准),而是访问(的左值)的波动性(根据基本原理)。

C11 版本 1.10 的缺陷报告摘要日期:2016 年 4 月DR 476左值的可变语义 04/2016 开放

当然,对于可观察行为所做的是implementation-dependent。

确实没有任何歧义。只是人们 无法相信 C 标准的行为可能就是这样,因为那不是 volatile 之前的历史用法(当 address-literal左值被视为 volatile 对象),如基本原理所预期的那样,如之前和之后的编译器所实现的那样,如 C++ 标准所解释和描述的那样,如 DR 中更正的那样。同样,标准很清楚,因为它没有说 non-volatile 访问是可观察的,所以它们不是。 (并且 "side effect" 是用于定义评估偏序的术语。)

[1] 或者至少希望现在可以。来自 underscore_d 的评论:

For C++, see also P0612R0: NB comment CH 2: volatile, which was adopted this month to clean up some leftover talk about "volatile objects" in the C++ Standard, when really accesses through volatile glvalues are what it meant (as, presumably/hopefully, what C meant).