【问题标题】:A block of code that is suppose to cause a bus error executes fine假设会导致总线错误的代码块执行得很好
【发布时间】:2023-03-28 00:29:01
【问题描述】:

我目前正在阅读 Expert C Programmign - Deep C Secrets。在第 164 页作者解释 Bus Error 和 Segmentation fault 的地方,他展示了这行代码

union { 
  char a[10];
  int i;
} u ;

int * p = ( int * ) &(u.a[1]);
*p = 17; /* the misaligned addr in p causes a bus error */

上面的代码是假设触发总线错误,但是当我运行它时,它运行良好,没有任何错误。作者给出了以下解释

这会导致总线错误,因为数组/整数联合确保字符数组“a”也位于整数的合理对齐地址,因此“a+1”绝对不是。然后我们尝试将 4 个字节存储到一个仅针对单字节访问对齐的地址中。一个好的编译器会警告未对齐,但它不能发现所有出现的情况。

我对上述语句的理解是,char 是 1 字节,我们试图将 4 字节的 int 放置在 char a[10] 的索引上,因此会发生总线错误(不确定是否我的理解是对是错)

我的问题是为什么上面的代码不会导致总线错误。

注意:我不是 CS 学生,简单的解释会有所帮助。

注意:已经提出了一个与此问题类似的问题,但仅针对上述代码块。

【问题讨论】:

  • “我有这本书说我不能在 30 公里/小时的区域内以 100 公里/小时的速度开车。但是我无论如何都做了并且没有被逮捕。为什么我没有被逮捕?”
  • 如果a 是一个指针(例如char *a;),我可以更真实地看到总线错误和段错误。在任何一种情况下,您都有 i 大约占用可用存储空间的 1/2,但是如果在这种情况下您将 i 更改为有效内存块之外的值 - 总线错误/段错误。
  • 您使用什么架构和操作系统?如果您使用的是英特尔,那么您将不会注意到任何差异(最坏的情况是会慢几纳秒以获得额外的内存获取)。如果您在 linux 上使用 ARM/PowerPC/MIPS 等,那么您可能会发现内核已被配置为执行自动“修复”。查找“/proc/cpu/alignment”以获取有关如何控制内核行为和查看修复计数的详细信息。
  • 在这个问题上有这么多不知情的答案和 cmet...

标签: c segmentation-fault bus-error


【解决方案1】:

我认为这本书是错误的。该代码导致未定义的行为。因此,期望它的任何特定行为是有缺陷的。另请注意,并非所有架构都会导致总线错误。如果这本书没有解释这个事实,那也说明不了什么。

【讨论】:

  • 是的 - 这是可以接受烧书的一种情况。
  • @MartinJames - 这太浪费了。但是人们总是可以使用废纸/卫生纸。
  • 这本书不一定是错的,但可能是不完整的。这在大多数英特尔 CPU 架构(处理硬件问题)上不是问题,但在嵌入式系统中使用的许多 32 位 CPU 架构上是有问题的,例如ARM、MIPS、PowerPC。通常,操作系统内核(例如 linux)可能被配置为捕获故障并执行“修复”。这是非常低效的,但对于大多数用户应用程序来说可能是可以接受的。对于实时应用程序和驱动程序来说,开销是不可接受的,并且 IIRC linux 内核不会修复由内核本身触发的错误。
  • 也许,@Dipstick。不同之处在于代码是否会导致未定义的行为。如果代码导致 UB 并且书表明这确实会导致总线错误,即使隐含假设 CPU 检测到它们,那么这本书肯定是错误的。我不排除交叉阅读和选择性引用使这看起来像是书中的错误。
  • @Ulrich Eckhardt - 这本书出自 1990 年代初期,描述了 Sun SPARC 架构,并确实提到了与内存对齐问题有关的“特别是 RISC 架构”。由于年代久远,这本书需要烧掉,但这些问题仍然非常重要。还要说行为未定义并没有真正的帮助;通常根据编译的体系结构使用不同的代码。 linux内核为此使用#define CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
【解决方案2】:

我对上述语句的理解是,char 是 1 字节,我们试图在 char a[10] 的索引上放置一个 4 字节的 int,因此会发生总线错误(不确定我的理解是否是对是错)

问题不在于charint 的大小,而在于它们的对齐方式。通常,体系结构对加载数据/代码的地址很挑剔,例如,您可能只能从 16 位的倍数地址加载 16 位整数,或者函数必须始终以 4 字节开始边界。

如果您不注意这一点,处理器可能会读取错误数据或以异常惩罚您。然后操作系统可以使用多个对齐的访问来模拟它,或者将其作为SIGBUS 传递给用户应用程序。后者可能是作者在他的设置中所经历的。

这一切与 C 有什么关系?

您所拥有的是未定义的行为。您附近的处理器、内存控制器、编译器、操作系统和鼻恶魔产卵之间的相互作用都会影响它的影响(如果有的话)。在您的计算机上,处理器本身可能支持未对齐的访问,因此它可以工作,但您仍然不能依赖它:它只是 undefined。 (尤其是优化,这些东西可能会反过来咬你。Works for me™ 不足以编写定义良好的 C 代码!)

【讨论】:

    【解决方案3】:

    您尝试获取的数据跨越 32 位边界,因此需要 2 次内存获取(但编译器在编译时不一定知道这一点)。

    注意:这本书很老了,讲的是 32 位 CPU。对于 64 位,您可能必须将 int * p = ( int * ) &(u.a[1]); 更改为 int * p = ( int * ) &(u.a[5]);,以便无法在一次内存中从对齐的地址获取所需的所有数据。

    出于向后兼容的原因,大多数 Intel CPU(以及 AMD 等衍生产品)会在指令级别自动处理内存对齐错误,因此您不会注意到任何错误(简单来说,它会自动添加额外的读取并锁定总线以确保没有在读取之间更改)。

    在许多其他 CPU 架构(ARM、PowerPC、MIPS、较新的 Intel 架构)上,示例代码可能会导致上述问题,但现在某些操作系统(例如 Linux)可以配置为自动捕获故障并执行“ fixup”允许程序在不知道有问题的情况下继续运行。对于大多数程序,这可能不会被用户注意到,但非常耗时,并且会在实时软件和驱动程序中引起真正的问题。

    时间关键代码通常根据编译时的 CPU 架构有条件地编译为执行或不执行未对齐访问。在 linux 中,伪文件“/proc/cpu/alignment”可用于控制内核行为并查看有关“修复”数量的统计信息。

    【讨论】:

    • 请注意,在 ARM 上,支持未对齐的负载实际上是可选的,据我所知,ARMv8 实际上要求支持未对齐的负载。
    猜你喜欢
    • 1970-01-01
    • 2021-04-06
    • 1970-01-01
    • 2011-05-07
    • 2020-01-27
    • 1970-01-01
    • 1970-01-01
    • 2021-12-17
    • 2021-11-15
    相关资源
    最近更新 更多