编译器优化导致硬件寄存器写入失败的一个例子

最近在调试导致一块网卡不能发包的驱动,最后追踪到这样一段代码:

// Memory-mapped buffer
struct Registers
{
    unsigned long DeviceCtrl;
    ..
};

#define DEVICE_RESET (1 << 26)
..

// m_pReg是一段memory-mapped I/O的起始地址,DeviceCtrl对应的是一个32位硬件寄存器
m_pReg->DeviceCtrl |= DEVICE_RESET;
如果改成下面的代码就可以正常工作:

m_pReg->DeviceCtrl = m_pReg->DeviceCtrl | DEVICE_RESET;
百思不得其解。我的印象中,这两个运算结果应该是完全一样的。决定打开编译器开关(msvc60, /FAs),看看生成的汇编有何不同。

#1
m_pReg->DeviceCtrl = m_pReg->DeviceCtrl | DEVICE_RESET;

汇编
mov    eax, DWORD PTR [esi+180]    ; 1.把指针变量m_pReg的值load到eax寄存器中
mov    eax, DWORD PTR [eax]        ; 2.把DeviceCtrl的值load到eax寄存器中,eax+0
or     eax, 67108864               ; 3.把(1<<26)和DeviceCtrl取或,再存入eax
mov    ecx, DWORD PTR [esi+180]    ; 4.同1
mov    DWORD PTR [ecx], eax        ; 5.把eax写回内存,完成对DeviceCtrl的更新

第一段汇编很容易理解,也和我想的一样。

#2
m_pReg->DeviceCtrl |= DEVICE_RESET;

汇编
mov    eax, DWORD PTR [esi+180]
pop    ecx
or     BYTE PTR [eax+3], 4
这段汇编大大出乎我的意料。首先,3和4哪来的?仔细想想,才发现这是编译器一个正常的优化:我要求或4个字节(1<<26),因为最低3个字节都是0(0x04000000),所以等价于只需要或一个字节,也就是最高字节0x04。由于CPU是Intel 的i5-440,小数端处理器,所以最高字节地址相对于DeviceCtrl地址的偏移正好是3。于是就产生了上面的汇编。

看上去也无不妥,为什么只写一个字节就会导致不发包呢?我的判断是肯定没有写下去,或者说没有被网卡接受。因为如果写下去了,上面两段代码的结局必然是一样的。。而且我在赋值结束后又读了下该寄存器,发现还是老的值。

第一个想法是CPU放在地址总线上的地址不对,导致网卡拒绝接受。我知道,网卡在接收数据前会检查地址总线,如果放在总线上的地址不是期望的(比如在这个地址根本就不存在任何寄存器),它就会忽略此写入请求。这里,32位寄存器DeviceCtrl的地址(偏移)是0,而下一个寄存器是4。3肯定不是一个合理的32位寄存器的起始地址。

于是乎就以为找到了答案,释然.....

.....

转念一想,不对。

x86 32位处理器的地址总线上一定是4字节对齐的地址(i5-440就是32位架构),所以即使程序要求在地址是3的地方写一个字节,CPU产生的地址也是对齐后的地址0。所以不存在地址非法被设备拒绝的情况。那是怎么回事?原来虽然地址是正确的,但只写了一个字节。CPU有4个Byte Enable信号对应从高到低4个字节,如果只传输最高字节,那只有对应的一个Byte Enable信号打开。网卡会不会检测该信号来决定必须4个信号同时打开时(写4字节)才接受数据呢?

这个想法最终在网卡的data sheet中得到了佐证:

The device has limited support of read and write requests when only part of the byte enable bits are set as described later in this section. Partial writes to the MSI-X table are supported. All other partial writes are ignored and silently dropped.

看来,涉及到硬件寄存器的操作时,还是一步一个脚印,要小心编译器优化!

后续:

我试了下,在VC6中即使关闭所有的优化(/Od)还是会有这个问题。相比较,在VC 2012中即使打开所有优化(Full Optimization, /Ox),也不会有这类数值优化存在。可能是微软发现了可能存在的问题,索性取消了。这就是所谓的premature优化吧。

在开发底层代码如驱动时,如果出现问题,可以先试着把所有优化关闭,或者跑跑debug版本(一般debug版本会自动关闭优化,或者大多数优化),看看问题是否还存在。

你可能感兴趣的:(嵌入式)