禁用中断会保护非易失性变量还是会发生重新排序?

时间:2018-12-13 17:39:14

标签: c embedded volatile interrupt-handling sequence-points

假设INTENABLE是启用/禁用中断的微控制器寄存器,我已在库中的某个地方将其声明为位于适当地址的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被“干净”地更新(也就是说,中断不会更新{{1 }}在my_var的读写之间),因为C标准保证了易失性内存访问是按顺序进行的。

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

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


编辑:

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

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

编辑2:

关于易失性访问:由于我不清楚标准是否不允许围绕易失性访问进行重新排序,是否允许但实际上没有发生,或者实际上是否允许,确实发生了某些事情,我想出了一个小测试程序:

-flto

使用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.3.1与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 被合并为一条指令,这两条指令都在两个易失性访问之后发生。这意味着GCC确实在优化时确实会重新排序(我将继续进行操作,并假设这意味着该标准已允许)。

3 个答案:

答案 0 :(得分:0)

在没有中断的情况下,我认为您可以安全地进行调度程序切换,而不必担心在后台更改变量。但是,从细节上讲,这可能取决于计算机体系结构。对于典型的x86确实如此。

具有非易失性变量的另一个陷阱是,如果编译器认为无法更改,则编译器将优化变量读取,无论该部分有无中断都会发生这种情况。但是除非该变量本质上是易失性的,例如输入引脚,否则“不应”破坏关键部分。

简短的回答:处于关键部分不会保存优化器中的非易失性变量。

答案 1 :(得分:0)

这里有一些值得关注的事情。

指令重新排序

关于指令重新排序作为优化的一部分,不允许编译器在易失性变量访问之间进行。对volatile变量进行“严格地根据抽象机规则进行评估”,这实际上意味着在volatile访问表达式末尾的序列点处,必须评估该表达式之前的所有内容。

在这方面,内联汇编程序也很可能也可以避免重新订购。任何重新排序或优化手动编写的汇编程序的编译器都将损坏并且不适合嵌入式系统编程。

这意味着,如果示例中的中断启用/禁用归结为设置/清除全局中断掩码(如某种形式的内联汇编宏),则编译器将无法很好地对其进行重新排序。如果它是对硬件寄存器的访问,那么(希望)这将是易失性的,并且也不能重新排序。

这意味着内联汇编器指令/易失性访问之间的内容相对于内联汇编器/易失性访问而言是安全的,可以重新排序,但与其他任何内容都不可以。

优化与ISR共享的变量/没有明显的副作用

主要回答here。在您的特定示例中,my_var没有明显的副作用,可以对其进行优化。如果从中断中修改它,也是如此。这是更大的危险,因为围绕非易失性变量访问的嵌入式asm / volatile访问无关紧要。

使用“意大利面条全局” /外部链接设计,在优化时,编译器可能确实无法做出各种假设。我不确定gcc的链接时优化在这里意味着什么,但是如果您告诉链接器不要担心其他翻译单位对意大利面条的访问,那么我确实认为可能会发生不好的事情。不是因为重新排序,而是因为一般的“无副作用”优化。尽管可以争论,但是如果您在整个程序中都使用extern,这是您最省心的事情。


如果您未启用优化功能,那么您会很安全。如果有的话,那么嵌入式系统编译器通常是很宽容的,并且不要进行过于激进的优化。 gcc是另一回事,它非常希望在嵌入式软件中以-O2或-O3造成严重破坏,尤其是当您的代码包含某种形式的行为不明确的行为时。

答案 2 :(得分:0)

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

一种查看方法是,想象任何易失性访问实际上是由特殊控制台上的I / O转换的,或者是名为 Access Values的终端或FIFO设备对 strong>其中:

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

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

ptrace语义是volatile的一个很好的规范(除了我之外,没有人使用它,但它仍然是有史以来最好的volatile规范):

  • 在程序已在明确定义的位置停止后,调试器/ ptrace可以检查易失性变量;
  • 任何易失性对象访问都是一组定义良好的PC(程序计数器)点,以便可以在其中设置断点(**):进行易失性访问的表达式会转换为代码中导致中断的一组地址在定义的C / C ++表达式处中断;
  • 在程序停止时,可以使用ptrace以任意方式(*)修改任何易失对象的状态,仅限于C / C ++中对象的合法值;使用ptrace更改易失性对象的位模式等同于在C / C ++定义明确的断点处在C / C ++中添加赋值表达式,因此等同于在运行时更改C / C ++源代码。

这意味着您在这些时间点上的挥发性对象具有定义良好的ptrace可观察状态。

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

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

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

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

如果不停止正在运行的程序(或线程),则无法对其进行检查。您无法在没有同步的情况下从任何CPU进行观察(ptrace提供了这种同步)。

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

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

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

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

陷阱(不是计算中的陷阱)应该至少在单个CPU,线性,有序语义编程中实现Java volatile类语义(根据定义,其中不会出现乱序执行,因为在Java上只有POV状态,唯一的一个CPU):

int *volatile p = 0;
p = new int(1);

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

但是可变指针不能被推测性地修改:直到编译器获得保证rhs(右侧)表达式不会抛出异常(因此保持p不变)的保证,它才不能修改可变对象(因为根据定义,可变访问是可以观察到的。)

返回您的代码:

INTENABLE = 0; // volatile write (A)
my_var += 5;  // normal write
INTENABLE = 1; // volatile write (B)

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

就ptrace语义而言,您可以在(A)和(B)点设置一个断点,并观察或更改INTENABLE的值,仅此而已。尽管my_var可能无法完全优化,因为它可以通过外部代码(信号处理代码)访问,但是该函数中没有其他可以访问它的功能,因此所以my_var的具体表示形式

如果您在以下两者之间调用了 truly 外部函数(在“集体翻译的代码”之外,编译器无法分析),则完全不同:

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"的含义不明确,但显然的意思是“访问地址中全局泄漏的任何内容”。

[请参见此处,我依靠的是规范的透明意图,而不是依靠其措辞,因为这些词语通常是错误选择的(#),而且任何人都不会使用它来构建实现,而只是人们的意见计数,这些词永远不会做。

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