C++并不保证所有类型的对象都占用连续的存储空间[intro.object]/5
平凡可复制或标准布局类型(3.9)的对象应占用连续的存储字节。
事实上,通过虚基类,您可以在主要实现中创建非连续对象。我尝试构建一个示例,其中对象 x
的基类子对象位于x
的起始地址之前。为了形象化这一点,请考虑以下图表,其中水平轴是地址空间,垂直轴是继承级别(级别1继承自级别0)。由dm
标记的字段被类的直接数据成员占用。
L | 00 08 16
--+---------
1 | dm
0 | dm
当使用继承时,这是一个常见的内存布局。然而,虚基类子对象的位置不是固定的,因为它可以被从同一基类虚拟继承的子类重新定位。这可能导致级别1(基类子)对象报告它从地址8开始,并且大小为16字节。如果我们简单地将这两个数字相加,我们会认为它占用地址空间[8,24),即使它实际上占用的是[0,16)。
如果我们可以创建这样一个级别1对象,那么我们就不能使用memcpy
来复制它:memcpy
会访问不属于该对象的内存(地址16到24)。在我的演示中,clang++的地址检查器将其捕获为堆栈缓冲区溢出。
如何构造这样一个对象?通过使用多重虚拟继承,我想出了一个具有以下内存布局的对象(虚表指针标记为vp
)。它由四层继承组成:
L 00 08 16 24 32 40 48
3 dm
2 vp dm
1 vp dm
0 dm
上述问题将出现在级别1基类子对象中。它的起始地址为32,大小为24字节(vptr、它自己的数据成员和级别0的数据成员)。
以下是在clang++和g++ @ coliru下进行此类内存布局的代码:
struct l0 {
std::int64_t dummy;
};
struct l1 : virtual l0 {
std::int64_t dummy;
};
struct l2 : virtual l0, virtual l1 {
std::int64_t dummy;
};
struct l3 : l2, virtual l1 {
std::int64_t dummy;
};
我们可以通过以下方式产生堆栈缓冲区溢出:
l3 o;
l1& so = o;
l1 t;
std::memcpy(&t, &so, sizeof(t));
下面是一个完整的演示,同时打印出有关内存布局的一些信息:
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#define PRINT_LOCATION() \
std::cout << std::setw(22) << __PRETTY_FUNCTION__ \
<< " at offset " << std::setw(2) \
<< (reinterpret_cast<char const*>(this) - addr) \
<< " ; data is at offset " << std::setw(2) \
<< (reinterpret_cast<char const*>(&dummy) - addr) \
<< " ; naively to offset " \
<< (reinterpret_cast<char const*>(this) - addr + sizeof(*this)) \
<< "\n"
struct l0 {
std::int64_t dummy;
void report(char const* addr) { PRINT_LOCATION(); }
};
struct l1 : virtual l0 {
std::int64_t dummy;
void report(char const* addr) { PRINT_LOCATION(); l0::report(addr); }
};
struct l2 : virtual l0, virtual l1 {
std::int64_t dummy;
void report(char const* addr) { PRINT_LOCATION(); l1::report(addr); }
};
struct l3 : l2, virtual l1 {
std::int64_t dummy;
void report(char const* addr) { PRINT_LOCATION(); l2::report(addr); }
};
void print_range(void const* b, std::size_t sz)
{
std::cout << "[" << (void const*)b << ", "
<< (void*)(reinterpret_cast<char const*>(b) + sz) << ")";
}
void my_memcpy(void* dst, void const* src, std::size_t sz)
{
std::cout << "copying from ";
print_range(src, sz);
std::cout << " to ";
print_range(dst, sz);
std::cout << "\n";
}
int main()
{
l3 o{};
o.report(reinterpret_cast<char const*>(&o));
std::cout << "the complete object occupies ";
print_range(&o, sizeof(o));
std::cout << "\n";
l1& so = o;
l1 t;
my_memcpy(&t, &so, sizeof(t));
}
演示现场
样本输出(为避免垂直滚动而缩写):
l3::报告位于偏移量 0; 数据位于偏移量 16; 简单地到达偏移量 48
l2::报告位于偏移量 0; 数据位于偏移量 8; 简单地到达偏移量 40
l1::报告位于偏移量 32; 数据位于偏移量 40; 简单地到达偏移量 56
l0::报告位于偏移量 24; 数据位于偏移量 24; 简单地到达偏移量 32
完整对象占用 [0x9f0,0xa20)
从 [0xa10,0xa28) 复制到 [0xa20,0xa38)
请注意两个强调的结束偏移量。
T
,如果两个指向不同T
对象obj1
和obj2
的指针,其中obj1
和obj2
都不是基类子对象,如果组成obj1
的底层字节被复制到obj2
中,则obj2
随后将持有与obj1
相同的值”。(强调我的)下面的示例使用了std::memcpy
。 - Mooing Duck