我遇到了指针运算的奇怪行为。我正在开发一个使用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;但没有变化。
我可以想到小/大端,但在这里我得到了意外的数据(前一个字节)而没有顺序倒转。
答案 0 :(得分:2)
您的CPU架构必须支持未对齐的加载和存储操作。
据我所知,它没有(我一直在使用STM32,这是一个基于ARM的皮质)。
如果您尝试从地址中读取uint32_t
值,该地址不能被uint32_t
整除(即不能被4整除),那么在“好”的情况下,您将获得错误的输出。
我不确定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)。
您需要遵循两个基本规则:
您的结构的每个实例都必须位于一个内存地址,该地址可以被结构中最大字段的大小整除。
结构中的每个字段必须位于偏移量(在结构内),该偏移量可以被该字段本身的大小整除。
例外:
如果CPU架构支持未对齐的加载和存储操作,则可能违反规则#1。然而,这样的操作通常效率较低(要求编译器在“之间”添加NOP)。理想情况下,即使编译器支持未对齐操作,也应该努力遵循规则#1 ,并让编译器知道数据已经完全对齐(使用专用#pragma
),按顺序允许编译器在可能的情况下使用对齐的操作。
如果编译器自动生成所需的填充,则可能违反规则#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位字节的机器。)