符号可见性、异常、运行时错误
Posted
技术标签:
【中文标题】符号可见性、异常、运行时错误【英文标题】:Symbol visibility, exceptions, runtime error 【发布时间】:2012-12-25 11:29:21 【问题描述】:我尝试更好地理解符号可见性。 GCC Wiki (http://gcc.gnu.org/wiki/Visibility) 有一个关于“C++ 异常问题”的部分。根据 GCC Wiki,由于未导出异常,可能会出现 runtime 错误。没有编译时错误/警告的运行时错误是非常危险的,所以我试图更好地理解这个问题。我做了一些实验,但我仍然无法重现它。任何想法如何重现该问题?
Wiki 中提到了三个库相互使用,所以我做了三个小库。
我运行以下命令:
没有 vtable 的异常类(按预期工作):
make
./dsouser
带有 vtable 的异常类,但它不导出(甚至不编译):
make HAS_VIRTUAL=1
异常类导出 vtable(按预期工作):
make HAS_VIRTUAL=1 EXCEPTION_VISIBLE=1
./dsouser
生成文件:
CXX=g++-4.7.1
CFLAGS=-ggdb -O0 -fvisibility=hidden
ifdef EXCEPTION_VISIBLE
CFLAGS+=-DEXCEPTION_VISIBLE
endif
ifdef HAS_VIRTUAL
CFLAGS+=-DHAS_VIRTUAL
endif
all: dsouser
libmydso.so: mydso.cpp mydso.h
$(CXX) $(CFLAGS) -fPIC -shared -Wl,-soname,$@ -o $@ $<
libmydso2.so: mydso2.cpp mydso.h mydso2.h libmydso.so
$(CXX) $(CFLAGS) -L. -fPIC -shared -Wl,-soname,$@ -o $@ $< -lmydso
libmydso3.so: mydso3.cpp mydso.h mydso2.h mydso3.h libmydso2.so
$(CXX) $(CFLAGS) -L. -fPIC -shared -Wl,-soname,$@ -o $@ $< -lmydso -lmydso2
dsouser: dsouser.cpp libmydso3.so
$(CXX) $< $(CFLAGS) -L. -o $@ -lmydso -lmydso2 -lmydso3
clean:
rm -f *.so *.o dsouser
.PHONY: all clean
mydso.h:
#ifndef DSO_H_INCLUDED
#define DSO_H_INCLUDED
#include <exception>
#define SYMBOL_VISIBLE __attribute__ ((visibility ("default")))
namespace dso
class
#ifdef EXCEPTION_VISIBLE
SYMBOL_VISIBLE
#endif
MyException : public std::exception
public:
#ifdef HAS_VIRTUAL
virtual void dump();
#endif
void SYMBOL_VISIBLE foo();
;
#endif
mydso.cpp:
#include <iostream>
#include "mydso.h"
namespace dso
#ifdef HAS_VIRTUAL
void MyException::dump()
#endif
void MyException::foo()
#ifdef HAS_VIRTUAL
dump();
#endif
mydso2.h:
#ifndef DSO2_H_INCLUDED
#define DSO2_H_INCLUDED
#define SYMBOL_VISIBLE __attribute__ ((visibility ("default")))
namespace dso2
void SYMBOL_VISIBLE some_func();
#endif
mydso2.cpp:
#include <iostream>
#include "mydso.h"
#include "mydso2.h"
namespace dso2
void some_func()
throw dso::MyException();
mydso3.h:
#ifndef DSO3_H_INCLUDED
#define DSO3_H_INCLUDED
#define SYMBOL_VISIBLE __attribute__ ((visibility ("default")))
namespace dso3
void SYMBOL_VISIBLE some_func();
#endif
mydso3.cpp:
#include <iostream>
#include "mydso2.h"
#include "mydso3.h"
#include <iostream>
namespace dso3
void some_func()
try
dso2::some_func();
catch (std::exception e)
std::cout << "Got exception\n";
dsouser.cpp:
#include <iostream>
#include "mydso3.h"
int main()
dso3::some_func();
return 0;
谢谢, 丹妮
【问题讨论】:
我也无法重现任何问题。我怀疑应该没有。链接的文章告诉我们正确捕获异常需要一个符号,但它没有告诉我们为什么需要它。它说有一个 typeinfo 查找,但它没有说明应该在哪里进行查找。在整个程序的符号表中?如果程序被剥离怎么办?只在抛出的异常数据中包含 typeinfo 指针不是更简单更容易吗? 我制作了另一个小型测试应用程序:一个带有异常(从 std::exception 继承)的库,它没有被导出,但它有一个虚拟方法,所以它有 vtable。该库有一个引发异常的函数。主程序包含带有异常的标头,但如果由于缺少类型信息而尝试准确捕获异常,则无法编译。但是它正确捕获了 std::exception。如果没有虚拟方法,它也会捕获我的异常。 【参考方案1】:我是添加类可见性支持的 GCC 原始补丁的作者,我最初克隆的 GCC 方法位于 http://www.nedprod.com/programs/gccvisibility.html。感谢 VargaD 亲自给我发电子邮件告诉我这个 SO 问题。
您观察到的行为对最近的 GCC 有效,但并非总是如此。当我最初在 2004 年修补 GCC 时,我向 GCC bugzilla 提交了一个请求,要求 GCC 异常处理运行时通过字符串比较它们的损坏符号而不是比较这些字符串的 地址 来比较抛出的类型 - 这是当时被 GCC 维护者拒绝为不可接受的运行时成本,尽管这种行为是 MSVC 所做的,并且尽管异常抛出期间的性能通常被认为不重要,因为它们应该很少见。因此,我不得不在我的可见性指南中添加一个特定的例外,以说明任何抛出的类型都不能在二进制文件中被隐藏,而不是一次,因为“隐藏”胜过“默认”,因此只需一个隐藏符号声明即可保证覆盖所有情况给定二进制文件中的相同符号。
接下来发生的事情我想我们都没有预料到 - KDE 非常公开地接受了我贡献的功能。在极短的时间内,几乎所有使用 GCC 的大型项目都参与其中。突然间,符号隐藏成为常态,而不是例外。
不幸的是,少数人没有正确地应用我的指南来处理异常抛出的类型,并且不断有关于 GCC 中不正确的跨共享对象异常处理的错误报告最终导致 GCC 维护人员放弃并在多年后修补正如我最初要求的那样,在抛出类型匹配的字符串比较中。因此,在较新的 GCC 中,情况要好一些。我没有更改我的指南和说明,因为自 v4.0 以来,该方法在每个 GCC 上仍然是最安全的,虽然由于现在使用字符串比较,较新的 GCC 在处理异常抛出方面更可靠,但遵循指南的规则并没有什么坏处那个。
这给我们带来了 typeinfo 问题。一个大问题是,最佳实践 C++ 要求您总是继承 虚拟 in throwable 类型,因为如果您组合两个异常类型都从 std::exception 继承(比方说) , 拥有两个等距的 std::exception 基类将导致 catch(std::exception&) 自动调用 terminate() 因为它无法解析要匹配的基类,所以你必须只有一个 std::exception 基类类,并且相同的原理适用于任何可能的可抛出类型组合。在任何 C++ 库中都特别需要这种最佳实践,因为您无法知道第三方用户会如何处理您的异常类型。
换句话说,这意味着在最佳实践中所有抛出的异常类型总是伴随着每个基类的连续 RTTI 链,并且异常匹配现在是在内部对类型进行成功的 dynamic_cast 的情况匹配,O(基类数量)操作。您猜对了,要使 dynamic_cast 能够处理一系列虚拟继承的类型,您需要该链中的每一个 都具有默认可见性。如果在执行 catch() 的代码中甚至隐藏了一个,那么整个 caboodle 就会翻倒,你会得到一个 terminate()。如果您重新编写上面的示例代码以虚拟继承并查看会发生什么,我会非常感兴趣 - 您的一个 cmets 说它拒绝链接,这很好。但是假设 DLL A 定义了类型 A,DLL B 将类型 A 子类化为 B,DLL C 将类型 B 子类化为 C,并且程序 D 在抛出类型 C 时尝试捕获类型 A 的异常。程序 D 将提供 A 的类型信息,但在尝试获取 B 和 C 类型的 RTTI 时应该出错。不过,也许最近的 GCC 也修复了这个问题?我不知道,近年来我的注意力都集中在 clang 上,因为这是所有 C++ 编译器的未来。
显然,这是一团糟,但它是特定于 ELF 的一团糟——这些都不会影响 PE 或 MachO,它们都通过首先不使用进程全局符号表来获得上述所有权利。然而,致力于 C++17 的 WG21 SG2 模块研究组必须有效地实现模块的导出模板才能解决 ODR 违规问题,而 C++17 是我见过的第一个使用 LLVM 编写的提议标准头脑。换句话说,C++17 编译器必须像 clang 一样将复杂的 AST 转储到磁盘上。这意味着 RTTI 可用保证的巨大增加——事实上,这就是我们有 SG7 反思研究组的原因,因为 C++ 模块中的 AST 可以极大地增加可能的自我反思机会。换句话说,预计随着 C++17 的采用,上述问题会很快消失。
所以,简而言之,现在继续遵循我的原始指南。未来十年,情况有望大大改善。还要感谢 Apple 为该解决方案提供资金,因为它的难度很大,所以已经很长时间了。
尼尔
【讨论】:
以上是关于符号可见性、异常、运行时错误的主要内容,如果未能解决你的问题,请参考以下文章