结构破坏中的怪异行为

时间:2019-03-15 10:57:39

标签: c++

我正在尝试将结构写入文件并读回。这样做的代码在这里:

#include <fstream>
#include <iostream>
#include <cstring>

using namespace std;

struct info {
  int id;
  string name;
};

int main(void) {
  info adam;
  adam.id = 50;
  adam.name = "adam";

  ofstream file("student_info.dat", ios::binary);
  file.write((char*)&adam, sizeof(info));
  file.close();

  info student;
  ifstream file2("student_info.dat", ios::binary);
  file2.read((char*)&student, sizeof(student));
  cout << "ID =" << student.id << " Name = " << student.name << endl;

  file2.close();
  return 0;
}

但是最后我遇到了奇怪的分割错误。

输出为:

ID =50 Name = adam
Segmentation fault (core dumped)

查看核心转储时,我发现在破坏结构信息时发生了一些奇怪的事情。

(gdb) bt
#0  0x00007f035330595c in ?? ()
#1  0x00000000004014d8 in info::~info() () at binio.cc:7
#2  0x00000000004013c9 in main () at binio.cc:21

我怀疑字符串破坏中发生了一些奇怪的事情,但是我无法找出确切的问题。任何帮助都会很棒。

我正在使用gcc 8.2.0。

3 个答案:

答案 0 :(得分:3)

您不能像这样序列化/反序列化。在此行上:

file2.read((char*)&student, sizeof(student));

您只是在info实例(其中包含std::string)上写1:1。这些不仅仅是字符数组-它们在堆上动态分配其存储并使用指针进行管理。因此,如果您这样覆盖字符串,则该字符串将变为无效,这是未定义的行为,因为其指针不再指向有效位置。

相反,您应该保存实际字符,而不是字符串对象,并在加载时创建一个包含该内容的新字符串。


通常,您可以使用琐碎的对象进行类似的复制。您可以像这样测试它:

std::cout << std::is_trivially_copyable<std::string>::value << '\n';

答案 1 :(得分:3)

要添加到已接受的答案中,因为询问者仍对“为什么在删除第一个对象时崩溃而感到困惑?”:

让我们看一下汇编,因为即使在显示UB的错误程序面前,它也不会说谎(不同于调试器)。

https://godbolt.org/z/pstZu5

(请注意,rsp-我们的堆栈指针-除了在main的开头和结尾处进行调整之外,永远不会更改。)

这是adam的初始化:

    lea     rax, [rsp+24]
    // ...
    mov     QWORD PTR [rsp+16], 0
    mov     QWORD PTR [rsp+8], rax
    mov     BYTE PTR [rsp+24], 0

似乎[rsp+16][rsp+24]拥有字符串的大小和容量,而[rsp+8]拥有指向内部缓冲区的指针。该指针被设置为指向字符串对象本身。

然后adam.name"adam"覆盖:

   call    std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_replace(unsigned long, unsigned long, char const*, unsigned long)

由于优化了较小的字符串,[rsp+8]处的缓冲区指针可能仍指向同一位置(rsp+24),以指示该字符串我们的缓冲区较小且没有内存分配(这是我的猜测)要清楚)。

稍后,我们以相同的方式初始化student

    lea     rax, [rsp+72]
    // ...
    mov     QWORD PTR [rsp+64], 0
    // ...
    mov     QWORD PTR [rsp+56], rax
    mov     BYTE PTR [rsp+72], 0

请注意student的缓冲区指针如何将指向student 以表示一个小的缓冲区。

现在,您将student的内部残酷地替换为adam的内部。突然之间,student的缓冲区指针不再指向预期位置。有问题吗?

    mov     rdi, QWORD PTR [rsp+56]
    lea     rax, [rsp+72]
    cmp     rdi, rax
    je      .L90
    call    operator delete(void*)

是的!如果student的内部缓冲区指向其他地方,而不是我们最初将其设置为(rsp+72)的地方,则它将delete指向该指针。此时,我们不知道adam的缓冲区指针(您复制到student中)的确切指向,但这肯定是错误的地方。如上所述,"adam"可能仍被小型字符串优化所覆盖,因此adam的缓冲区指针可能与之前的位置完全相同:rsp+24。由于我们将其复制到student中,并且与rsp+72不同,因此我们将调用delete(rsp+24)-位于我们自己的堆栈中间。环境并不觉得很有趣,您会在第一个释放位置中遇到段错误(第二个位置甚至不会delete,因为那里的世界仍然很好-adam没有受到您的伤害)。


最重要的是:不要试图超越编译器(“它不能进行段错误,因为它将在同一堆上!”)。你会输的。遵循语言规则,没有人受伤。 ;)

旁注:gcc中的这种设计甚至可能是故意的。我相信他们可以轻松地存储nullptr而不是指向字符串对象来表示一个小的字符串缓冲区。但是在那种情况下,您不会因这种不当行为而遭受打击。

答案 2 :(得分:0)

简要地并从概念上进行思考,当完成adam.name = "adam";时,会在内部为adam.name分配适当的内存。

完成file2.read((char*)&student, sizeof(student));时,您正在存储器位置即地址&student上写,该地址尚未正确分配以容纳正在读取的数据。 student.adam没有分配足够的有效内存。在read对象的位置上进行这样的student处理实际上会导致内存损坏。