通过反汇编分析C语言中volatile关键字的含义

时间:2021-12-26 04:05:08

根据C语言标准,volatile关键字的作用是禁止编译器对相关变量的存取进行优化。本文利用VC 2010和GCC 4.4.7,分析volatile关键字对生成的汇编代码的影响,以验证volatile的具体含义。

VC 2010

以下是基础C代码

int gMark = 1;

int _tmain(int argc, _TCHAR* argv[])
{
while(gMark){
}
return 0;
}
以下是VC 2010 Releae编译后的汇编代码,可以看到循环直接被被优化掉了。

00A51000  jmp         wmain (0A51000h) 

如果把gMark声明为volatile int,再次VC 2010 Releae编译,生成的汇编代码如下,可以看到每次循环都要重新读取一次gMark所在的内存。

01071000 mov         eax,dword ptr [gMark(1073018h)]

01071005 test        eax,eax 

01071007  jne         wmain (1071000h)

GCC 4.4.7

以下是基础C代码

int gMark = 1;
int main()
{
while(gMark){
}
return 0;
}
以下是g++ -O3编译后的反汇编代码,可以看到循环直接被被优化掉了,与VC完全一样。

Dump of assembler code forfunction main:

   0x0000000000400680 <+0>:  jmp   0x400680 <main>

Endof assembler dump.

如果把gMark声明为volatile int,g++ -O3编译后的汇编代码如下。可以看到每次循环都要重新读取一次gMark所在的内存,这点与VC完全一致。

Dumpof assembler code for function main:

   0x0000000000400680 <+0>:  movl  $0x1,-0x4(%rsp)

   0x0000000000400688 <+8>:  nopl  0x0(%rax,%rax,1)

   0x0000000000400690 <+16>: mov   -0x4(%rsp),%eax

   0x0000000000400694 <+20>: test  %eax,%eax

   0x0000000000400696 <+22>: jne   0x400690 <main+16>

   0x0000000000400698 <+24>: repz retq

Endof assembler dump.

结论

如果不加volatile关键字,编译器对C代码中的多次读取该变量的操作,有可能只生成一次加载内存到寄存器的指令。如果加上volatile,则每次C源码中用到该变量,都需要从内存中重新加载。

其它

以上基本说明了volatile的含义。另外,如果C代码的函数中有多处对volatile变量的读写,编译器还必须在生成的汇编代码中保留这些volatile变量的读写顺序,见wiki(http://en.wikipedia.org/wiki/Memory_barrier)。

还有一个经常混淆的问题是,volatile是否还定义了原子语义?用volatile修饰变量,能不能保证对一个变量的读写操作不会被其他线程打断?我们说根据C标准,volatile是没有原子语义的。另外,刚才生成的汇编代码也说明了volatile变量的读写是没有原子性的。

最后是volatile和memory barrier到底是什么关系。我的理解是,volatile是对编译器优化进行干预,memory barrier是对CPU的乱序执行进行干预,因此两者没有关系。但是有些编译器对volatile变量赋予了acquire和release语义,从而使得volatile能有一定的memory barrier的作用。详细见http://msdn.microsoft.com/zh-cn/library/ms686355。因为这和编译器以及编译器的版本是密切相关的,对于开发应用程序的程序员,就当它不存在好了。