【问题标题】:Strange pointer arithmetic奇怪的指针算法
【发布时间】:2014-01-31 19:33:38
【问题描述】:

我遇到了指针算术的奇怪行为。我正在开发一个程序,使用 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 到 unsigned long。

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;但没有变化。

我可以想到小/大端,但在这里我得到了意想不到的数据(以前的字节)并且没有顺序颠倒。

【问题讨论】:

  • 感谢您快速回复,将我重定向到重复的问题。我的 ARM 架构是 ARM7TDMI,正如大家所指出的,它不支持非对齐访问。但可能出于同样的原因,即使结构成员访问也会给出不正确的结果。出于这个原因,我无法在我的 SD 卡上使用任何现成的库,比如 fat。任何提示/解决方案都会有很大帮助。
  • @user3258584 尝试禁用旋转功能并强制 cpu 创建中止并在那里处理未对齐的访问。您在编译时尝试过-mno-unaligned-access 吗? gcc.gnu.org/onlinedocs/gcc/ARM-Options.html
  • 感谢您的建议。我一定会尝试这个解决方案。并在此处更新结果。

标签: gcc arm pointer-arithmetic


【解决方案1】:

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

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

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

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

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

数据表应描述处理器的预期行为。

更新:

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

因此,无论哪种情况,您都应该确保无论何时执行大小为 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. 结构中的每个字段都必须位于可以被该字段本身的大小整除的偏移量(结构内)。

例外:

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

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

【讨论】:

  • 事实上,Cortex-M3 does support unaligned accesses,尽管您可以为此类访问启用故障生成。 OP 有一个较旧的基于 ARM7 的芯片。
  • 感谢您的快速回复。我该如何处理这个问题,特别是在访问结构成员时?
  • 谢谢。我遵循了这种方式,并且运行良好。但是,这种方法不适用于预定义的结构,例如磁盘上的胖引导记录。但我设法与他们合作。
  • 不客气;正如我在答案末尾提到的那样,您仍然必须确保您使用的每个 myStruct_t* 变量都指向一个可被最大字段大小整除的内存地址。因此,如果您将 FAT 从磁盘读取到 RAM,那么您应该确保您在 RAM 中的 FAT 结构位于一个可以被其中最大字段的大小整除的内存地址。
  • @user3258584 解析类似 FAT 的过程称为 序列化。通常,您使用char * 并提取每个字节并执行 shiftsOR 来累积值。较新的 ARM 有汇编程序来帮助;如果这是一个应用程序热路径,您可以使用优化。您的编译器可能支持__attribute((packed)); 等。
【解决方案2】:

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

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

请参阅: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 类型和可能的协处理器值。它也是高度不可移植的代码。

【讨论】:

    【解决方案3】:

    这被称为“未定义的行为”。您的代码将一个无效的unsigned long * 值转换为unsigned long *。该操作的语义是未定义的行为,这意味着几乎任何事情都可能发生*。

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

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

    基本上,不要那样做。

    更安全的替代方法是将字节写入uint32_t。比如:

    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 &amp;&amp; CHAR_BITS == 8,但这是一个更安全的假设。 (也就是说,它几乎可以在任何 8 位字节的机器上工作。)

    【讨论】:

      猜你喜欢
      • 2016-01-31
      • 1970-01-01
      • 1970-01-01
      • 2013-06-12
      • 1970-01-01
      • 2020-06-14
      • 1970-01-01
      • 2023-04-10
      • 2020-01-22
      相关资源
      最近更新 更多