assembly 理解“volatile”关键字和比较的工作原理

piztneat  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(172)

如果一个变量没有用关键字volatile指定,编译器可能会缓存。变量 * 必须 * 总是从内存访问,否则直到它的事务单元结束。我想知道的一点在于汇编部分。

int main() {
    /* volatile */ int lock = 999;
    while (lock);
}

x86-64-clang-3.0.0 compiler上,其汇编代码如下所示。

main:                                   # @main
        mov     DWORD PTR [RSP - 4], 0
        mov     DWORD PTR [RSP - 8], 999

.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        cmp     DWORD PTR [RSP - 8], 0
        je      .LBB0_3
        jmp     .LBB0_1

.LBB0_3:
        mov     EAX, DWORD PTR [RSP - 4]
        ret

当在中注解volatile关键字时,结果如下。

main:                                   # @main
        mov     DWORD PTR [RSP - 4], 0
        mov     DWORD PTR [RSP - 8], 999

.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        mov     EAX, DWORD PTR [RSP - 8]
        cmp     EAX, 0
        je      .LBB0_3
        jmp     .LBB0_1

.LBB0_3:
        mov     EAX, DWORD PTR [RSP - 4]
        ret

我想知道和不明白的地方,

  • cmp DWORD PTR [RSP - 8], 0.〈---为什么用0进行比较,而DWORD PTR [RSP - 8]999保持在范围内?
  • 为什么要将DWORD PTR [RSP - 8]复制到EAX中,为什么要在0EAX之间进行比较?
aemubtdh

aemubtdh1#

看起来您忘记启用优化。-O0处理 * 所有 * 变量(register变量除外)pretty similarly to volatile for consistent debugging
启用优化后,编译器可以将非易失性加载从循环中提升出来。while(locked);的编译类似于源代码,如

if (locked) {
    while(1){}
}

或者因为locked有一个编译时常量初始化器,所以整个函数应该编译成jmp main(一个无限循环)。

有关更多详细信息,请参见MCU编程- C++ O2优化中断while循环

为什么要将DWORD PTR [RSP - 8]复制到EAX中,为什么要在0和EAX之间进行比较?
当你使用volatile时,一些编译器在将加载合并到其他指令的内存操作数方面做得更差。这只是一个错过的优化。
(虽然cmp [mem], imm可能效率 * 较低 *。我忘了它是否可以与JCC或其他东西宏融合。使用RIP相对寻址模式,它不能微融合加载,但寄存器基址是可以的。)
cmp EAX, 0是奇怪的,我猜优化被禁用的clang不会将test eax,eax作为与零进行比较的窥视孔优化。

如@user3386109所述,布尔上下文中的locked等效于C / C++中的locked != 0

lp0sw83n

lp0sw83n2#

编译器不知道缓存,它不是一个缓存的东西,它告诉编译器,值可能会在访问之间改变。因此,为了功能性地实现我们的代码,它需要按照我们要求的顺序执行我们要求的访问。不能优化。

void fun1 ( void )
{
    /* volatile */ int lock = 999;
    while (lock) continue;
}
void fun2 ( void )
{
    volatile int lock = 999;
    while (lock) continue;
}
volatile int vlock;
int ulock;
void fun3 ( void )
{
    while(vlock) continue;
}
void fun4 ( void )
{
    while(ulock) continue;
}
void fun5 ( void )
{
    vlock=3;
    vlock=4;
}
void fun6 ( void )
{
    ulock=3;
    ulock=4;
}

我觉得戴着手臂更容易看清......没关系。

Disassembly of section .text:

00001000 <fun1>:
    1000:   eafffffe    b   1000 <fun1>

