奇怪的指针算术

时间:2014-01-31 19:33:39

标签: gcc arm pointer-arithmetic

我遇到了指针运算的奇怪行为。我正在开发一个使用ARM GNU工具链(在Linux上)从LPC2148开发SD卡的程序。我的SD卡扇区包含数据(十六进制),如(从linux“xxd”命令检查): fe 2a 01 34 21 45 aa 35 90 75 52 78 打印单个字节时,打印效果非常好。

char *ch = buffer; /* char buffer[512]; */
for(i=0; i<12; i++)
    debug("%x ", *ch++);

这里调试功能在UART上发送输出。 但是指针运算特意添加一个不是4的倍数的数字给出了太奇怪的结果。     uint32_t * p; // uint32_t是无符号长的typedef。

p = (uint32_t*)((char*)buffer + 0);
debug("%x ", *p);   // prints 34012afe   // correct

p = (uint32_t*)((char*)buffer + 4);
debug("%x ", *p);   // prints 35aa4521  // correct

p = (uint32_t*)((char*)buffer + 2);
debug("%x ", *p);   // prints 0134fe2a  // TOO STRANGE??

我选择了错误的编译器选项吗?请帮忙。 我尝试了优化选项-0和-s;但没有变化。

我可以想到小/大端,但在这里我得到了意外的数据(前一个字节)而没有顺序倒转。

3 个答案:

答案 0 :(得分:2)

您的CPU架构必须支持未对齐的加载和存储操作。

据我所知,它没有(我一直在使用STM32,这是一个基于ARM的皮质)。

如果您尝试从地址中读取uint32_t值,该地址不能被uint32_t整除(即不能被4整除),那么在“好”的情况下,您将获得错误的输出。

我不确定buffer的地址是什么,但您在问题中描述的三次uint32_t次尝试中至少有一次要求处理器执行未对齐的加载操作。

在STM32上,您将收到内存访问冲突(导致硬故障异常)。

数据表应提供处理器预期行为的说明。

UPDATE:

即使您的处理器 支持未对齐的加载和存储操作,您也应该尽量避免使用它们,因为它可能会影响整体运行时间(与“正常”加载和存储操作相比)

因此,在任何一种情况下,都应该确保无论何时执行大小为N的内存访问(读取或写入)操作,目标地址都可以被N整除。例如:

uint08_t x = *(uint08_t*)y; // 'y' must point to a memory address divisible by 1
uint16_t x = *(uint16_t*)y; // 'y' must point to a memory address divisible by 2
uint32_t x = *(uint32_t*)y; // 'y' must point to a memory address divisible by 4
uint64_t x = *(uint64_t*)y; // 'y' must point to a memory address divisible by 8

为了确保您的数据结构,请始终定义它们,以便每个字段x都位于可被sizeof(x)整除的偏移处。例如:

struct
{
    uint16_t a; // offset 0, divisible by sizeof(uint16_t), which is 2
    uint08_t b; // offset 2, divisible by sizeof(uint08_t), which is 1
    uint08_t a; // offset 3, divisible by sizeof(uint08_t), which is 1
    uint32_t c; // offset 4, divisible by sizeof(uint32_t), which is 4
    uint64_t d; // offset 8, divisible by sizeof(uint64_t), which is 8
}

请注意,保证您的数据结构“安全”,并且您仍需确保您使用的每个myStruct_t*变量都指向到一个可被最大字段大小整除的内存地址(在上面的例子中,为8)。

内容:

您需要遵循两个基本规则:

  1. 您的结构的每个实例都必须位于一个内存地址,该地址可以被结构中最大字段的大小整除。

  2. 结构中的每个字段必须位于偏移量(在结构内),该偏移量可以被该字段本身的大小整除。

  3. 例外:

    1. 如果CPU架构支持未对齐的加载和存储操作,则可能违反规则#1。然而,这样的操作通常效率较低(要求编译器在“之间”添加NOP)。理想情况下,即使编译器支持未对齐操作,也应该努力遵循规则#1 ,并让编译器知道数据已经完全对齐(使用专用#pragma),按顺序允许编译器在可能的情况下使用对齐的操作。

    2. 如果编译器自动生成所需的填充,则可能违反规则#2。当然,这会改变结构的每个实例的大小。建议始终使用显式填充(而不是依赖于当前编译器,可以在以后的某个时间点替换它。)

答案 1 :(得分:2)

LDR是加载数据的ARM指令。您已向编译器说谎指针是32位值。它没有正确对齐。你支付了价格。这是LDR文档,

  

如果地址不是字对齐的,则加载的值将向右旋转位[1:0]的值的8倍。

请参阅:4.2.1. LDR and STR, words and unsigned bytes,尤其是字词转移的地址对齐部分。

基本上你的代码就像,

  p = (uint32_t*)((char*)buffer + 0);
  p = (p>>16)|(p<<16);
  debug("%x ", *p);   // prints 0134fe2a

但已编码为ARM上的一条指令。此行为取决于ARM CPU类型和可能的协处理器值。它也是高度不可移植的代码。

答案 2 :(得分:0)

它被称为&#34;未定义的行为&#34;。您的代码正在将unsigned long *中的有效值unsigned long *转换为buffer。该操作的语义是未定义的行为,这意味着几乎任何事情都可能发生*。

在这种情况下,您的两个示例的行为与预期相符的原因是因为您很幸运,uint32_t恰好是字对齐的。你的第三个例子并不那么幸运(如果是的话,其他两个就没有了),所以你最终得到了一个指针,在2个最低位有额外的垃圾。根据您使用的ARM版本,可能会导致未对齐的读取(它看起来是您所希望的),或者它可能导致对齐读取(使用最重要的30位)和旋转(字)旋转了最低有效2位中指示的字节数)。很明显,后者是你的第三个例子中发生的事情。

无论如何,从技术上讲,所有3个示例输出都是正确的。程序崩溃所有3个程序也是正确的。

基本上,不要这样做。

更安全的替代方法是将字节写入uint32_t w; memcpy(&w, buffer, 4); debug("%x ", w); memcpy(&w, buffer+4, 4); debug("%x ", w); memcpy(&w, buffer+2, 4); debug("%x ", w); 。类似的东西:

sizeof(uint32_t) == 4 && CHAR_BITS == 8

当然,这仍然是$("#btnCloseMenu").on("click", function() { $('.dropdown-menu').hide(); }); ,但这是一个更安全的假设。 (即它应该适用于任何具有8位字节的机器。)