编译原理:volatile关键字
1、volatile有什么含义?有什么用法?
官方定义是:
一个变量也许会被后台程序改变。
关键字volatile与const绝对独立。它指示一个变量也许会被某种方式修改,这种方式按照正常程序流程分析是无法预知的(例如,一个变量也许会被一个中断服务程序所修改)。这个关键字使用以下语法定义:
volatile data-defiinition;
注:变量如果加了voletile修饰,则会从内存中重新装载内容,而不是直接从寄存器中拷贝内容。
volatile的作用是告知编译器,它修饰的变量随时都可能被改变,因此,编译后的程序每次在使用该变量的值时,都会从变量的内存地址中读取数据,而不是从寄存器中获取。
下面是volatile变量的几个例子:
- 并行设备的硬件寄存器(如:状态寄存器)
- 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
- 多线程应用中被几个任务共享的变量
2、volatile和编译器的优化有关:
在本次线程内,当读取一个变量时,为了提高读取速度,编译器进行优化时有时会先把变量读取到一个寄存器中;以后,再读取变量值时,就直接从寄存器中读取;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以保持一致。 在本次线程内,当读取一个变量时,为了提高读取速度,编译器进行优化时有时会先把变量读取到一个寄存器中;以后,再读取变量值时,就直接从寄存器中读取;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以保持一致。
当变量因别的线程值发生改变,上面寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
当寄存器因别的线程改变了值,原变量的值也不会改变,也会造成应用程序读取的值和实际的变量值不一致。
小例子:
#include <stdio.h>
#include <iostream>
int main(void)
{
const volatile int local = 10; // or const int local = 10;
int* ptr = (int*)&local;
printf("Initial value of local : %d \n", local);
*ptr = 100;
printf("Modified value of local: %d \n", local);
std::cin.get();
}
没有使用volatile时,通过g++ 编译后的结果:
00t00000000000400596 <main>:
400596: 55 push %rbp
400597: 48 89 e5 mov %rsp,%rbp
40059a: 48 83 ec 20 sub $0x20,%rsp
40059e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4005a5: 00 00
4005a7: 48 89 45 f8 mov %rax,-0x8(%rbp)
4005ab: 31 c0 xor %eax,%eax
//-0x14(%rbp)是local所在位置(也即,local=10)
4005ad: c7 45 ec 0a 00 00 00 movl $0xa,-0x14(%rbp)
//取local地址到寄存器
4005b4: 48 8d 45 ec lea -0x14(%rbp),%rax
//将local的地址赋给ptr(-0x10(%rbp))
4005b8: 48 89 45 f0 mov %rax,-0x10(%rbp)
//将10写入寄存器%esi,也即printf的第二个参数
4005bc: be 0a 00 00 00 mov $0xa,%esi
//%edi是printf的一个参数,也即字符串"Initial value...."
4005c1: bf 94 06 40 00 mov $0x400694,%edi
4005c6: b8 00 00 00 00 mov $0x0,%eax
4005cb: e8 a0 fe ff ff callq 400470 <printf@plt>
//取ptr的值,也即local的地址
4005d0: 48 8b 45 f0 mov -0x10(%rbp),%rax
//将100写入ptr所指向的地址,也即local
4005d4: c7 00 64 00 00 00 movl $0x64,(%rax)
//将10写入%esi,也即printf
4005da: be 0a 00 00 00 mov $0xa,%esi
4005df: bf b2 06 40 00 mov $0x4006b2,%edi
4005e4: b8 00 00 00 00 mov $0x0,%eax
4005e9: e8 82 fe ff ff callq 400470 <printf@plt>
4005ee: b8 00 00 00 00 mov $0x0,%eax
4005f3: 48 8b 55 f8 mov -0x8(%rbp),%rdx
4005f7: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
4005fe: 00 00
400600: 74 05 je 400607 <main+0x71>
400602: e8 59 fe ff ff callq 400460 <__stack_chk_fail@plt>
400607: c9 leaveq
400608: c3 retq
400609: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
可以看到在没有volatile的时候,local就直接被优化成了0xa;在第二次调用printf的时候,作为参数传给printf的时候,也是直接 mov $0xa, %esi;所以修改并没有体现。
使用volatile的汇编,重点要看的就是第二个printf之前的部分代码,在准备printf的参数,这里可以看到,多了一步从内存中取值的过程:
0000000000400596 <main>:
400596: 55 push %rbp
400597: 48 89 e5 mov %rsp,%rbp
40059a: 48 83 ec 20 sub $0x20,%rsp
40059e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
4005a5: 00 00
4005a7: 48 89 45 f8 mov %rax,-0x8(%rbp)
4005ab: 31 c0 xor %eax,%eax
4005ad: c7 45 ec 0a 00 00 00 movl $0xa,-0x14(%rbp)
4005b4: 48 8d 45 ec lea -0x14(%rbp),%rax
4005b8: 48 89 45 f0 mov %rax,-0x10(%rbp)
4005bc: 8b 45 ec mov -0x14(%rbp),%eax
4005bf: 89 c6 mov %eax,%esi
4005c1: bf 94 06 40 00 mov $0x400694,%edi
4005c6: b8 00 00 00 00 mov $0x0,%eax
4005cb: e8 a0 fe ff ff callq 400470 <printf@plt>
4005d0: 48 8b 45 f0 mov -0x10(%rbp),%rax
//依然是更新内存
4005d4: c7 00 64 00 00 00 movl $0x64,(%rax)
//取local的值(更新之后的,也即100)
4005da: 8b 45 ec mov -0x14(%rbp),%eax
// 将100放入%esi
4005dd: 89 c6 mov %eax,%esi
4005df: bf b2 06 40 00 mov $0x4006b2,%edi
4005e4: b8 00 00 00 00 mov $0x0,%eax
4005e9: e8 82 fe ff ff callq 400470 <printf@plt>
4005ee: b8 00 00 00 00 mov $0x0,%eax
4005f3: 48 8b 55 f8 mov -0x8(%rbp),%rdx
4005f7: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
即有volatile的时候,会执行一次:
mov -0x14(%rbp),%eax
mov %eax,%esi
从内存中重新装载内容,而不是直接从寄存器中拷贝内容。