通过易失性引用/指针访问声明的非易失性对象是否会在所述访问时赋予易失性规则?

und*_*e_d 26 c c++ pointers volatile language-lawyer

这将是一个漫长的过程,关于它的上下文并提供尽可能多的信息,我必须通过各种链接和引用来蜿蜒 - 这通常是我们进入C/C++标准兔子洞后的唯一方法.如果您对此帖有更好的引用或任何其他改进,请告诉我.但是要事先总结,你可以责怪@ zwol发布这个;-)并且目的是从两个命题中找到真相:

  • 执行C和(通过导入;请参阅注释)C++标准要求访问通过volatile *volatile &必须引用最初声明的对象volatile才能具有volatile语义?
  • 或者volatile通过volatile指针/引用访问非限定对象足够/应该使所述访问行为就像声明对象一样volatile

无论哪种方式,如果(看起来)措辞与意图相比有些含糊不清 - 我们能否在标准本身中明确说明?

这些相互排斥的解释中的第一个更常见,而且并非完全没有依据.但是,我希望表明对第二个问题有很大的"合理怀疑" - 尤其是当我们回到基本原理和WG论文中的一些先前段落时.


接受的智慧:被引用的对象本身必须已被声明 volatile

昨天流行的问题是"挥发性"这个波动的定义,还是GCC有一些标准的合规性问题?通过假设一个volatile引用会赋予volatilevolatile指示物行为- 但发现它没有,或者在不同程度上以不可预测的方式发生行为而产生.

接受的答案最初得出结论,只有声明的指称类型才重要.这个和大多数评论似乎都同意我们所熟知的等效原则在起作用const:volatile如果引用与引用对象具有相同的cv -qualification,则行为将仅(或根本定义):

该段落中的关键词是对象.volatile sig_atomic_t flag;是一个易变的对象.*(volatile char *)foo仅仅是通过挥发性合格左值的访问,标准不要求具有任何特殊效果.- zwol

这种解释似乎得到了广泛的解释,正如对这个相似但希望不重复的问题的回答所示:指向易失性指向非易失性对象的行为要求但即使在那里也存在不确定性:答案是'不',然后说'也许'!无论如何......让我们检查一下标准,看看'不是'的基础.


标准所说的......或者不是

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

6如果尝试const通过使用具有非const限定类型的左值来修改使用-qualified类型定义的对象,则行为未定义.如果尝试通过使用具有非限定类型的左值来引用使用volatile-qualified类型定义的对象volatile-,则行为未定义.(133)

...标准似乎没有明确提到相反的情况,即volatile.此外,在对其进行总结volatile和操作时,它现在讨论具有 volatile -qualified类型的对象:

7 具有volatile-qualified类型对象可能以实现未知的方式进行修改或具有其他未知的副作用.因此,任何涉及这种对象的表达都应严格按照抽象机的规则进行评估,如5.1.2.3所述.此外,在每个序列点,最后存储在对象中的值应与抽象机器规定的值一致,除非由前面提到的未知因素修改.(134)什么构成对具有volatile-qualified类型的对象的访问是实现-defined.

我们是否假设"has"等同于"被定义为"?或者"有"可以指对象和参考限定符的组合?

一位评论者用这种措辞总结了这个问题:

n1548§6.7.36开始,该标准使用短语"使用volatile限定类型定义的对象"将其与"具有volatile限定类型的左值"区分开来.不幸的是,这个"用"与"左值"区分的"对象"没有结转,然后标准使用"具有volatile限定类型的对象",并说"构成对具有volatile限定类型的对象的访问权限"是实现定义的"(为了清楚起见,可以说"左值"或"对象定义为").那好吧.- 迪特里希·艾普

同一部分的第4段似乎不那么频繁引用,但可能很有用,我们将在下一节中看到.


合理怀疑:/是否是一个volatile指针/引用旨在赋予volatile其解引用语义?

上述答案有一个评论,其中作者引用委员会早些时候的声明,对"参考必须符合指称"的想法产生怀疑:

有趣的是,在有[一句话C99理由volatile ]为暗示委员会意味着用于*(volatile T*)x强制将一个接入到x被视为易失性; 但是标准的实际措辞没有实现这一点. - zwol

我们可以从上面提到的第二个线程中找到有关基本原理的这一点的更多信息:指针到易失性指向非易失性对象的行为要求

另一方面,这篇文章引用了国际标准的基本原理6.7.3 - 编程语言 - C:

将值转换为限定类型无效; 资格(volatile,比如说)可能对访问没有影响,因为它已经发生在案例之前.如果需要使用volatile语义访问非易失性对象,则该技术是将对象的地址强制转换为适当的指向限定类型​​的类型,然后取消引用该指针.

- philipxy

那个Bytes线程,我们被称为C99 s6.7.3 p3 - 又名C11的p4 - 并且这个分析:

有关段落就在理由文件第6.7.3.1节之前.如果您还需要引用标准文档本身,请引用6.7.3 p3:

与限定类型关联的属性仅对作为左值的表达式有意义.

