结构破坏中的奇怪行为

Weird behavior in struct destruction

本文关键字:结构      更新时间:2023-10-16

我正在尝试将结构写入文件并将其读回。执行此操作的代码如下:

#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。

你不能像那样序列化/反序列化。在此行中:

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

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

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


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

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

为了补充接受的答案,因为提问者仍然对"为什么它在删除第一个对象时崩溃?"感到困惑:

让我们看一下双汇编,因为它不能撒谎,即使面对显示 UB 的错误程序(与调试器不同)。

https://godbolt.org/z/pstZu5

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

以下是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以表示小缓冲区。

现在你残酷地用adam的内部替换student的内部.突然,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,而不是指向字符串对象来表示一个小的字符串缓冲区。但在这种情况下,你不会从这种渎职行为中分离出来。

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

完成后file2.read((char*)&student, sizeof(student));,您将在内存位置写入,即地址&student尚未正确分配以容纳正在读取的数据。student.adam没有足够的有效内存分配给它。在对对象的位置执行此类readstudent实际上会导致内存损坏。