我目前正在阅读《专家C编程-深度C秘密》。在第164页,作者在解释总线错误和分段故障时,展示了这行代码
union {
char a[10];
int i;
} u ;
int * p = ( int * ) &(u.a[1]);
*p = 17; /* the misaligned addr in p causes a bus error */
上面的代码假设会触发一个总线错误,但当我运行它时,它运行得很好,没有任何错误。作者给出了以下解释
这会导致总线错误,因为array/int并集确保字符数组"a"也位于整数的合理对齐地址,因此"a+1"绝对不是。然后,我们尝试将4个字节存储到一个仅用于单字节访问的地址中。一个好的编译器会对错位发出警告,但它不能发现所有的错误。
我对上述语句的理解是,char
是1字节,我们正试图将4字节的int
放在char a[10]
的索引上,因此将发生总线错误(不确定我的理解是对是错)
我的问题是为什么上面的代码没有导致总线错误。
注意:我不是CS学生,简单的解释会有所帮助。
注意:已经提出了一个与此问题类似的问题,但仅针对上述代码块。
我认为这本书是错的。该代码导致未定义的行为。因此,期望它有任何特定的行为是有缺陷的。还要注意,并非所有体系结构都会导致总线错误。如果这本书没有解释这一事实,那也不能说明问题。
我对上述语句的理解是,char是1字节,我们试图在char a[10]的索引上放置一个4字节的int,因此将发生总线错误(不确定我的理解是对是错)
问题不在于char
或int
的大小,而在于它们的对齐。通常,体系结构对加载数据/代码的地址很挑剔,例如,您可能只能从16位的倍数地址加载16位整数,或者函数必须始终从4字节边界开始。
如果你没有注意到这一点,处理器可能会读取错误的数据,或者用异常来惩罚你。然后,操作系统可以使用多个对齐的访问来模拟它,或者将它作为SIGBUS
传递给用户应用程序。后者可能是作者在他的设置中所经历的。
这一切与C有什么关系
你所拥有的是未定义的行为。处理器、内存控制器、编译器、操作系统和你附近的鼻恶魔的交互都会影响它的效果(如果有的话)。在您的计算机上,处理器可能本机支持未对齐的访问,因此它有效,但它仍然是您不能依赖的:它只是未定义。(尤其是在优化的情况下,这些东西可能会反噬你。对我有用™不足以编写定义良好的C代码!)
您试图获取的数据跨越32位边界,因此需要2次内存获取(但编译器在编译时不一定知道这一点)。
注意:这本书很老,讲的是32位CPU。对于64位,您可能需要将int * p = ( int * ) &(u.a[1]);
更改为int * p = ( int * ) &(u.a[5]);
,这样就无法在一次内存提取中从对齐的地址获得所需的所有数据。
出于向后兼容性的原因,大多数英特尔CPU(以及AMD等衍生产品)都会在指令级别自动处理内存对齐错误,这样你就不会注意到任何错误(简单地说,它会在锁定总线后自动添加额外的读取,以确保读取之间没有任何变化)。
在许多其他CPU体系结构(ARM、PowerPC、MIPS、更新的Intel体系结构)上,示例代码会导致所述的问题,但现在一些操作系统,如Linux,可以配置为自动捕捉故障并进行"修复",使程序在不存在问题的情况下继续运行。对于大多数程序来说,这可能会被用户忽视,但相当耗时,并且会在实时软件和驱动程序中造成实际问题。
时间关键型代码通常是有条件地编译的,以根据编译的CPU架构进行或不进行未对齐的访问。在linux中,伪文件"/proc/cpu/alignment"可以用于控制内核行为并查看有关"修复"数量的统计信息。