这是 GCC 优化器错误还是功能?

问题描述 投票:0回答:1

我观察到一种奇怪的行为,GCC(但不是 clang)将在全局销毁阶段跳过指针释放后重置表达式,并且具有某些

-O?
值。我可以在我尝试过的每个版本(7.2.0、9.4.0、12.3.0 和 13.2.0)中重现此问题,并且我试图了解这是否是一个错误。

该现象发生在lmdb的C++包装器中;以下是直接涉及的功能:

static inline void
lmdb::env_close(MDB_env* const env) noexcept {
  delete ::mdb_env_close(env);
}

class lmdb::env {
protected:
  MDB_env* _handle{nullptr};

public:
  ~env() noexcept {
    try { close(); } catch (...) {}
  }

  MDB_env* handle() const noexcept {
    return _handle;
  }

  void close() noexcept {
    if (handle()) {
      lmdb::env_close(handle());
      _handle = nullptr;
//   std::cerr << " handle now " << handle();
    }
//     std::cerr << "\n";
  }

我在一个类中使用这个包装器

LMDBHook
,我在其中创建了一个静态全局变量:

class LMDBHook
{
public:
    ~LMDBHook()
    {
      if (s_envExists) {
          s_lmdbEnv.close();
          s_envExists = false;
          delete[] s_lz4CompState;
      }
    }

    static bool init()
    {
        if (!s_envExists) {
            try {
                s_lmdbEnv = lmdb::env::create();
//snip
            } catch (const lmdb::error &e) {
                // as per the documentation: the environment must be closed even if creation failed!
                s_lmdbEnv.close();
            }
        }
        return false;
    }
//snip
    static lmdb::env s_lmdbEnv;
    static bool s_envExists;
    static char* s_lz4CompState;
    static size_t s_mapSize;
};

static LMDBHook LMDB;

lmdb::env LMDBHook::s_lmdbEnv{nullptr};
bool LMDBHook::s_envExists = false;
char *LMDBHook::s_lz4CompState = nullptr;
// set the initial map size to 64Mb
size_t LMDBHook::s_mapSize = 1024UL * 1024UL * 64UL;

我第一次遇到这个问题是因为使用 GCC 而不是 clang 构建时,使用

LMDBHook
的应用程序会在退出前崩溃,最初认为这是编译器对全局销毁阶段调用 dtor 的顺序的一些微妙影响。事实上,在初始化静态成员变量之后分配
LMDH
不会触发该问题。

但这里的根本问题是,来自

_handle = nullptr;

lmdb::env::close()
 行将在全局销毁阶段被 GCC 跳过(但在调用时不会,例如在 
lmdb::env::create()
 中的 
LMDBHook::init()
 之后),并且“仅”在编译时-O1、-O2、-O3 或 -Ofast(因此不能与 -O0、-Og、-Os 或 -Oz 一起使用)。
lmdb::env::close()
 中的 2 个跟踪输出表达式是我的;如果我对它们进行注释,则不会跳过重置指令。

自从如此多的版本发布以来,并且仅在全局销毁期间的特定情况下才会发生这种情况,这一事实使我认为这可能不是一个错误,但如果是这样,则行为不应该依赖于优化级别(我认为)。

我制作了一个独立/独立的演示器,其中包含使用跟踪输出表达式扩展的

lmdb++

