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

时间:2016-06-18 14:06:15

标签: c gcc arduino avr

我正在查看Atmel网站上的文档,我遇到了example,他们解释了重新排序的一些问题。

以下是示例代码:

#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();
}

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

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

如果cli()MACRO扩展为内联asm并明确地破坏了内存,那怎么可能呢?编译器如何在此语句之前或之后自由移动?

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

我还从cli()和sei()MACRO中删除了内存clobber,生成的代码完全相同。

当然,如果我将test2函数参数声明为volatile,则不会重新排序,我认为这是因为不能对其他volatile语句(技术上是内联函数)重新排序volatile语句。我的假设是否正确?

可以针对易失性内联asm重新排序易失性访问吗?

非易失性访问是否可以针对易失性内联asm进行重新排序?

有些奇怪的是,Atmel声称他们需要内存崩溃来强制执行与asm相关的易失性访问的排序。这对我没有任何意义。

如果编译器障碍不适合这个,那么我怎么能防止任何外部代码“泄漏”到关键区域?

如果有人能说清楚,我会很感激。

由于

1 个答案:

答案 0 :(得分:0)

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

这是由于avr-gcc的实现细节所致:编译器的支持库libgcc提供了许多以汇编形式编写的函数,以提高性能。包括用于整数除法的函数,例如__udivmodhi4。并非所有这些功能都会破坏所有callee-used registers as specified by the avr-gcc ABI。特别是,__udivmodhi4不会掩盖Z寄存器。

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

示例

int div (int a, int b, volatile const __flash char *z)
{
    int ab;

    (void) *z;
    asm volatile ("" : "+r" (a));
    ab = a / b;
    asm volatile ("" : "+r" (ab));
    (void) *z;

    return ab;
}

使用avr-gcc -S -Os -mmcu=atmega8 ...进行编译以获取程序集文件*.s

div:
    movw  r30,r20
    lpm   r18,Z
    rcall __divmodhi4
    movw  r24,r22
    lpm   r18,Z
    ret

说明

(void) *z从闪存读取一个字节,并且为了使用lpm指令,该地址必须位于Z完成的movw r30,r20寄存器中。通过lpm读取后,编译器发出rcall __divmodhi4以执行带符号的16位除法。如果这是一个普通的(非透明的)调用,则编译器将不了解被调用方的内部工作,但是由于avr后端通过手工对调用进行建模,因此编译器知道指令序列不会改变{ {1}} ,因此可以在通话后再次使用Z,而无需费心。由于较少的寄存器压力,因此可以更好地生成代码,尤其是Z不需要在分区周围保存/恢复。

z仅用于订购代码:它是易失性的,因此不能针对易失性读取asm重新排序。并且*z不得针对除法进行重新排序,因为asm会更改asma –至少这就是我们要假装并通过约束告诉编译器。 (这些变量实际上并未更改,但这在这里无关紧要。)

我还修改了代码,以ab的形式在每个语句之前添加了内存屏障,并且似乎没有任何改变。

该部分不涉及内存(这是没有内存障碍的透明调用),因此编译器机制可能会针对内存障碍/访问对其进行重新排序。

如果需要特定的命令,则必须像上面的示例中那样引入人为的依赖关系。

为了区分普通调用和透明调用,您可以通过__asm volatile("" ::: "memory");将生成的程序集转储到.s文件中,其中-save-temps -dp打印insn名称:

-dp

每个既不是void func0 (void); int func1 (int a, int b) { return a / b; } void func2 (void) { func0(); } 也不是call_insn的呼叫都是透明呼叫,在这种情况下是call_value_insn

*divmodhi4_call