结构破坏中的奇怪行为
Weird behavior in struct destruction
我正在尝试将结构写入文件并读回。这样做的代码在这里:
#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.
你不能这样serialize/deserialize。在这条线上:
file2.read((char*)&student, sizeof(student));
您只是在 info
的实例上写入 1:1,其中包括 std::string
。这些不仅仅是字符数组——它们在堆上动态分配存储空间并使用指针进行管理。因此,如果您这样覆盖它,该字符串将变得无效,这是未定义的行为,因为它的指针不再指向有效位置。
相反,您应该保存实际的字符,而不是字符串对象,并在加载时创建一个包含该内容的新字符串。
一般情况下,您可以对琐碎的对象进行这样的复制。你可以这样测试:
std::cout << std::is_trivially_copyable<std::string>::value << '\n';
简单地从概念上思考,当adam.name = "adam";
完成后,在内部为adam.name
分配了适当的内存。
当 file2.read((char*)&student, sizeof(student));
完成后,您正在写入内存位置,即地址 &student
尚未正确分配以容纳正在读取的数据。 student.adam
没有分配足够的有效内存。将 read
放入 student
对象的位置实际上会导致内存损坏。
添加到已接受的答案中,因为提问者仍然对"why does it crash on the deletion of the first object?"感到困惑:
让我们看看反汇编,因为它不能说谎,即使面对展示 UB 的错误程序(与调试器不同)。
(请注意,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
没有被你伤害)。
底线:不要试图智取编译器 ("it can't segfault because it'll be on the same heap!")。你会输的。遵守语言规则,没有人会受到伤害。 ;)
旁注:gcc
中的这种设计甚至可能是故意的。我相信他们可以很容易地存储一个 nullptr
而不是指向字符串对象来表示一个小的字符串缓冲区。但在那种情况下,你不会因为这种不当行为而出现段错误。
我正在尝试将结构写入文件并读回。这样做的代码在这里:
#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.
你不能这样serialize/deserialize。在这条线上:
file2.read((char*)&student, sizeof(student));
您只是在 info
的实例上写入 1:1,其中包括 std::string
。这些不仅仅是字符数组——它们在堆上动态分配存储空间并使用指针进行管理。因此,如果您这样覆盖它,该字符串将变得无效,这是未定义的行为,因为它的指针不再指向有效位置。
相反,您应该保存实际的字符,而不是字符串对象,并在加载时创建一个包含该内容的新字符串。
一般情况下,您可以对琐碎的对象进行这样的复制。你可以这样测试:
std::cout << std::is_trivially_copyable<std::string>::value << '\n';
简单地从概念上思考,当adam.name = "adam";
完成后,在内部为adam.name
分配了适当的内存。
当 file2.read((char*)&student, sizeof(student));
完成后,您正在写入内存位置,即地址 &student
尚未正确分配以容纳正在读取的数据。 student.adam
没有分配足够的有效内存。将 read
放入 student
对象的位置实际上会导致内存损坏。
添加到已接受的答案中,因为提问者仍然对"why does it crash on the deletion of the first object?"感到困惑:
让我们看看反汇编,因为它不能说谎,即使面对展示 UB 的错误程序(与调试器不同)。
(请注意,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
没有被你伤害)。
底线:不要试图智取编译器 ("it can't segfault because it'll be on the same heap!")。你会输的。遵守语言规则,没有人会受到伤害。 ;)
旁注:gcc
中的这种设计甚至可能是故意的。我相信他们可以很容易地存储一个 nullptr
而不是指向字符串对象来表示一个小的字符串缓冲区。但在那种情况下,你不会因为这种不当行为而出现段错误。