使用 volatile 破坏系统代码的九种方法

首先怼上:
It’s hard to overstate how bad an idea it is for a compiler to use strange heuristics about code structure to guess the developer’s intent.
编译器使用奇怪的试探法来猜测开发人员的意图,这个想法有多糟糕,怎么强调都不为过。

 
 

1 两个典型用法

(1)

#define TCNT1 (*(volatile uint16_t *)(0x4C))
signed char a, b, c;
uint16_t time_mul (void) {
  uint16_t first = TCNT1;
  c = a * b;
  uint16_t second = TCNT1;
  return second - first;
}

(2)

volatile int done;
__attribute((signal)) void __vector_4 (void) {
  done = 1;
}
void wait_for_done (void) {
  while (!done) ;
}

这个attribute signal我搜了一下,他要求两点:
1.中断退出时,必须恢复使用过的寄存器。
2.使用RETI退出中断,而不是RET。AVR单片机进入中断会关闭中断,退出时使用RETI重新开启中断。

 
 

2 volatile使用的太多了

(1)volatile可能会隐藏bug,比如并发问题。
(2)用的太多会导致编译器优化效率下降。
(3)它引入的开销很难被追踪到,因为已经分散在系统各个位置了。
(4)不写文档说明,别人很难意会你用volatile的用意。。。

建议,只有在你非常明确要使用volatile时才使用,别瞎鸡儿用。

 
 

3 限定符放错位置

volatile限定符遵守的规则和const类型。
那么就有如下情况:

int *p;                              // pointer to int
volatile int *p_to_vol;              // pointer to volatile int
int *volatile vol_p;                 // volatile pointer to int
volatile int *volatile vol_p_to_vol; // volatile pointer to volatile int

注意区分指针是volatile的,还是目标地址是volatile的。
如果是目标地址volatile,我们可以进行typedef如
typedef volatile int vint
进而使用它定义变量,就不易混淆了(???)
vint *REG = 0xFEED;

struct/unio也可以是volatile的,则它所有的成员也都是volatile的。

这种情况非常特殊:
const volatile int *p
const代表我不会store to it而不是它不会改变。
所以这种使用方法很有用,比如定义一个会自动变化的timer时,我们不会向他存储,但是需要每次得到更新后的数据。这个定义在C标准中是明确指出的。

 
 

4 不一致的限定符

1、
c文件中使用定义如下
volatile unsigned long ipi_count;
但是在头文件中却这样声明,
extern unsigned long ipi_count;
最新版本的GCC对这样的情况会报错。
但是一些嵌入式编译器不会。

 
2、
隐式类型转换,把一个volatile指针,传递给一个函数,但它的参数并没有volatile限定符。
这会导致编译器警告。
显式类型转换,不会警告,在C语言标准中,这是未定义行为。

Summary: Never use inconsistent qualification.
If a variable is declared as volatile, then all accesses to it, direct or indirect, must be through volatiles and pointers-to-volatile.

 
 

5 volatile变量和非volatile变量在一起时,必须强调访问顺序

volatile int ready;
int message[100];
void foo (int i) {
  message[i/10] = 42;
  // asm volatile ("" : : : "memory");
  ready = 1;
}

编译器会把ready的赋值搞到message赋值之前,这就会导致bug。(大多数编译器都会这样做!)
有一个方法可以避免这样情况,就是给message也加上volatile。但是在整个程序当中,这会减少很多优化。

我们需要的是一个编译器屏障compiler barrier
C标准没有定义这个,但是很多编译器都有定义。
asm volatile ("" : : : "memory");
这条指令之前的内存读写,不会搞到这条指令之后。
这条指令之后的内存读写,不会搞到这条指令之前。
编译器屏障是对编译器读写指令重排序而言的,内存屏障是对处理器指令重排序而言的。

 
 

6 volatile不能保证原子性,应该用锁

A volatile read or write of a 32-bit int is atomic on most modern hardware, but volatile has nothing to do with it.
32位对齐的访问在现代处理器上,是原子性的,但是跟volatile没有关系。
 
锁也要阻止编译器重排序,(那两行内联汇编)
这是AVR单片机中TinyOS里的例子,用于获取、释放全局的中断锁,

char__nesc_atomic_start(void) {
  char result = SREG;
  __nesc_disable_interrupt();
  asm volatile("" : : : "memory");
  return result;
}
void __nesc_atomic_end(char save) {
  asm volatile("" : : : "memory");
  SREG = save;
}

因为这俩函数通常是inline的,可能出现在任何位置。

 
 

7 volatile多处理器情况下,作用有限

volatile对于指令重排序、多处理器的情况,作用是有限的。
它只规定要去内存读写数据,而没有限定硬件的具体行为。

ARM Linux里的自旋锁:

static inline void arch_spin_unlock(arch_spinlock_t *lock) {
  smp_mb();
  __asm__ __volatile__("str %1, [%0]\n" : : "r" (&lock->lock), "r" (0) : "cc");
}
在解锁之前,smp_mb先执行。

多处理器内存屏障smp_mb可以简单描述为
__asm__ __volatile__ ("dmb" : : : "memory");
它即是编译器优化的屏障,也是内存屏障。

Summary: Without help from properly designed synchronization libraries, writing correct concurrent code on an out-of-order machine or multiprocessor is extremely hard, and volatile is of little help.
不使用同步库代码,直接在乱序CPU和多处理器上解决并发问题,非常难,volatile作用很小。

 
 

8 多线程中的volatile没有任何卵用

乱用,只会导致编译器无法优化代码,使程序变慢。

Arch Robison says that volatile is almost useless for multi-threaded programming.
这哥们儿好像是《结构化并行程序设计》这本书的作者。

 
 

9 编译器无视volatile的情况

volatile int x;
void foo (void) {
  x = x;
}
我想load这个x然后存回到它自己。
MSP430编译器说滚:
$ msp430-gcc -O vol.c -S -o -
foo:
  ret

GCC 4.0之后的版本会修正这个问题,但是之前的版本都会说滚。

 
 

原文地址: Nine ways to break your systems code using volatile

 
 

Linus祖师爷说

原文地址: volatile considered evil

The only acceptable uses for “volatile” are:

  • in -code-, i.e., for things like the definition of “readb()” etc, where we use it to force a particular access.
  • with inline asm
  • on “jiffies”, for stupid legacy reasons(it’s like a random number)
     

The only thing they do is(使用volatile的人)

  • potentially make the compiler generate worse code for no reason (the “no reason” being that if there aren’t any barriers in between, the compiler -should- merge accesses合并内存访问)
  • make some people -believe- that that compiler does something “right”.
     
I suspect we should just face up to the fact that:1"volatile" on kernel data is basically always a bug, and you should 
     use locking. "volatile" doesn't help anything at all with memory
     ordering and friends, so it's insane to think it "solves" anything on
     its own.
     在内核中别用volatile2)on "iomem" pointers it does make sense, but those need special
     accessor functions -anyway-, so things like test_bit() wouldn't work
     on them.
     人家test_bit确实需要每次都访问内存,但是人家依赖的是原子操作。
     
(3if you spin on a value [that's] changing, you should use "cpu_relax()" or
     "barrier()" anyway, which will force gcc to re-load any values from
     memory over the loop.
     如果你在用自旋锁,你反正是要用cpu_relax()或者barrier()它们都会强制GCC做一次对内存的重新加载。
     跟volatile更没关系。

 
 
 
完。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值