表达 (volatile WHATEVER) non_volatile_object_identifier左值,因此"挥发性"限定符是无意义的.

相反,表达式 * (volatile WHATEVER *) & non_volatile_object_identifier 左值(它可以放在赋值语句的左侧),因此在这种情况下,'volatile'限定符的属性具有其预期含义.

- Tim Rentsch

WG Paper N1381中,有一个非常具体的示范支持这一想法,特别是关于第一个相关问题.这引入了附件来做OP想要的事情 - 保证非冗余的内存填充.在讨论可能的实现时,它似乎支持这个想法 - 通过省略声明任何要求 - 使用指针来改变非对象应该基于指针限定符生成代码,而不管引用对象的那个...memset_s()volatilevolatile

  1. 独立于平台的'secure-memset'解决方案:
void *secure_memset(void *v, int c , size_t n) {
    volatile unsigned char *p = v;
    while (n--) *p++ = c;
    return v;
}
Run Code Online (Sandbox Code Playgroud)

这种方法可以防止内存的清理被优化掉,并且它应该适用于任何符合标准的平台.

......那些没有这样做的编译器正在注意......

最近有人注意到一些编译器违反了标准,并不总是尊重volatile限定符.


谁是对的?

那令人筋疲力尽.这里肯定有很多解释空间,这取决于你碰巧阅读哪些文件而不是哪些文件,以及你如何选择解释很多不够具体的文字.似乎很明显有些不对劲:要么:

  • 基本原理和N1381是错误的或随意的措辞,或
  • 他们被追溯性地特别无效......或者
  • 标准是错误的或随意的措辞.

我希望我们能做得比过去似乎已经包围过的所有歧义和猜测做得更好 - 并且得到一个更有说服力的声明.为此,非常欢迎来自专家的任何进一步的资料和想法.

phi*_*pxy 7

通过易失性引用/指针访问声明的非易失性对象是否会在所述访问时赋予易失性规则?

volatile在C&C++中并不意味着同样的事情.C++标准通过volatile lvalues进行访问可观察.[1]它表示它打算与C行为相同.这就是C原理中描述的行为.然而,C标准表示可以观察到对volatile声明的对象的访问.(请注意,未定义通过非易失性左值访问volatile声明的对象.)

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

C11版本1.10的缺陷报告摘要日期:2016年4月DR 476左值的易失性语义04/2016打开

当然,关于可观察行为的做法是依赖于实现的.

真的没有任何含糊之处.只是人们无法相信 C标准行为可能是它的volatile本质,因为这不是历史使用前的(当地址 - 文字左值被认为是易变的对象时),正如基本原理所预期的那样,由编译器在之前和之后实现,如C++标准所解释和描述的那样,在DR中进行了更正.同样,标准很明确,因为它没有说非易失性访问是可观察的,所以它们不是.("副作用"是用于定义评估偏序的术语.)

[1]或者至少希望现在能做到.来自underscore_d的评论:

对于C++,另请参阅P0612R0:NB注释CH 2:volatile,本月采用它来清理C++标准中关于"volatile对象"的一些剩余的讨论,当真正通过volatile glvalues进行访问时它是什么意思(因为,大概是/希望,C意味着什么).


Ric*_*ges 5

转换回答是因为我认为一个深思熟虑的非回答可能有助于在这里发现真相.

我想潜在的问题是"我们对内存模型的期望是多么抽象?".通过将非vol指针限定为volatile,我们似乎要求编译器"直接写入I/O或内存".没关系,但是如果编译器之前已经推断出"内存"不需要存在,它该怎么办?回溯并创建内存,还是忽略你?

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

内存映射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;
} 
Run Code Online (Sandbox Code Playgroud)

这显然是为了确保在函数返回之前实际修改v到(int*)v + n的内存.

但是,如果推断出v中的内存从未被需要,是否可以省略对此函数的调用,这一点尚不清楚.

我认为,如果以前在程序中,已经推断出内存根本不需要存在,那么如果调用被省略,我将不会感到惊讶,无论转换为volatile.

谢谢.因为地址被占用,是不是占用内存所需的对象?

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);
}
Run Code Online (Sandbox Code Playgroud)

收益率:

clearmem(void*, unsigned long):
        leaq    (%rdi,%rsi), %rax
        testq   %rsi, %rsi
        je      .L4
.L5:
        movb    $0, (%rdi)
        addq    $1, %rdi
        cmpq    %rax, %rdi
        jne     .L5
.L4:
        xorl    %eax, %eax
        ret
use_privacy(A):
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L10:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L10
        ret
main:
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L13:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L13
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L14:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L14
        leaq    -120(%rsp), %rax
        leaq    100(%rax), %rdx
.L15:
        movb    $0, (%rax)
        addq    $1, %rax
        cmpq    %rdx, %rax
        jne     .L15
        xorl    %eax, %eax
        ret
Run Code Online (Sandbox Code Playgroud)

clang并没有忽视私人阵列的构建,因此我无法在那里得出任何结论.