【问题标题】:Bus error with allocated memory on a heap在堆上分配内存的总线错误
【发布时间】:2017-03-06 16:35:33
【问题描述】:

我在这样的代码中有总线错误

char* mem_original;
int int_var = 987411;
mem_original = new char [250];
memcpy(&mem_original[250-sizeof(int)], &int_var, sizeof(int));
...
const unsigned char* mem_u_const = (unsigned char*)mem_original;
...
const unsigned char *location = mem_u_const + 250 - sizeof(int);

std::cout << "sizeof(int) = " << sizeof(int) << std::endl;//it's printed out as 4
std::cout << "byte 0 = " << int(*location) << std::endl;
std::cout << "byte 1 = " << int(*(location+1)) << std::endl;
std::cout << "byte 2 = " << int(*(location+2)) << std::endl;
std::cout << "byte 3 = " << int(*(location+3)) << std::endl;
int original_var = *((const int*)location);
std::cout << "original_var = " << original_var << std::endl;

几次效果都很好,打印出来:

sizeof(int) = 4
byte 0 = 0
byte 1 = 15
byte 2 = 17
byte 3 = 19
original_var = 987411

然后它失败了:

sizeof(int) = 4
byte 0 = 0
byte 1 = 15
byte 2 = 17
byte 3 = 19
Bus Error

它在 Solaris OS (C++ 5.12) 上构建和运行 Linux (gcc 4.12) 和 Windows (msvc-9.0) 上的相同代码运行良好。

我们可以看到:

  1. new[] 在堆上分配了内存。
  2. 内存可访问(我们可以逐字节读取)
  3. 内存完全包含应有的内容,没有损坏。

那么总线错误的原因可能是什么?我应该去哪里看?

统一更新: 如果我最后 memcpy(...) locationoriginal_var,它的工作原理。但是*((const int*)location) 的问题是什么?

【问题讨论】:

  • 除了我的答案,正确的地方是在调试器中运行程序,看看哪一行代码使程序崩溃,以及导致总线错误的指针包含什么。然后,您可以再次运行并单步执行以查看设置的位置。
  • @Davislor,不幸的是,这种情况并非一直都在发生。它可能会在 Bus Error 之前进行多次迭代,并且仅在 Solaris 上显示,因此很难调试。
  • 另一种调试技术是添加运行时检查。定义一个内联函数,如:template &lt;type T&gt; inline bool is_aligned( const T* p ) { return (uintptr_t)(void*)(p) % alignof(T) == 0; }。 (如果您没有 C++11,只需跳过模板并使用常量 4 而不是 alignof(T)。)然后,每当您进行指针数学运算和转换时,您可以 assert 将指针 is_aligned .如果没有,您将立即使程序崩溃,并诊断出错误发生的位置。
  • 哎呀,写了type 我的意思是classtypename

标签: c++ solaris bus-error


【解决方案1】:

对于在具有对齐限制(例如 SPARC)的硬件方面没有经验的开发人员来说,这是一个常见问题。 x86 硬件非常可以容忍未对齐的访问,尽管会影响性能。其他类型的硬件? SIGBUS.

这行代码:

int original_var = *((const int*)location);

调用未定义的行为。您正在接受unsigned char * 并将其指向的内容解释为int。你不能安全地做到这一点。时期。这是未定义的行为 - 正是您遇到的原因。

您违反了严格的别名规则。请参阅What is the strict aliasing rule? 简而言之,您不能将一种类型的对象称为另一种类型。 char * 没有也不能引用 int

