2010-06-14 71 views
6

我正在使用基于x86的内核来操纵32位内存映射寄存器。只有当CPU产生32位宽的读取和写入该寄存器时,我的硬件才能正常工作。该寄存器在32位地址上对齐,并且在字节粒度上不可寻址。控制对存储器映射寄存器的读写访问宽度C

我该怎么做才能保证我的C(或C99)编译器只会在所有情况下生成完整的32位宽读写?

例如,如果我不喜欢这样的读 - 修改 - 写操作:

volatile uint32_t* p_reg = 0xCAFE0000; 
*p_reg |= 0x01; 

我不希望编译器耍小聪明的事实,只有底部字节的变化,并产生8- bit宽读/写。由于机器代码对于x86上的8位操作通常更密集,所以我害怕出现不必要的优化。一般禁用优化不是一种选择。

-----编辑-------
一个有趣和非常相关论文:http://www.cs.utah.edu/~regehr/papers/emsoft08-preprint.pdf

+0

对不起“自我推销”,但您可能会发现该项目对测试内存映射硬件或设置/读取内存映射寄存器有用:https://code.google.com/p/jeeamtee/wiki/Main。此致,Valentin Heinitz – 2010-10-20 15:31:36

回答

6

您的疑虑包含在volatile限定符中。

6.7.3/6“类型限定符”云:

具有volatile限定类型可能未知的方式实施或具有其他未知的副作用被修改的对象。因此,任何涉及这样的对象的表达都应严格按照抽象机器的规则进行评估,如5.1.2.3所述。此外,在每个序列点上,最后存储在对象中的值应与抽象机器规定的值一致,但前面提到的未知因素会对其进行修改。对具有易失性限定类型的对象的访问是由实现定义的。

5.1.2.3“计划执行”说(除其他事项外):

在抽象的机器,由语义指定的所有表达式求值。

这之后,其通常被称为“AS-如果”规则,其允许实现不遵循抽象机语义的句子如果最终结果是一样的:

如果实际实现可以推断出它的值没有被使用,并且没有产生所需的副作用(包括由调用函数或访问volatile对象引起的任何副作用),则实际实现不需要评估表达式的一部分。

但是,6.7.3/6本质上说,表达式中使用的volatile限定类型不能应用'as-if'规则 - 必须遵循实际的抽象机器语义。因此,如果取消指向易失性32位类型的指针,则必须读取或写入完整的32位值(取决于操作)。

0

好了,一般来说我不会期望它优化了高位字节,如果你将该寄存器键入为32位易失性。由于使用volatile关键字,编译器不能认为高位字节的值是0x00。因此,即使您仅使用8位字面值,它也必须写入完整的32位。我在0x86或Ti处理器或其他嵌入式处理器上从未遇到过这个问题。一般来说volatile关键字就足够了。唯一的一点是,如果处理器本身不支持您要编写的字大小,但这对32位数字的0x86不应该是个问题。

尽管编译器可以生成使用4位写入的指令流,但这不会是单个32位写入时的处理器时间或指令空间的优化。

+0

volatile限定符不会阻止编译器将访问宽度从32位变窄为8位。从它的角度来看,高24位易失性位未被触及。而且,8位指令编码导致更少的指令字节,所以-Os优化有理由偏好这一点。 – srking 2010-06-14 21:02:04

+0

它可以防止编译器假定该值与读取的值相同。它不能缩小访问范围,因为它需要写回所有的32位数据以保证这个值是假设的。 – NoMoreZealots 2010-06-15 13:25:54

0

如果在访问硬件时不使用字节(无符号字符)类型,编译器将不会生成8位数据传输指令。

volatile uint32_t* p_reg = 0xCAFE0000; 
const uint32_t value = 0x01; // This trick tells the compiler the constant is 32 bits. 
*p_reg |= value; 

你将不得不读端口为32位的值,修改该值,然后写回:

uint32_t reg_value = *p_reg; 
reg_value |= 0x01; 
*p_reg = reg_value; 
+0

同意,但寻找比“更好的机会”更强大的东西。 – srking 2010-06-14 21:02:28

4

只有这样,才能保证编译器会做正确的事情是用汇编语言编写你的负载和存储例程,并从C中调用它们。我多年来使用的编译器中有100%可以并且会错误(包括GCC)。

有时候优化器会让你获得,例如你希望将一些常量在编译器中看作是一个小数字0x10可以说,写入一个32位寄存器,这就是你特别提出的问题以及我所看到的其他好东西编译器试着去做。有些编译器会决定用8位写入代替32位写入并更改指令更便宜。随着编译器试图节省程序空间而不仅仅是内存周期,变量指令长度目标将会使这种情况变得更糟。 (xor ax,ax而不是mov eax,0例如)

随着一些像gcc一样不断发展的代码,今天运行的代码没有明天的工作保证(甚至不能编译某些版本的gcc gcc版本)。同样,在你的办公桌上使用编译器的代码可能不适用于其他人。

进行猜测和试验,并创建加载和存储功能。

这样做的副作用是,如果/当您想以某种方式模拟您的代码或让代码在应用程序空间而不是金属上运行(反之亦然),则创建一个很好的抽象层,汇编程序功能可以替换为模拟目标,也可以替换为通过网络与设备连接的目标的代码等。

+0

我已经写了15年的硬件接口,从来没有写过汇编器保证32位写访问。实际上,易失性告诉编译器,它不能假定指令之间存储器地址的先前值。 – NoMoreZealots 2010-06-15 13:41:49

+0

表示同意,但我失败了。如果你将你的加载和存储例程作为一个独立的.c文件从你调用它的那个加载和存储例程,并且不内联它们,并且不让说llvm尝试优化整个应用程序,那么我会添加到我的评论中你可以避免使用汇编程序,并且可以可靠地工作 – 2010-06-15 19:34:15

+1

如果我们想玩这个游戏,那么我已经做了20多年,很多平台上有很多编译器。大多数情况下,它可以工作,但有时候你会陷入窘境,而你无法弄清楚编译器为什么优化或改变你的代码。它可以工作数周或数月,然后添加或更改第n行代码,并改变编译方式。用户说保证,如果你不想要“保证工作”,而是“大多数时间工作”,说超过99%但低于100%,那么易失性(在单独的文件中的独立功能)将满足该要求。 – 2010-06-15 19:38:09

0

由于对硬件的读 - 修改 - 写操作对于几条指令来说总是有很大的风险,所以大多数处理器都提供了一条指令,用一条不能中断的单指令来操作寄存器/存储器。

根据您操作的注册类型,它可能会在您的修改阶段更改,然后您会写回一个错误值。

因为dwelch建议在汇编中编写自己的读取 - 修改 - 写入功能,所以我会推荐它。

我从来没有听说过优化类型的编译器(进行类型转换以优化目的)。如果它被声明为int32,它总是一个int32,并且将始终在内存中对齐。检查你的编译器文档,看看各种优化是如何工作的。

我想我知道你的关注来自哪里,结构。结构通常被填充到最佳对齐。这就是为什么你需要在它们周围包装一个#pragma pack()来让它们字节对齐。

你可以单步穿过程序集,然后你会看到编译器如何翻译你的代码。我很确定它没有改变你的类型。