使用inplace new运算符堆栈解开clobbering内存

时间:2014-10-29 06:11:42

标签: c++ c++11 vector

我有一个非常令人讨厌的错误,一直困扰着我。这是情况,我正在创建一个内存文件系统。我为每个要执行读写操作的文件预先分配了数据块。为了实现目录,我有一个简单的std :: vector对象,其中包含目录中的所有文件。此向量对象位于每个目录中文件的顶部。因此,为了从目录读取和写入,我将前16个字节读入字符缓冲区并键入将其转换为向量(16个字节,因为我的系统上sizeof(vector<T>)为16)。具体来说,前16个字节不是向量的元素,而是向量本身。但是,在退出键功能后,矢量以某种方式被破坏了。

以下代码不会引发异常,并且可以正确地将向量保存到字符缓冲区以便稍后检索。

#include <vector>
char dblock[16];

typedef std::vector<int> Entries;
void foo() {
    char buf[sizeof(Entries)];
    Entries* test = new (buf)Entries();
    test->push_back(0);
    for (int i = 0; i < sizeof(std::vector<int>); ++i) {
        dblock[i] = buf[i];
    }
}

void bar() {
    char buf[sizeof(Entries)];
    for (int i = 0; i < sizeof(std::vector<int>); ++i) {
        buf[i] = dblock[i];
    }
    Entries* test = (Entries*)buf;
    test->back();
}

int main()
{
    foo();
    bar();
    return 0;
}

但是,一旦将函数foo更改为包含这样的参数,只要我尝试使用迭代器,就会抛出异常。

void foo(int this_arg_breaks_everything) {
    char buf[sizeof(Entries)];
    Entries* test = new (buf)Entries();
    test->push_back(0);
    for (int i = 0; i < sizeof(std::vector<int>); ++i) {
        dblock[i] = buf[i];
    }
}

看看反汇编,当函数拆除它的帧堆栈时,我发现了问题汇编:

add         esp,128h  <----- After stack is reduced, vector goes to an unusable state.
cmp         ebp,esp  
call        __RTC_CheckEsp (0D912BCh)  
mov         esp,ebp  
pop         ebp  
ret

我通过使用调试器测试if((Entries *)dblock) - &gt; back()是否返回异常来找到此代码。具体而言,例外是“访问冲突读取位置0XCCCCCCCC”。有问题的位置是std :: vector的std :: _ Container_base12 :: _ Myproxy-&gt; _Mycont-&gt; _Myproxy == 0XCCCCCCCC;你可以在xutility的第165行找到它。

是的,只有在使用inplace new运算符时才会发生错误。使用普通的new运算符然后将测试写入dblock不会引发异常。

因此,我得出的结论是,这种情况的最终原因是某种程度上编译器正在做一个糟糕的堆栈展开,破坏它不应该存在的某些内存部分。

编辑:为了清楚起见改变了措辞。 Edit2:解释幻数。

4 个答案:

答案 0 :(得分:2)

在visual studio 2013中,这会产生错误,在看了向量的内部数据后,很容易找出原因。 Vector分配了一个内部对象,它执行了很多繁重的工作,这个内部对象又将指针存储回向量,从而知道向量应该是的位置。当向量的内存从一个位置移动到另一个位置时,它占用的内存会发生变化,因此内部对象现在转而指向后来被调试代码擦除的内存。

看看你的代码,这看起来完全一样。 std :: _ Container_base12是向量使用的基类,它有一个名为myProxy的成员。 myProxy是一个内部对象,它执行繁重的工作,并且指针指向包含它的向量。当您移动向量时,您无法更新此指针,因此当您使用移动的向量数据时,它会尝试使用仍在尝试引用回原始向量位置的myProxy。因为擦除了该数据区域,所以它会在其中查找指针,而是找到“CCCCCCCC”&#39;这是调试代码对擦除的内存数据的作用。它试图访问该内存位置,一切都爆炸。

答案 1 :(得分:1)

你正在做的事情(将一个等同于memcpy的不透明C ++对象序列化到本地缓冲区)不会可持续地工作,因为vector对象是具有大量指向堆内存的指针的深层对象。不过要回答你的问题,为什么你会崩溃。

问题是对齐。当你尝试做

char buf[sizeof(Entries)];
Entries* test = new (buf)Entries();

这假定buf具有对象Entries的正确对齐方式。我不会声称知道vector的内部结构,但我打赌它看起来像

class vector
{
  T* start;
  T* end;
  other stuff
}

即。它是一堆堆指针。指针需要寄存器对齐,这在64位机器上是8个字节。通过N个字节对齐意味着您可以将地址均匀地除以N.但是,您在堆栈上分配buf,这不保证有任何对齐,但可能意外地具有8字节对齐,因为它是唯一的在您的本地堆栈框架上。但是,如果您向foo声明一个参数,并且该参数是一个4字节的int,那么buf不再是8字节对齐,因为您只是增加了4个字节。然后,当您尝试访问未对齐的指针时,您会崩溃。

作为实验,请尝试将foo更改为

void foo(int unused1, int unused2) {

这可能会意外重新对齐buf,并且可能不会崩溃。但是,停止你正在做的事情并且不要这样做。

有关详细信息,请参阅http://en.wikipedia.org/wiki/Data_structure_alignment 这是关于序列化的指导:http://www.parashift.com/c++-faq/serialization.html。您可以考虑一个可序列化的Boost向量类。

答案 2 :(得分:0)

为什么不写下面的内容?

std::vector<int> dblock;

void foo() {
    dblock.push_back(0);
}

void bar() {
    dblock.back();
}

答案 3 :(得分:0)

最简单的答案:未定义的行为。

std::vector不是trivially copyable,您不能memcpy从一个地方到另一个地方。

您的代码的另一个问题是dblock可能与std::vector的对齐方式不同。这可能会导致某些处理器崩溃。

第三个问题是编译器有时会在将buff复制到dblock时返回垃圾。 这是因为你打破了strict aliasing rule