【问题标题】:Volatile and sequence point易失性和序列点
【发布时间】:2019-01-07 09:09:48
【问题描述】:

给定以下代码:

unsigned int global_flag = 0;

void exception_handle()
{
    global_flag = 1;
}

void func()
{
    /* access will cause exception which will assign global_flag = 1
       then execution continues */
    volatile unsigned int x = *(unsigned int *)(0x60000000U); /* memory protection unit configured to raise exception upon accessing this address */

    if (global_flag == 1)
    {
        /* some code */
    }
}

鉴于volatilemust not be reordered across sequence points

最低要求是在一个序列点之前的所有 对 volatile 对象的访问已经稳定,并且没有后续 已发生访问

并给出以下关于sequence points

序列点出现在以下位置... (1) .. (2) .. (3) 在完整表达式的末尾。此类别包括表达式 语句(例如赋值 a=b;)、return 语句、 if、switch、while 或 do-while 语句的控制表达式, 以及 for 语句中的所有三个表达式。

是否承诺volatile unsigned int x = *(unsigned int *)(0x60000000U); 会在if (global_flag == 1) 之前发生(在二进制asm 中,CPU 乱序执行与这里无关)?

根据上面的引用,volatile unsigned int x = *(unsigned int *)(0x60000000U); 必须在下一个序列点结束之前被评估,而volatile unsigned int x = *(unsigned int *)(0x60000000U); 本身就是一个序列点,所以这意味着每个volatile 分配都在分配时被评估?

如果上述问题的答案是否定的,那么下一个序列点在ifend,是否意味着可以执行类似的操作:

if (global_flag == 1)
{
    volatile unsigned int x = *(unsigned int *)(0x60000000U);
    /* some code */
}

系统是一个嵌入式的-ARM cortex m0,单核,单线程应用程序。

【问题讨论】:

  • 如果我理解正确,那么第一个问题是“是”?为什么你认为它可能是“不”?当然,如果编译器可以知道您的代码必须导致未定义的行为,它可以假设例如代码永远不会被访问并完全优化它... 您不能依赖未定义的行为以定义的方式工作C!
  • 在您的示例中,global_flag 定义中缺少 volatile 是错误还是有意选择?
  • @hyde 当然,许多低级编程环境都支持代码模式,这些代码模式通常以定义和确定的方式未定义的行为。
  • 这里有一个有趣而微妙的点:您的代码不包含赋值,只是一个初始化,我相信在带有初始化器的定义的末尾没有序列点。我也认为这是标准中的一个缺陷。
  • ILLEGAL_ADDRESS 是什么?

标签: c language-lawyer volatile


【解决方案1】:

在您的 sn-p 中,变量 global_flag 不是易失性的,因此没有什么可以阻止编译器跨序列点移动对 global_flag 的访问,或者在情况允许的情况下将其完全删除。谈论访问x和访问global_flag的顺序是没有意义的,因为后者不是可观察的事件,只有前者是。

(另请注意,表达式*(unsigned int *)(0x60000000U) 中没有volatile 限定符。我认为您确实希望特别处理该表达式,但您的代码没有这样做。允许编译器生成代码它提前对*(unsigned int *)(0x60000000U) 进行评估,然后在其盘子上执行大量其他操作,然后将获得的值分配给x,这将满足C 标准对volatile 左值的约束。 )

如果您的 sn-p 有 unsigned int volatile global_flag = 0;*(volatile unsigned int *)(0x60000000U),那么“是否承诺……”这个问题的答案将是明确的“是”。

【讨论】:

  • 但是 volatile 评估不是基于抽象机器规则(没有优化)吗?而且,如果global_flagvolatile 并且我也会使初始化程序*(volatile unsigned int *)(0x60000000U) 易变,那么位于*(unsigned int *)(0x60000000U)global_flag == 1 之间的序列点是否(与@Kamul Cuk 答案的@melpomene 评论有关) 会承诺所需的订单吗?
  • @user2162550 易失性左值的评估基于抽象机器规则。对其他一切的评价都是“好像”。例如,无法访问a,无法访问b,也无法在以下函数中添加:gcc.godbolt.org/z/NUVOA0,尽管x 是一个易失变量。
  • @user2162550 是的,如果您使global_flag*(unsigned int *)(0x60000000U) 易失性,那么存在于对它们的任何访问对之间的序列点将保证它们在程序集中以相同的顺序被访问源代码中的代码。
  • 我仍然认为这是编译器供应商创造的神话。访问 volatile 对象的所有表达式都必须“按照语义指定进行评估”。语义几乎意味着“整个 C 标准”。其中初始化 volatile 变量 x 的表达式明显排在 if (global_flag == 1) 之前。不允许编译器通过重新排序操作来优化这一点,C 标准中没有任何内容支持该概念!然而,可观察行为的定义与之背道而驰。
【解决方案2】:

是否承诺 volatile unsigned int x = *(unsigned int *)(ILLEGAL_ADDRESS);将在 if (global_flag == 1) 之前发生

来自信息丰富的C11 AnnexC(添加换行符/格式以提高可读性):

以下是5.1.2.3中描述的顺序点:
...
- 在完整表达式的评估和要评估的下一个完整表达式之间。
- 以下是完整的表达方式:
- 不属于复合文字 (6.7.9) 的初始值设定项;
- 表达式语句中的表达式 (6.8.3);
- 选择语句(if 或 switch)的控制表达式 (6.8.4);
- while 或 do 语句的控制表达式 (6.8.5);
- for 语句的每个(可选)表达式 (6.8.5.3);
- return 语句 (6.8.6.4) 中的(可选)表达式。

由于*(unsigned int *)(ILLEGAL_ADDRESS); 是一个初始化器(赋值表达式)并且初始化器不是复合文字的一部分,它是一个完整的表达式。下一个完整的表达式是if中的控制语句,所以在ifx的初始化之间有一个序列点。

还有来自著名的C11 5.1.2.3p6

对一致性实现的最低要求是:

对易失性对象的访问严格按照抽象机的规则进行评估。
...

由于x是一个volatile对象,它被严格初始化为抽象机,所以在序列点之后它的右值必须等于*(unsigned int *)(ILLEGAL_ADDRESS)操作的结果。

所以是的,x 对象的初始化必须发生在if 内的控制表达式之前。

关于未定义的行为,C11 6.5.3.2p4 的引述很好:

如果给指针分配了无效值,则一元 * 运算符的行为未定义。

正如你评论的那样:

在我的系统内存模型中不允许访问地址 0x60000000

可以推断(unsigned int*)0x60000000是一个无效指针,所以一元*运算符应该是spawn dragons

【讨论】:

  • abstract machine 是什么?
  • @user2162550 5.1.2.3.p1 通常是带有编译器但没有优化的机器。所以每次访问都是不稳定的,并且执行并生成代码。
  • @KamilCuk 在*(unsigned int *)(ILLEGAL_ADDRESS)(初始化器)的末尾有一个序列点,在global_flag == 1(条件)的末尾有一个序列点。 x初始化后看不到序列点。
  • @melpomene 如果序列点不打算在初始化之后,那么 sn-p int x = 1; x++; 将包含未定义的行为。这不可能是委员会的意图。
  • 另外,初始化需要声明符。 C17 6.7.6/3:“完整声明符是不属于另一个声明符的声明符。完整声明符的结尾是一个序列点。”
猜你喜欢
  • 1970-01-01
  • 2023-03-21
  • 2018-01-18
  • 2017-08-02
  • 1970-01-01
  • 1970-01-01
  • 2015-11-07
  • 2016-05-31
  • 1970-01-01
相关资源
最近更新 更多