我观察到一种奇怪的行为,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
函数,但会生成一个很好的指针重置,即使我认识到。
此处的评论中所述。这与指针类型结合会导致“零初始化”情况。 专用参考文章包含一些与此初始化风格相关的 GCC 回归报告。 GCC 可能会错误地将零初始化指针视为引用类型,但我找不到任何证据证明这种行为的可能性。出于好奇,您是否尝试切换初始化? 第二点是程序位置表
,通过@plt
指令访问。由于这里的 GCC 只生成代码,而不转换为 LLVM IR,因此这个过程会导致在指针的地址上调用
operator delete
,而不是重置指针本身,并且 NULL
重置并不被认为是必要的,而指针被认为已经被破坏。虽然你之前提到过静态成员,但它应该是侧面损坏,太糟糕了。不管怎样,原因可能是优化器错误地将指针视为引用类型。当使用 clang
时,LLVM IR 不允许这种生成,因为代码会变得模糊。