首页 文章

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

提问于
浏览
25

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

  • Do the C and (by import; see comments) C++ Standards require that accesses via volatile * or volatile & must refer to an object originally declared volatile in order to have volatile semantics?

  • is accessing a non-volatile-qualified object through a volatile pointer/reference sufficient/supposed to make said accesses behave as if the object was declared volatile?

无论哪种方式,如果(看起来如此)措辞与标准本身相比有些模棱两可 - can we get that made clear

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

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

昨天的流行问题Is the definition of “volatile” this volatile, or is GCC having some standard compliancy problems?是通过假设 volatile 引用会在非 volatile 指示物上赋予 volatile 行为而产生的 - 但发现它没有,或者在不同程度上以不可预测的方式做到了 .

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

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

这种解释似乎被广泛持有,正如对这个相似但希望不重复的问题的回答中所见:Requirements for behavior of pointer-to-volatile pointing to non-volatile object但是's uncertainty even there: right after the answer says '没有', it then says '可能'! Anyway...let'检查标准以查看'no'的基础 .

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

C11, N1548, §6.7.3 :而's clear that it'的UB通过不共享所述限定符的指针访问对象 defined with volatileconst 类型...

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

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

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

我们假设"has"相当于"was defined with"?或can "has"是指对象和参考限定符的组合?

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

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

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

合理怀疑:/是一个易失性的指针/引用是为了赋予volatile它的解引用语义?

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

有趣的是,那里有一句话[C99基于挥发性的理由]意味着委员会意味着*(易变的T *)x迫使对x的一次访问被视为易变;但是标准的实际措辞没有实现这一点 . - zwol

我们可以从上面提到的第2个帖子中找到有关基本原理的更多信息:Requirements for behavior of pointer-to-volatile pointing to non-volatile object

另一方面,这篇文章引用了国际标准的基本原理6.7.3 - 编程语言 - C:对一个合格类型的值的转换没有效果;资格(volatile,比如说)可能对访问没有影响,因为它已经发生在案例之前 . 如果需要使用volatile语义访问非易失性对象,则该技术是将对象的地址强制转换为适当的指向限定类型的类型,然后取消引用该指针 . -philipxy

that Bytes thread,我们're referred to C99 s6.7.3 p3 - a.k.a. C11' s p4 - 并且这个分析:

有关段落就在理由文件第6.7.3.1节之前 . 如果您还需要引用标准文档本身,请引用6.7.3 p3:与限定类型相关联的属性仅对作为左值的表达式有意义 . 表达式(volatile WHATEVER)non_volatile_object_identifier不是左值,因此'volatile'限定符无意义 . 相反,表达式*(volatile WHATEVER *)和non_volatile_object_identifier是一个左值(它可以放在赋值语句的左侧),因此在这种情况下,'volatile'限定符的属性具有其预期含义 . -Tim Rentsch

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

独立于平台的'secure-memset'解决方案:

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

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

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

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

谁是对的?

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

  • 基本原理和N1381是错误的或随意的措辞,或者

  • 他们被追溯性地特别无效......或者

  • 标准是错误的或随意的措辞 .

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

4 回答

  • 7

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

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

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

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

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

    真的没有't any ambiguity. It'只是人们无法相信C标准行为可能是它的原因,因为这不是历史使用前 volatile (当地址文字左值被认为是易失性对象时),如预期的那样根据理论由C标准解释和描述,由编制者在之前和之后实施的基本原理,如DR中所述 . 同样,标准是明确的,因为它没有't say that non-volatile accesses are observable, so they' . ("side effect"是用于定义评估偏序的术语 . )

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

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

  • 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;
    }
    

    这显然是为了确保在函数返回之前实际修改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);
    }
    

    收益率:

    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
    

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

  • -1

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

    标准将 volatile 访问的语义分类为实现定义,并且不以任何方式,形状或形式要求实现必须有效地定义它们 . 因此,如果提供文件化和实际行为一致,那么像gcc那样的语义的实现会使实现不符合,但仅仅使它对于本来可能适用的目的而言是无用的,这是不合理的 .

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

    我发现奇怪的是,人们在过去几年中对标准是否允许编译器为某些构造实现无用的语义变得如此感兴趣,目的是为了提高代码的性能,这些代码不需要这样的语义来使用命令来放弃它们行开关或#pragma指令 . 如果一个程序包含[假设] #pragma gcc_loose_volatile 指令,gcc可以做任何它喜欢的事情,无论人们如何解释标准关于 volatile 的要求,如果它不包含这样的指令,无论是否无用,无用的语义将是无用的 . 标准禁止或不禁止 .

  • 2

    对象是易失性的还是非易失性的 . 使用volatile引用引用对象将产生无论对象是否为volatile的正确代码 .

相关问题