首页 > 解决方案 > 在什么情况下 MSVC C++ 编译器有时会在函数 operator new[] 返回的指针之前直接写入数组大小?

问题描述

我目前正在开发一个内存跟踪器,我们正在重载函数 operator new[],它有很多变体。在编写一些单元测试时,我偶然发现了一个事实,即 MSVC C++ 2019(使用 ISO C++ 17 标准(std:c++17)编译器设置)在指针返回到来电者,但只是有时。我一直找不到任何记录在案的情况下会发生这种情况。谁能解释一下这些条件是什么,我如何在运行时检测到它们,或者指向我的任何文档?

为了确定这是否发生,我不得不反汇编代码。这是C++:

const size_t k_NumFoos = 6;
Foo* pFoo = new Foo[k_NumFoos];

这是反汇编:

00007FF747BB3683  call        operator new[] (07FF747A00946h)  
00007FF747BB3688  mov         qword ptr [rbp+19E8h],rax  
00007FF747BB368F  cmp         qword ptr [rbp+19E8h],0  
00007FF747BB3697  je          ____C_A_T_C_H____T_E_S_T____0+0FF7h (07FF747BB36F7h)  
00007FF747BB3699  mov         rax,qword ptr [rbp+19E8h]  
00007FF747BB36A0  mov         qword ptr [rax],6  
00007FF747BB36A7  mov         rax,qword ptr [rbp+19E8h]  
00007FF747BB36AE  add         rax,8  
00007FF747BB36B2  mov         qword ptr [rbp+1B58h],rax  

cmpje行来自我们用于单元测试的 Catch2 库。后面的两个movsje是它写入数组大小的地方。接下来的三行 ( mov, add, mov) 是将指针移动到写入数组大小的位置之后的位置。大多数情况下,这一切都很好。

我们还使用 MSVirtualAlloc作为重载函数 operator new[] 内部的分配器。从返回的地址VirtualAlloc必须与使用 的函数 operator new[]std::align_t对齐,当对齐大于默认的最大对齐时,在最后三行反汇编中指针的移动会与返回的对齐地址混淆。最初,我认为使用函数 operator new[] 进行的所有分配都会有这种行为。因此,我测试了函数运算符 new[] 的其他一些用法,并发现在我测试的所有情况下它都是正确的。我编写了代码来调整这种行为,然后遇到了一种情况,它没有表现出在返回分配之前写入数组大小的行为。

这是在返回分配之前未写入数组大小的 C++:

char **utf8Argv = new char *[ argc ];

argc等于 1。该行来自Session::applyCommandLineCatch2 库中的方法。反汇编看起来像这样:

00007FF73E189C6A  call        operator new[] (07FF73E07D6D8h)  
00007FF73E189C6F  mov         qword ptr [rbp+168h],rax  
00007FF73E189C76  mov         rax,qword ptr [rbp+168h]  
00007FF73E189C7D  mov         qword ptr [utf8Argv],rax  

注意在callto之后operator new[] (07FF73E07D6F8h)没有写入数组大小。在查看两者的差异时,我可以看到一个写入指针,而另一个写入指向指针的指针。但是,据我所知,在运行时,这些信息都不能在内部用于函数 operator new[]。

这里的代码来自一个 Debug | x64 构建。关于如何确定这种行为何时发生的任何想法?

更新(对于下面的 convo): Foo 类:

template<size_t ArrLen>
class TFoo
{
public:
    TFoo()
    {
        memset(m_bar, 0, ArrLen);
    }
    TFoo(const TFoo<ArrLen>& other)
    {
        strncpy_s(m_bar, other.m_bar, ArrLen);
    }
    TFoo(TFoo<ArrLen>&& victim)
    {
        strncpy_s(m_bar, victim.m_bar, ArrLen);
    }
    ~TFoo()
    {
    }
    TFoo<ArrLen>& operator= (const TFoo<ArrLen>& other)
    {
        strncpy_s(m_bar, other.m_bar, ArrLen);
    }
    TFoo<ArrLen>& operator= (TFoo<ArrLen>&& victim)
    {
        strncpy_s(m_bar, victim.m_bar, ArrLen);
    }

    const char* GetBar()
    {
        return m_bar;
    }
    void SetBar(const char bar[ArrLen])
    {
        strncpy_s(m_bar, bar, ArrLen);
    }

protected:
    char m_bar[ArrLen];
};
using Foo = TFoo<8>;

标签: c++arraysmemoryvisual-c++disassembly

解决方案


猜测一下,我认为编译器在分配具有析构函数的对象时,会在指针返回给您之前写出分配的对象数,当您调用delete []. 在这种情况下,编译器必须发出代码来销毁您调用时分配的每个对象delete [],为此,它需要知道数组中存在多少对象。

OTOH,对于类似的东西char *,不需要计数,因此,作为一个小的优化,没有发出任何东西,或者看起来如此。

我认为您不会在任何地方找到此文档,并且该行为可能会在编译器的未来版本中发生变化。它似乎不是标准的一部分。


推荐阅读