C ++不能保证所有类型的对象都占用其连续的存储字节[intro.object] / 5
普通可复制或标准布局类型(3.9)的对象应占用连续的存储字节。
实际上,通过虚拟基类,您可以在主要实现中创建不连续的对象。我试图建立一个示例,其中对象的基类子对象x
位于起始地址之前x
。为了可视化,请考虑以下图形/表格,其中水平轴是地址空间,垂直轴是继承级别(级别1从级别0继承)。标记为的字段dm
被类的直接数据成员占用。
L | 00 08 16
-+ ---------
1 | dm
0 | dm
这是使用继承时通常的内存布局。但是,虚拟基类子对象的位置不是固定的,因为它可以由子类重新定位,这些子类也实际上是从同一基类继承的。这可能会导致以下情况:级别1(基类子对象)报告它始于地址8,并且为16字节大。如果我们天真地将这两个数字相加,即使它实际占用了[0,16),我们也会认为它占用了地址空间[8,24]。
如果我们可以创建这样的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 :: report 0处的报告; 数据偏移16; 天真地抵消48
l2 :: report 0处的报告; 数据在偏移量8处;天真地抵消40
l1 :: report在偏移量32处;数据偏移40; 天真地抵消56
l0 :: report在偏移量24处;数据在偏移量24处; 天真地抵消32
完整的对象占用[0x9f0,0xa20)
从[0xa10,0xa28)复制到[0xa20,0xa38)
注意两个强调的末端偏移量。