00001004 <fun2>:
    1004:   e59f3018    ldr r3, [pc, #24]   ; 1024 <fun2+0x20>
    1008:   e24dd008    sub sp, sp, #8
    100c:   e58d3004    str r3, [sp, #4]
    1010:   e59d3004    ldr r3, [sp, #4]
    1014:   e3530000    cmp r3, #0
    1018:   1afffffc    bne 1010 <fun2+0xc>
    101c:   e28dd008    add sp, sp, #8
    1020:   e12fff1e    bx  lr
    1024:   000003e7    andeq   r0, r0, r7, ror #7

00001028 <fun3>:
    1028:   e59f200c    ldr r2, [pc, #12]   ; 103c <fun3+0x14>
    102c:   e5923000    ldr r3, [r2]
    1030:   e3530000    cmp r3, #0
    1034:   012fff1e    bxeq    lr
    1038:   eafffffb    b   102c <fun3+0x4>
    103c:   00002000    

00001040 <fun4>:
    1040:   e59f3014    ldr r3, [pc, #20]   ; 105c <fun4+0x1c>
    1044:   e5933000    ldr r3, [r3]
    1048:   e3530000    cmp r3, #0
    104c:   012fff1e    bxeq    lr
    1050:   e3530000    cmp r3, #0
    1054:   012fff1e    bxeq    lr
    1058:   eafffffa    b   1048 <fun4+0x8>
    105c:   00002004    

00001060 <fun5>:
    1060:   e3a01003    mov r1, #3
    1064:   e3a02004    mov r2, #4
    1068:   e59f3008    ldr r3, [pc, #8]    ; 1078 <fun5+0x18>
    106c:   e5831000    str r1, [r3]
    1070:   e5832000    str r2, [r3]
    1074:   e12fff1e    bx  lr
    1078:   00002000    

0000107c <fun6>:
    107c:   e3a02004    mov r2, #4
    1080:   e59f3004    ldr r3, [pc, #4]    ; 108c <fun6+0x10>
    1084:   e5832000    str r2, [r3]
    1088:   e12fff1e    bx  lr
    108c:   00002004    

Disassembly of section .bss:

00002000 <vlock>:
    2000:   00000000    

00002004 <ulock>:
    2004:   00000000

第一条最能说明问题:

00001000 <fun1>:
    1000:   eafffffe    b   1000 <fun1>

作为一个被初始化的非易失性局部变量,编译器可以假设它在两次访问之间不会改变值,所以它在while循环中永远不会改变,所以这本质上是一个while 1循环。如果初始值是零,这将是一个简单的返回,因为它永远不会是非零的,是非易失性的。
fun 2是局部变量,则需要构建堆栈帧。
它做的是假设代码试图做的事情,等待这个共享变量,一个可以在循环过程中改变的变量

1010:   e59d3004    ldr r3, [sp, #4]
    1014:   e3530000    cmp r3, #0
    1018:   1afffffc    bne 1010 <fun2+0xc>

因此它对它进行采样,并测试每次通过循环时采样的内容。
fun 3和fun 4处理相同,但更实际,因为函数代码的外部不会更改lock,非全局性对while循环没有多大意义。

102c:   e5923000    ldr r3, [r2]
1030:   e3530000    cmp r3, #0
1034:   012fff1e    bxeq    lr
1038:   eafffffb    b   102c <fun3+0x4>

对于volatile fun 3情况,必须在每个循环中读取和测试变量

1044:   e5933000    ldr r3, [r3]
1048:   e3530000    cmp r3, #0
104c:   012fff1e    bxeq    lr
1050:   e3530000    cmp r3, #0
1054:   012fff1e    bxeq    lr
1058:   eafffffa    b   1048 <fun4+0x8>

因为非易失性变量是全局变量,所以它必须对它进行一次采样,编译器在这里所做的非常有趣,必须考虑为什么它要这样做,但无论如何,你可以看到“循环”重新测试存储在寄存器(不是缓存的)中的值,该值永远不会随着适当的程序而改变。从功能上讲,我们要求它通过使用非易失性变量只读取变量一次,然后它会无限期地测试该值。
fun 5和fun 6进一步说明了volatile要求编译器在执行下一个操作/访问代码之前,先在变量的存储位置执行对变量的访问。非易失性时,编译器可以优化第一个存储,而只优化最后一个存储,就好像您将代码作为一个整体来看待此函数(fun 6)会将变数设定为4,因此函数会将变数设定为4。
x86解决方案同样有趣,repz retq到处都是(用我电脑上的编译器),不难找出那是怎么回事。
aarch 64、x86、mips、riscv、msp430和pdp 11后端都不会对fun 3()进行双重检查。
pdp 11实际上是更容易阅读的代码(这并不奇怪)

00000000 <_fun1>:
   0:   01ff            br  0 <_fun1>

00000002 <_fun2>:
   2:   65c6 fffe       add $-2, sp
   6:   15ce 03e7       mov $1747, (sp)
   a:   1380            mov (sp), r0
   c:   02fe            bne a <_fun2+0x8>
   e:   65c6 0002       add $2, sp
  12:   0087            rts pc

00000014 <_fun3>:
  14:   1dc0 0026       mov $3e <_vlock>, r0
  18:   02fd            bne 14 <_fun3>
  1a:   0087            rts pc

0000001c <_fun4>:
  1c:   1dc0 001c       mov $3c <_ulock>, r0
  20:   0bc0            tst r0
  22:   02fe            bne 20 <_fun4+0x4>
  24:   0087            rts pc

00000026 <_fun5>:
  26:   15f7 0003 0012  mov $3, $3e <_vlock>
  2c:   15f7 0004 000c  mov $4, $3e <_vlock>
  32:   0087            rts pc

00000034 <_fun6>:
  34:   15f7 0004 0002  mov $4, $3c <_ulock>
  3a:   0087            rts pc

(this为未链接版本)
cmp DWORD PTR [RSP - 8],0 .〈---为什么用0进行比较,而DWORD PTR [RSP - 8]的值在999以内?
而"真“”假“比较表示它等于零还是不等于零
为什么要将DWORD PTR [RSP - 8]复制到EAX中,为什么要在0和EAX之间进行比较?

mov -0x8(%rsp),%eax
cmp 0,%eax
cmp 0,-0x8(%rsp)

as so.s -o so.o
so.s: Assembler messages:
so.s:3: Error: too many memory references for `cmp'

比较需要一个寄存器,所以它读入一个寄存器,这样它就可以进行比较了,因为它不能在一条指令中完成立即数和内存访问之间的比较,如果他们能在一条指令中完成的话,他们早就做了。

相关问题