gpt4 book ai didi

c - 禁用中断会保护非 volatile 变量还是会发生重新排序?

转载 作者:行者123 更新时间:2023-12-02 04:26:50 24 4
gpt4 key购买 nike

假设INTENABLE是启用/禁用中断的微 Controller 寄存器,我已在库中的某个地方将其声明为位于适当地址的volatile变量。 my_var是在一个或多个中断以及my_func中修改的一些变量。

my_func中,我想在my_var中执行一些操作,该操作可以读写(例如+=)原子(从某种意义上说,它必须完全在中断之后或之前发生-中断不会在发生时发生)。

我通常会遇到的是这样的事情:

int my_var = 0;

void my_interrupt_handler(void)
{
// ...

my_var += 3;

// ...
}

int my_func(void)
{
// ...

INTENABLE = 0;
my_var += 5;
INTENABLE = 1;

// ...
}

如果我理解正确,如果将my_var声明为 volatile,则可以保证 my_var被“干净地”更新(也就是说,中断不会在 my_var的读写之间更新 my_func),因为C标准保证 Volatile 存储器访问按顺序进行。

我需要确认的部分是未声明 volatile的时间。然后,编译器将无法保证在禁用中断的情况下进行更新,对吗?

我想知道是因为我编写了类似的代码(带有非 Volatile 变量),不同之处在于我通过另一个编译单元(某些库文件)的功能禁用了中断。如果我对事情的理解正确,那么可能起作用的实际原因是编译器无法假定未通过编译单元外部的调用读取或修改该变量。因此,例如,如果我使用GCC的 -flto进行编译,则可能会在关键区域之外进行重新排序(坏事情)。我有这个权利吗?

编辑:

多亏了Lundin的评论,我才意识到我将外设的中断寄存器禁用的情况与使用特定的汇编指令禁用处理器上所有中断的情况混合在一起。

我可以想象启用/禁用处理器中断的指令会阻止其他指令本身从前到后或从后到前重新排序,但是我仍然不确定那是否是正确的。

编辑2:

关于 Volatile 访问:因为我不清楚在 Volatile 访问周围重新排序是否是标准不允许的,允许但在实践中没有发生的事情,还是在实践中被允许但确实发生的事情,所以我提出了用一个小的测试程序:
volatile int my_volatile_var;

int my_non_volatile_var;

void my_func(void)
{
my_volatile_var = 1;
my_non_volatile_var += 2;
my_volatile_var = 0;
my_non_volatile_var += 2;
}