 头文件,其中创建和关闭函数只需使用 
MDB_env
new
 以及 
delete 分配
lmdb
结构
 头文件,添加了 
MDB_env
 结构的定义(通常是不透明的): 
https://github.com/RJVB/gcc_pottial_optimiser_bug.git 。自述文件包含如何使用 Makefile 的说明,但它应该是非常不言自明的。

这是一些示例输出:

> make -B CXX=g++-mp-12 && lmdbhook g++-mp-12 --version g++-mp-12 (MacPorts gcc12 12.3.0_4+cpucompat+libcxx) 12.3.0 Copyright (C) 2022 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. g++-mp-12 -std=c++11 -O3 -o lmdbhook lmdbhook.cpp LMDBHook::LMDBHook() s_lmdbEnv=0x404248 lmdb::env::env(MDB_env*) this=0x404248 handle=0 lmdb::env::env(MDB_env*) this=0x7ffec95f40f8 handle=0x1333020 lmdb::env::~env() this=0x7ffec95f40f8 void lmdb::env::close() this=0x7ffec95f40f8 handle=0 static bool LMDBHook::init() s_lmdbEnv=0x1333020 handle=0x1333020 void lmdb::env::close() this=0x404248 handle=0x1333020 void lmdb::env_close(MDB_env*) env=0x1333020 static bool LMDBHook::init() s_lmdbEnv=0 handle=0 lmdb::env::env(MDB_env*) this=0x7ffec95f40f8 handle=0x1333020 lmdb::env::~env() this=0x7ffec95f40f8 void lmdb::env::close() this=0x7ffec95f40f8 handle=0 static bool LMDBHook::init() s_lmdbEnv=0x1333020 handle=0x1333020 mapsize=67108864 LZ4 state buffer:16384 LMDB instance is 0x404248 lmdb::env::~env() this=0x404248 void lmdb::env::close() this=0x404248 handle=0x1333020 void lmdb::env_close(MDB_env*) env=0x1333020 LMDBHook::~LMDBHook() void lmdb::env::close() this=0x404248 handle=0x1333020 void lmdb::env_close(MDB_env*) env=0x1333020 *** Error in `lmdbhook': double free or corruption (!prev): 0x0000000001333020 *** Abort > make -B CXX=clang++-mp-12 && lmdbhook clang++-mp-12 --version clang version 12.0.1 Target: x86_64-unknown-linux-gnu Thread model: posix InstalledDir: /opt/local/libexec/llvm-12/bin clang++-mp-12 -std=c++11 -O3 -o lmdbhook lmdbhook.cpp LMDBHook::LMDBHook() s_lmdbEnv=0x205090 lmdb::env::env(MDB_env *const) this=0x205090 handle=0 lmdb::env::env(MDB_env *const) this=0x7ffef6c06ca0 handle=0x2e8c20 lmdb::env::~env() this=0x7ffef6c06ca0 void lmdb::env::close() this=0x7ffef6c06ca0 handle=0 static bool LMDBHook::init() s_lmdbEnv=0x2e8c20 handle=0x2e8c20 void lmdb::env::close() this=0x205090 handle=0x2e8c20 void lmdb::env_close(MDB_env *const) env=0x2e8c20 static bool LMDBHook::init() s_lmdbEnv=0 handle=0 lmdb::env::env(MDB_env *const) this=0x7ffef6c06ca0 handle=0x2e8c20 lmdb::env::~env() this=0x7ffef6c06ca0 void lmdb::env::close() this=0x7ffef6c06ca0 handle=0 static bool LMDBHook::init() s_lmdbEnv=0x2e8c20 handle=0x2e8c20 mapsize=67108864 LZ4 state buffer:16384 LMDB instance is 0x205090 lmdb::env::~env() this=0x205090 void lmdb::env::close() this=0x205090 handle=0x2e8c20 void lmdb::env_close(MDB_env *const) env=0x2e8c20 LMDBHook::~LMDBHook() void lmdb::env::close() this=0x205090 handle=0
编辑:上面的示例是在 Linux 上使用自建编译器,但系统编译器在 Mac 上也显示出相同的行为,并且 C++ 运行时(libc++ 或 libstdc++)的选择对这两个平台都没有影响。

EDIT2:演示器还有一个

lmdb::env::close()

 的替代实现,就像我写的那样,它将 
_handle
 ptr 缓存在 tmp 中。变量,重置 
_handle
 然后释放缓存的指针。无论这个问题到底是什么,此实现都不受制于。

EDIT3:我通过 gdb (13.2) 快速浏览了汇编代码。并不是说我真的能读懂它,但我没有看到其中有任何可以解释条件行为的内容。使用

g++ -O1 -g

 构建并在 
lmdh::env::close()
 上设置断点(显然有 6 个位置!)并一直运行,直到出现问题的调用,我明白了

Breakpoint 1.1, lmdb::env::close (this=0x405250 <LMDBHook::s_lmdbEnv>) at lmdb+++.h:1185 1185 std::cerr << __PRETTY_FUNCTION__ << " this=" << this << " handle=" << handle() << "\n"; (gdb) n void lmdb::env::close() this=0x405250 handle=0x418020 1187 if (handle()) { (gdb) n 1188 lmdb::env_close(handle()); (gdb) void lmdb::env_close(MDB_env*) env=0x418020 lmdb::env::~env (this=<optimised out>, __in_chrg=<optimised out>) at lmdb+++.h:1189 1189 _handle = nullptr; (gdb) p _handle value has been optimised out (gdb) info line 1189 Line 1189 of "lmdb+++.h" starts at address 0x401672 <_ZN4lmdb3envD2Ev+270> and ends at 0x401679. (gdb) disas 0x401672,0x401679 Dump of assembler code from 0x401672 to 0x401679: => 0x0000000000401672 <_ZN4lmdb3envD2Ev+270>: add $0x8,%rsp 0x0000000000401676 <_ZN4lmdb3envD2Ev+274>: pop %rbx 0x0000000000401677 <_ZN4lmdb3envD2Ev+275>: pop %rbp 0x0000000000401678 <_ZN4lmdb3envD2Ev+276>: ret End of assembler dump. (gdb) c Continuing. LMDBHook::~LMDBHook() Breakpoint 1.2, lmdb::env::close (this=0x405250 <LMDBHook::s_lmdbEnv>) at lmdb+++.h:1185 1185 std::cerr << __PRETTY_FUNCTION__ << " this=" << this << " handle=" << handle() << "\n"; (gdb) c Continuing. void lmdb::env::close() this=0x405250 handle=0x418020 void lmdb::env_close(MDB_env*) env=0x418020 *** Error in `/home/bertin/work/src/Scratch/KDE/KF5/LMDBHookTest/lmdbhook': double free or corruption (!prev): 0x0000000000418020 *** Program received signal SIGABRT, Aborted.
上面反汇编的第1189行上方的2条asm指令:

0x000000000040166a <+262>: mov %rbx,%rdi 0x000000000040166d <+265>: call 0x4010c0 <_ZdlPv@plt>
我可以认出这是对

delete(env)

的呼叫。下面的 3 条指令似乎没有将任何内容设置为 NULL,但正如我所说,我并没有真正阅读 x86 程序集。

这是使用

clang++-12 -O1 -g