Oracle 的 Solaris Studio 编译器实际上提供了一个命令行参数,可以让您摆脱在 SPARC 硬件上的问题 - -xmemalign=1i(请参阅 https://docs.oracle.com/cd/E19205-01/819-5265/bjavc/index.html)。尽管对 GCC 公平,如果没有该选项,您在代码中执行的强制操作仍将在 Studio 编译器下 SIGBUS

或者,正如您已经指出的那样,您可以使用memcpy() 来复制字节,无论它们是什么 - 只要您知道源对象可以安全地复制到目标对象中 - 是的,有些情况当那为真时。

【讨论】:

  • @BenVoigt 你确定吗?他们对SIGBUS 免疫?只有其中一个可以正确地成为 SPARC 硬件上int 的地址。我在 SPARC 上使用 GCC 的经验(诚然是不久前......)是他们会获得 SIGBUS,但这可能是一个旧的 GCC 错误,应该是安全代码'不违反严格的别名。
  • @BenVoigt 是的,明白了。我误读了它——可能被我多年前的经历所影响。我会解决的。
  • 谢谢!但是假设我想使用已经在内存中的现有 POD 结构 (char*)。现在我不能只创建指向该结构的指针并将char* 转换为指向结构的指针。我可以创建指针联合吗?像union { mytype* myptr_; char* charptr_; };,然后不用强制转换,直接给charptr_分配内存,然后使用myptr_
  • @Arkady 我可以创建指针联合吗?就像union { mytype* myptr_; char* charptr_; }; 不,你不能那样做。您不能别名的不是 pointer 值,而是指针引用的具有固定类型的对象。 [unsigned] char 数组的任意偏移量不能被视为int。您可以通过memcpy()该地址的数据复制到int 中(除非它包含trap representation),但无论如何您都不能直接将其视为int对引用对象的指针执行操作。
  • @Arkady 如果您真的想在 Solaris/SPARC 上执行此操作,可以使用 Oracle 的编译器 (oracle.com/technetwork/server-storage/developerstudio/downloads/…) 代替 GCC,并尝试使用 -xmemalign=1i编译器的命令行选项。您可能会招致显着的性能下降。根据我的经验,它会起作用。请注意,您将未对齐的 int 值传递给的任何 GCC 编译代码仍将失败并返回 SIGBUS
【解决方案2】:

我在编译您的代码时收到以下警告:

main.cpp:19:26: warning: cast from 'const unsigned char *' to 'const int *' increases required alignment from 1 to 4 [-Wcast-align]
    int original_var = *((const int*)location);
                         ^~~~~~~~~~~~~~~~~~~~

这似乎是总线错误的原因,因为improperly aligned access can cause a bus error

【讨论】:

  • 即使我知道连续有 4 个字节?你想说,这样的*((const int*)location 代码从语言的角度来看是不正确的,即使开发人员知道location 指向足够的字节数?
  • 除了连续 4 个字节之外,内存访问必须正确对齐 int。正如链接答案中所指出的,执行访问的机器指令需要这样做。 C++ 也要求这样做。
  • 如果我知道有int* 的数组,你不知道我怎样才能从char* 得到int*
  • @Arkady See stackoverflow.com/questions/98650/… 当你强制 char 地址引用 int 时,你调用了未定义的行为。
  • @Arkady:请参阅stackoverflow.com/a/3903577/103167,了解为什么连续 4 个字节不够好。
【解决方案3】:

虽然我现在无法访问 SPARC 来测试这个,但根据我在那个平台上的经验,我很确定这条线是你的问题:

const unsigned char *location = mem_u_const + 250 - sizeof(int);

mem_u_const 块最初由new 分配用于字符数组。由于 sizeof(unsigned char) 是 1 而 sizeof(int) 是 4,因此您要添加 246 个字节。这不是 4 的倍数。

在 SPARC 上,CPU 只能读取与 4 字节边界对齐的 4 字节字。您尝试读取未对齐的单词是导致总线错误的原因。

我建议分配一个 struct 和一个 unsigned char 数组,后跟一个 int,而不是像导致此错误的那样使用一堆指针数学和强制转换。

【讨论】:

  • 你想说 mem_u_const + 240 可以毫无问题地读取,因为 240 是 4 的倍数?
  • 我不认为 new 在分配 char 对象时保证返回一个 4 字节对齐的地址,如果可移植性是一个问题,你不希望你的当您将其重新编译为 64 位代码或在其他体系结构上时要中断的代码。您应该分配一个struct,而不是分配一个字节块。如果内存块需要以几种不同的方式解释,请分配unionstructs。更易读、更优雅、更便携、更安全。
  • 然而,如果你真的不想分配一个结构体,你可以分配一个max_align_t的数组。其中一个的存储保证正确对齐,以用作指向任何标量类型的指针。或者你可以使用alignas
  • 感谢您的帮助和回答,如果我有两个“接受”第二个将是您的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-10
相关资源
最近更新 更多