使用7.3t版的 arm-none-eabi-gcc与Cortex-M0的 -O2( arm-none-eabi-gcc -O2 -mcpu=cortex-m0 -c example.c)进行编译,我得到以下程序集:
movs    r2, #1
movs r1, #0
ldr r3, [pc, #12] ; (14 <my_func+0x14>)
str r2, [r3, #0]
ldr r2, [pc, #12] ; (18 <my_func+0x18>)
str r1, [r3, #0]
ldr r3, [r2, #0]
adds r3, #4
str r3, [r2, #0]
bx lr

在这里,您可以清楚地看到两个 my_non_volatile_var += 2被合并为一条指令,这两条指令都在两个volatile访问之后发生。这意味着GCC确实在优化时确实会重新排序(我将继续进行下去,并假设这意味着该标准已允许)。

最佳答案

C / C++ volatile的保证用途范围非常狭窄:直接与外界交互(以异步方式调用C / C++编写的信号处理程序是“外部”);这就是易失对象访问被定义为可观察到的的原因,就像控制台I / O和程序的退出值(main的返回值)一样。

看到它的一种方式是想象任何 Volatile 访问实际上是由特殊控制台上的I / O转换的,或者是名为的终端设备或FIFO设备对,访问其中:

  • 将对类型T的对象x的 Volatile 写入x = v;转换为写入FIFO 访问,将写入顺序指定为4字节("write", T, &x, v)
  • x的 Volatile 读取(从左值到右值转换)转换为写入。访问 3项("read", T, &x),并等待上的值值

  • 这样,volatile就像一个交互式控制台。

    ptrace语义是volatile的一个很好的规范(除了我之外,没有人使用它,但它仍然是有史以来最好的volatile规范):
  • 在程序已将停止在定义明确的位置之后,调试器/ ptrace可以检查 Volatile 变量;
  • 任何 Volatile 对象访问都是一组定义良好的PC(程序计数器)点,以便可以在其中设置断点(**):执行 Volatile 访问的表达式会转换为代码中的一组地址,其中中断会导致中断在定义的C / C++表达式中;
  • 程序停止时,可以使用ptrace以任意方式(*)修改任何易失对象的状态,仅限于C / C++中对象的合法值;用ptrace更改 Volatile 对象的位模式等效于在C / C++定义明确的断点处在C / C++中添加赋值表达式,因此等效于在运行时更改C / C++源代码。

  • 这意味着在这些时间点上,您对 Volatile 对象具有明确定义的ptrace可观察状态。

    (*)但是您不能使用ptrace将 Volatile 对象设置为无效的位模式:编译器可以假定任何对象都具有ABI定义的合法位模式。 ptrace用于访问 Volatile 状态的所有用法必须遵循与单独编译的代码共享的对象的ABI规范。例如,如果ABI不允许,那么编译器可以假定易失数字对象不具有负零值。 (对于IEEE浮点数,显然负零是有效状态,从语义上与正零不同)。

    (**)内联和循环展开会在汇编/二进制代码中生成许多与唯一C / C++点相对应的点;调试器通过为一个源级别断点设置许多PC级别断点来解决此问题。

    ptrace语义甚至不暗示 Volatile 局部变量存储在堆栈中而不是寄存器中。这意味着如调试数据中所述,变量的位置可以通过堆栈中的稳定地址(显然在函数调用期间保持稳定)或在保存的寄存器的表示中在可寻址存储器中进行修改。暂停的程序,在执行线程暂停时,它是调度程序保存的寄存器的临时完整副本。

    [在实践中,所有编译器都提供了比ptrace语义更强的保证:所有volatile对象都具有稳定的地址,即使它们的地址从未在C / C++代码中使用;这种保证有时是没有用的,并且严格地悲观。较轻的ptrace语义保证本身对于“高级汇编”中的寄存器中的自动变量非常有用。]

    您必须先停止正在运行的程序(或线程),然后才能停止它。如果没有同步,您将无法从任何CPU进行观察(ptrace提供了这种同步)。

    这些保证适用于任何优化级别。在最小优化下,所有变量实际上实际上都是易失的,并且该程序可以在任何表达式处停止。

    在更高的优化级别上,如果变量不包含任何合法运行的有用信息,则可以减少计算,甚至可以优化变量。最明显的情况是“准const”变量,该变量未声明为const,而是使用a-if const:设置一次且永不更改。如果用于设置它的表达式可以在以后重新计算,则该变量在运行时不携带任何信息。

    许多带有有用信息的变量的范围仍然有限:如果程序中没有可以将带符号整数类型设置为数学负结果的表达式(该结果为负数,则不是负数,因为2补码系统溢出),则编译器可以假定它们没有负值。在编译器中或通过ptrace将它们设置为负值的任何尝试都将不受支持,因为编译器可以生成整合假设的代码。使对象 Volatile 将迫使编译器允许该对象的任何合法值,即使在完整代码(每个TU(转换单元)中可以访问该对象的所有路径中的代码)中仅存在正值的分配可以访问该对象)。

    请注意,对于超出集体翻译代码集(一起编译和优化的所有TU)共享的任何对象,除了适用的ABI之外,都不能假定对象的可能值。

    陷阱(不像在计算中那样是陷阱)应该至少在单个CPU,线性,有序语义编程中期望Java类似于volatile的语义(根据定义,这里不会出现乱序执行,因为状态上只有POV,一个且唯一的CPU):
    int *volatile p = 0;
    p = new int(1);

    没有 Volatile 保证 p只能为null或指向具有值1的对象:在 int的初始化与 Volatile 对象的设置之间没有隐含的易变顺序,因此,异步信号处理程序或断点volatile分配可能看不到初始化的 int

    但是volatile指针可能无法进行推测性的修改:直到编译器获得保证rhs(右侧)表达式不会引发异常(因此保持 p不变)之前,它才不能修改volatile对象(因为volatile访问是根据定义可观察到)。

    回到您的代码:
    INTENABLE = 0; // volatile write (A)
    my_var += 5; // normal write
    INTENABLE = 1; // volatile write (B)

    这里 INTENABLE是 Volatile 的,因此所有访问都是可见的;编译器必须准确地产生那些副作用;正常的写操作是抽象机器的内部操作,编译器只需要保留这些副作用WRT即可生成正确的结果,而无需考虑C / C++抽象语义之外的任何信号。

    就ptrace语义而言,您可以在点(A)和(B)处设置一个断点,然后观察或更改 INTENABLE的值,仅此而已。尽管 my_var可能无法完全优化,因为它可以通过外部代码(信号处理代码)访问,但是该函数中没有其他可以访问它的东西, ,因此my_var的具体表示不必根据其值匹配到那时的抽象机

    如果您在以下两者之间调用了真正的外部(在“集体翻译的代码”外部,编译器无法分析)无执行功能,则有所不同:
    INTENABLE = 0; // volatile write (A)
    external_func_1(); // actual NOP be can access my_var
    my_var += 5; // normal write
    external_func_2(); // actual NOP be can access my_var
    INTENABLE = 1; // volatile write (B)

    请注意,这两个对任何都不可能做的外部函数的调用都是必需的:
  • external_func_1()可能观察到my_var的先前值
  • external_func_2()可能会观察到my_var的新值

  • 这些调用针对必须根据ABI进行的外部,单独编译的NOP函数。因此, 所有全局可访问对象必须带有其抽象机值的ABI表示:对象必须达到其规范状态,这与优化器不同,优化器知道某些对象的某些具体内存表示未达到抽象值。机。

    在GCC中,此类无所事事的外部函数可以拼写为 asm("" : : : "memory");asm("");"memory"的名称含糊不清,但明确表示“访问内存中地址已全局泄漏的所有内容”。

    [请参阅此处,我依靠的是规范的透明意图,而不是依靠其措辞,因为措辞常常会被错误地选择(#),而且任何人都不会使用它们来构建实现,而且只有人们的意见才有意义。话永远不会做。

    (#)至少在普通的编程语言世界中,人们没有资格编写正式甚至正确的规范。 ]

    关于c - 禁用中断会保护非 volatile 变量还是会发生重新排序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53767347/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com