 构建的相同 2 行的组装:

Line 1188 of "./lmdb+++.h" starts at address 0x202f3b <_ZN4lmdb3env5closeEv+107> and ends at 0x202f43 <_ZN4lmdb3env5closeEv+115>. (gdb) info line 1189 Line 1189 of "./lmdb+++.h" starts at address 0x202f4b <_ZN4lmdb3env5closeEv+123> and ends at 0x202f52 <_ZN4lmdb3env5closeEv+130>. (gdb) disas 0x202f3b,0x202f52 Dump of assembler code from 0x202f3b to 0x202f52: 0x0000000000202f3b <_ZN4lmdb3env5closeEv+107>: mov %r14,%rdi 0x0000000000202f3e <_ZN4lmdb3env5closeEv+110>: call 0x202f70 <_ZNK4lmdb3env6handleEv> 0x0000000000202f43 <_ZN4lmdb3env5closeEv+115>: mov %rax,%rdi 0x0000000000202f46 <_ZN4lmdb3env5closeEv+118>: call 0x202860 <_ZN4lmdbL9env_closeEP7MDB_env> => 0x0000000000202f4b <_ZN4lmdb3env5closeEv+123>: movq $0x0,(%r14) End of assembler dump.
Clang 不会使用 -O1 内联 

env_close

 函数,但会生成一个很好的指针重置,即使我认识到。

optimization g++ clang++ object-destruction
1个回答
0
投票
在这里我们似乎有两种情况的叠加。首先,类似列表的初始化如

此处的评论中所述。这与指针类型结合会导致“零初始化”情况。 专用参考文章包含一些与此初始化风格相关的 GCC 回归报告。 GCC 可能会错误地将零初始化指针视为引用类型,但我找不到任何证据证明这种行为的可能性。出于好奇,您是否尝试切换初始化? 第二点是程序位置表

,通过

@plt指令访问。由于这里的 GCC 只生成代码,而不转换为 LLVM IR,因此这个过程会导致在指针的地址上调用

operator delete
,而不是重置指针本身,并且
NULL
重置并不被认为是必要的,而指针被认为已经被破坏。虽然你之前提到过静态成员,但它应该是侧面损坏,太糟糕了。不管怎样,原因可能是优化器错误地将指针视为引用类型。当使用
clang
时,LLVM IR 不允许这种生成,因为代码会变得模糊。
© www.soinside.com 2019 - 2024. All rights reserved.