为什么这个编译器屏障不强制执行排序?

Cor*_*250 6 c gcc avr arduino

我在查看 Atmel 网站上的文档时发现了这个示例,其中解释了重新排序的一些问题。

这是示例代码:

#define cli() __asm volatile( "cli" ::: "memory" )
#define sei() __asm volatile( "sei" ::: "memory" )

unsigned int ivar;

void test2( unsigned int val )
{
  val = 65535U / val;

  cli();

  ivar = val;

  sei();
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,他们正在实施类似关键区域的机制。cli 指令禁用中断,sei 指令启用中断。通常,我会保存中断状态并恢复到该状态,但我离题了......

他们注意到的问题是,启用优化后,第一行的除法实际上会移到 cli 指令之后。当您尝试尽可能短地进入关键区域时,这可能会导致一些问题。

如果 cli() MACRO 扩展为显式破坏内存的内联汇编,这怎么可能呢?编译器如何自由地在此语句之前或之后移动内容?

另外,我修改了代码,以在每个语句之前以 的形式包含内存屏障__asm volatile("" ::: "memory");,但它似乎没有改变任何内容。

我还从 cli() 和 sei() 宏中删除了内存破坏,并且生成的代码是相同的。

当然,如果我将 test2 函数参数声明为 易失性,则不会进行重新排序,我认为这是因为易失性语句无法相对于其他易失性语句(技术上是内联汇编)进行重新排序。我的假设正确吗?

是否可以针对易失性内联汇编重新排序易失性访问?

是否可以相对于易失性内联汇编重新排序非易失性访问?

奇怪的是,Atmel 声称他们需要内存破坏器只是为了强制执行与 asm 相关的易失性访问的顺序。这对我来说没有任何意义。

如果编译器屏障不是正确的解决方案,那么我该如何防止任何外部代码“泄漏”到关键区域?

如果有人能提供一些线索,我将不胜感激。

谢谢

ema*_*uts 3

\n

如果 cli() MACRO 扩展为显式破坏内存的内联汇编,这怎么可能呢?编译器如何自由地在此语句之前或之后移动内容?

\n
\n

这是由于以下实现细节造成的avr-gcc:编译器的支持库 libgcc 提供了许多以汇编语言编写的函数以提高性能;包括整数除法函数,例如__udivmodhi4. 并非所有这些函数都会破坏avr-gcc ABI 指定的所有被调用者使用的寄存器。特别是,__udivmodhi4不会破坏Z寄存器。

\n

avr-gcc如下利用它:在没有 16 位除法指令(如 AVR)的机器上,GCC 将发出库调用而不是为其内联生成代码。 avr-gcc然而,假装该体系结构确实具有此类除法指令,并将其建模为对处理器寄存器产生影响,就像库调用一样。最后,在所有代码分析和优化之后,avr后端将这条指令打印为[R]CALL __udivmodhi4. 我们称其为透明调用,即编译器分析看不到的调用。

\n

例子

\n
int div (int a, int b, volatile const __flash char *z)\n{\n    int ab;\n\n    (void) *z;\n    asm volatile ("" : "+r" (a));\n    ab = a / b;\n    asm volatile ("" : "+r" (ab));\n    (void) *z;\n\n    return ab;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

编译它以avr-gcc -S -Os -mmcu=atmega8 ...获得汇编文件*.s

\n
div:\n    movw  r30,r20\n    lpm   r18,Z\n    rcall __divmodhi4\n    movw  r24,r22\n    lpm   r18,Z\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n

解释

\n

(void) *z从Flash中读取一个字节,为了使用lpm指令,地址必须位于Z由 完成的寄存器中movw r30,r20。读取 via 后lpm,编译器发出rcall __divmodhi4执行有符号 16 位除法的命令。如果这是一个普通(非透明)调用,编译器对被调用者的内部工作一无所知,但由于 avr 后端手动对调用进行建模,编译器知道指令序列不会改变Z,因此可以使用Z通话后再次进行,无需再多费吹灰之力。由于寄存器压力较小,因此可以更好地生成代码,特别z是不需要在除法周围保存/恢复。

\n

asm只是用于对代码进行排序:它是易失性的,因此不能根据易失性读取重新排序*z。并且asm不能根据除法重新排序,因为asm更改aab\xe2\x80\x93 至少是我们假装并通过约束告诉编译器的内容。(这些变量实际上没有改变,但这并不重要。)

\n
\n

另外,我修改了代码,以在每个语句之前包含内存屏障__asm volatile("" ::: "memory");,它似乎没有改变任何内容。

\n
\n

该除法不会触及内存(这是一个没有内存破坏的透明调用),因此编译器机器可能会根据内存破坏/访问对其进行重新排序。

\n

如果您需要特定的顺序,那么您必须引入人工依赖项,如上面的示例所示。

\n

为了区分普通调用和透明调用,您可以.s通过-save-temps -dpwhere -dpprints insn 名称将生成的程序集转储到文件中:

\n
void func0 (void);\n\nint func1 (int a, int b)\n{\n    return a / b;\n}\n\nvoid func2 (void)\n{\n    func0();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

每个调用既不是call_insn也不call_value_insn是透明调用, *divmodhi4_call在本例中:

\n
func1:\n    rcall __divmodhi4    ;  17  [c=0 l=1]  *divmodhi4_call\n    movw r24,r22         ;  18  [c=4 l=1]  *movhi/0\n    ret                  ;  23  [c=0 l=1]  return\n\nfunc2:\n    rjmp func0           ;  5   [c=0 l=1]  call_insn/3\n
Run Code Online (Sandbox Code Playgroud)\n