在OSX + llvm/libc++上内存损坏/结构重排序

Memory corruption/structure reordering on OSX + llvm/libc++

本文关键字:损坏 结构 排序 内存 libc++ OSX llvm      更新时间:2023-10-16

在最终得到一个跨平台项目来编译后,我在OSX上出现了最奇怪的错误。程序以不同的方式崩溃(但有时可能存活下来以显示其ui)。逐步通过XCode调试器,我看到多个地方的子对象的值根据上下文改变。这就是我遇到的问题:

class third
{
public:
    int some_data;
    void do_something()
    {
    }
    
};
class second
{
public:
    third * thirdPtr;
    
    second()
        : thirdPtr(nullptr)
    {
        thirdPtr = new third();
        printf("second = 0x%X, third = 0x%X", this, thirdPtr);
    }
    
};

class first
{
    second * secondInstance;
    first()
        : secondInstance(nullptr)
    {
        secondInstance = new second();
        printf("second = 0x%X, third = 0x%X", secondInstance, secondInstance->thirdPtr);
        // (maybe) crash
        secondInstance->thirdPtr->do_something();
        
    }
    
};

在逐步执行时,我向第三个指针添加了一个观察点。这是一个示例输出:

Watchpoint 1 hit:
old value: 0x00000001
new value: 0x00000000
Watchpoint 1 hit:
old value: 0x00000000
new value: 0x04821c34

程序的stdout:

second = 0x4821C20, third = 0x4821C34
second = 0x4821C20, third = 0x3404821C

我从来没有见过这样的事情。显然,这些结构体更复杂,并且使用继承,但在初始化时,没有其他任何东西可以访问或写入这些结构体。我的源代码中有很多奇怪的小故障和问题,所以我认为这个小故障是多个地方的问题。请注意,源代码在其他平台上已经完美运行了很长时间,即使在外部库代码中也会出现错误(我使用JUCE进行项目)。

一开始我怀疑new操作符是假的,但它似乎与对象的组成有关。有趣的是,这两个不同的三分之一似乎有很强的相似性。

我在这里真的很茫然,我准备得出结论,llvm重新安排了编译单元之间结构体的布局(类/结构体当然在单独的文件中)..什么的。我想我错了,但有人经历过这样的事情吗?

我添加了以下代码。(在第二个构造函数中):

    {
        thirdPtr = new third();
        printf("second = 0x%X, third = 0x%X", this, thirdPtr);
        printf("position = %d", offsetof(second, thirdPtr);
    }

(第一个构造函数内部):

    {
        secondInstance = new second();
        printf("second = 0x%X, third = 0x%X", secondInstance, secondInstance->thirdPtr);
        printf("position = %d", offsetof(second, thirdptr));
        
    }

然后打印:

13
16

注意:这是完整结构的输出。不是这里给出的例子)。
但是:结构的布局实际上是不同的,这取决于所使用的编译/翻译单元。这到底是怎么回事?

所以我决定检查对齐,这可能是关键问题:

stdout from second constructor:

second = 0x3649090, third = 0x36490A4
offset of third in second = 16
sizeof second = 232
align of third = 4, align of second = 4

stdout from第一个构造函数:

second = 0x3649090, third = 0xA4036490
offset of second in third = 13
sizeof second = 226
align of third 1, align of second 1

所以看起来对齐方式在翻译单位中改变了。我能做些什么来在整个项目中强制执行一个标准?

我设法让它'运行',因为,它不会立即崩溃,通过应用属性((打包))到第二个类。但这真的让我感到不安全,为什么我必须这样做?在翻译单位中是否存在操纵此设置的全局设置?

很明显,当使用用户定义的结构体的打包对齐时,重置打包堆栈是多么重要。

visual studio编译器和clang的llvm前端都支持使用#pragma指令的相同语法,可以在这里学习:
http://msdn.microsoft.com/en-us/library/2e70t5y1.aspx

在我的例子中,包含的头文件有一个不匹配的#pragma pack指令,如下所示:

    #ifdef __MSVC__
        #pragma pack(push, 1)
    #else
        #pragma pack (push, 1)
    #endif
    ....
    #if defined(__MSVC__)
        #pragma pack(pop)
    #endif

打包堆栈将被任何编译器更改,但只有在使用msvc++编译器时才会恢复。这使得包含该文件的翻译单元的打包对齐方式与不包含该文件的翻译单元的打包对齐方式不同,即使两个翻译单元看到完全相同的结构定义。为了完整起见,下面是(在我的例子中)更正的#pragma指令:

    #if defined(__MSVC__) || defined (__LLVM__)
        #pragma pack (push, 1)
    #endif
    #if defined(__MSVC__) || defined (__LLVM__)
        #pragma pack(pop)
    #endif