Gcc 奇异指针算法

Gcc 奇异指针算法,gcc,arm,pointer-arithmetic,Gcc,Arm,Pointer Arithmetic,我发现指针运算的行为太奇怪了。我正在开发一个在Linux上使用ARM GNU工具链从LPC2148开发SD卡的程序。我的SD卡a扇区包含从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++); 我是否选择了错误的编译器选

我发现指针运算的行为太奇怪了。我正在开发一个在Linux上使用ARM GNU工具链从LPC2148开发SD卡的程序。我的SD卡a扇区包含从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++);
我是否选择了错误的编译器选项?请帮忙。 我尝试了优化选项-0和-s;但是没有变化


我可以想到little/big-endian,但这里我得到了以前字节的意外数据,并且没有顺序反转。

您的CPU体系结构必须支持未对齐的加载和存储操作

据我所知,它不是,我一直在使用STM32,这是一种基于手臂的皮质

如果您试图从一个地址读取一个uint32_t值,而该地址不能被uint32_t的大小整除,即不能被4整除,那么在好的情况下,您只会得到错误的输出

我不确定缓冲区的地址,但您在问题中描述的三次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
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
}
为了在数据结构中确保这一点,请始终对其进行定义,使每个字段x位于可被sizeofx整除的偏移量处。例如:

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
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整除

总结: 您需要遵循两条基本规则:

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

结构中的每个字段必须位于结构中的偏移量处,该偏移量可被该字段本身的大小整除

例外情况:

如果CPU体系结构支持未对齐的加载和存储操作,则可能违反规则1。然而,这种操作通常效率较低,需要编译器在两者之间添加NOP。理想情况下,即使编译器支持未对齐的操作,也应该努力遵循规则1,并让编译器知道数据使用专用的pragma进行了很好的对齐,以便允许编译器在可能的情况下使用对齐的操作

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


您的CPU体系结构必须支持未对齐的加载和存储操作

据我所知,它不是,我一直在使用STM32,这是一种基于手臂的皮质

如果您试图从一个地址读取一个uint32_t值,而该地址不能被uint32_t的大小整除,即不能被4整除,那么在好的情况下,您只会得到错误的输出

我不确定缓冲区的地址,但您在问题中描述的三次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
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
}
为了在数据结构中确保这一点,请始终对其进行定义,使每个字段x位于可被sizeofx整除的偏移量处。例如:

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
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整除

总结: 您需要遵循两条基本规则:

每一个我 结构的nstance必须位于可被结构中最大字段大小整除的内存地址

结构中的每个字段必须位于结构中的偏移量处,该偏移量可被该字段本身的大小整除

例外情况:

如果CPU体系结构支持未对齐的加载和存储操作,则可能违反规则1。然而,这种操作通常效率较低,需要编译器在两者之间添加NOP。理想情况下,即使编译器支持未对齐的操作,也应该努力遵循规则1,并让编译器知道数据使用专用的pragma进行了很好的对齐,以便允许编译器在可能的情况下使用对齐的操作

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

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

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

请参阅:,特别是字传输的地址对齐部分

基本上你的代码是这样的

  p = (uint32_t*)((char*)buffer + 0);
  p = (p>>16)|(p<<16);
  debug("%x ", *p);   // prints 0134fe2a
但是已经编码到手臂上的一条指令。此行为取决于ARM CPU类型以及可能的协处理器值。它也是高度不可移植的代码。

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

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

请参阅:,特别是字传输的地址对齐部分

基本上你的代码是这样的

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

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

它被称为未定义行为。代码正在将无效的无符号长*值强制转换为无符号长*。该操作的语义是未定义的行为,这意味着几乎任何事情都可能发生*

在本例中,您的两个示例的行为符合预期的原因是您运气好,缓冲区恰好是单词对齐的。您的第三个示例没有那么幸运,如果幸运的话,那么其他两个就没有那么幸运了,因此您最终得到了一个指针,在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);

当然,这仍然假设sizeofuint32_t==4&&CHAR_位==8,但这是一个更安全的假设。也就是说,它几乎可以在任何具有8位字节的机器上工作。

它被称为未定义行为。代码正在将无效的无符号长*值强制转换为无符号长*。该操作的语义是未定义的行为,这意味着几乎任何事情都可能发生*

在本例中,您的两个示例的行为符合预期的原因是您运气好,缓冲区恰好是单词对齐的。您的第三个示例没有那么幸运,如果幸运的话,那么其他两个就没有那么幸运了,因此您最终得到了一个指针,在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);
当然,这仍然假设sizeofuint32_t==4&&CHAR_位==8,但这是一个更安全的假设。也就是说,它应该可以在几乎所有具有8位字节的机器上工作。

事实上,Cortex-M3可以生成
r此类访问。OP有一个旧的基于ARM7的芯片。感谢您的快速回复。如何处理此问题,特别是在访问结构件时?谢谢。我按照这条路走,效果很好。但是,这种方法不适用于预定义的结构,如磁盘上的fat引导记录。但我还是设法和他们合作了;正如我在答案末尾提到的,您仍然必须确保您使用的每个myStruct_t*变量都指向一个可以被最大字段大小整除的内存地址。因此,如果您将FAT从磁盘读取到RAM,那么您应该确保RAM中的FAT结构位于一个内存地址,该地址可以被其中最大字段的大小整除。@user3258584解析类似FAT的内容的过程称为序列化。通常,您使用char*并提取每个字节,然后进行移位和/或累积值。最近的武器装备有装配工协助;如果这是应用程序热路径,则可以使用优化。您的编译器可能支持uu attributepacked;或者诸如此类。事实上,Cortex-M3,尽管您可以为此类访问启用故障生成。OP有一个旧的基于ARM7的芯片。感谢您的快速回复。如何处理此问题,特别是在访问结构件时?谢谢。我按照这条路走,效果很好。但是,这种方法不适用于预定义的结构,如磁盘上的fat引导记录。但我还是设法和他们合作了;正如我在答案末尾提到的,您仍然必须确保您使用的每个myStruct_t*变量都指向一个可以被最大字段大小整除的内存地址。因此,如果您将FAT从磁盘读取到RAM,那么您应该确保RAM中的FAT结构位于一个内存地址,该地址可以被其中最大字段的大小整除。@user3258584解析类似FAT的内容的过程称为序列化。通常,您使用char*并提取每个字节,然后进行移位和/或累积值。最近的武器装备有装配工协助;如果这是应用程序热路径,则可以使用优化。您的编译器可能支持uu attributepacked;或者类似的。可能重复感谢您的快速回复,将我重定向到重复的问题。我的ARM架构是ARM7TDMI,正如大家所指出的,它不支持未对齐的访问。但可能出于同样的原因,即使是结构成员访问也会给出错误的结果。由于这个原因,我无法在SD卡上使用任何像fat这样的现成库。任何提示/解决方案都会有很大帮助。@user3258584尝试禁用旋转功能,并强制cpu创建中止并处理未对齐的访问。编译时是否尝试过-mno unaligned access?谢谢你的建议。我一定会试试这个解决方案。并在此处更新结果。可能重复感谢您的快速回复,将我重定向到重复的问题。我的ARM架构是ARM7TDMI,正如大家所指出的,它不支持未对齐的访问。但可能出于同样的原因,即使是结构成员访问也会给出错误的结果。由于这个原因,我无法在SD卡上使用任何像fat这样的现成库。任何提示/解决方案都会有很大帮助。@user3258584尝试禁用旋转功能,并强制cpu创建中止并处理未对齐的访问。编译时是否尝试过-mno unaligned access?谢谢你的建议。我一定会试试这个解决方案。并在此处更新结果。