Symbol visibility,异常,运行时错误

21

我正在努力更好地理解符号可见性。GCC Wiki(http://gcc.gnu.org/wiki/Visibility)有一节关于“C++异常问题”。据GCC Wiki称,由于未导出异常,可能会出现运行时错误。没有编译时错误/警告的运行时错误非常危险,因此我试图更好地理解这个问题。我进行了一些实验,但仍然无法重现它。有任何想法如何重现这个问题吗?

Wiki提到了三个库相互使用,所以我做了三个小库。

我运行了以下命令:

没有虚表的异常类(按预期工作):

make
./dsouser

具有虚函数表但未导出的异常类(甚至无法编译):

make HAS_VIRTUAL=1

异常类导出的虚表(按预期工作):

make HAS_VIRTUAL=1 EXCEPTION_VISIBLE=1
./dsouser

Makefile:

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;
}

谢谢,Dani


我也无法重现任何问题。我怀疑不应该有任何问题。链接的文章告诉我们,需要一个符号来正确捕获异常,但它并没有告诉我们为什么需要它。它说有一个typeinfo查找,但它没有说查找应该在哪里进行。在整个程序的符号表中吗?如果程序被剥离了呢?将typeinfo指针包含在抛出的异常数据中是否更简单、更容易呢? - n. m.
我制作了另一个小测试应用程序:一个库和一个继承自std::exception的异常(未导出),但它有一个虚方法,因此它具有vtable。该库有一个抛出异常的函数。主程序包括具有异常的标头,但是如果我尝试捕获完全符合我的异常类型的异常,则无法编译。然而,它可以正确地捕获std::exception。如果没有虚方法,它也会捕获我的异常。 - VargaD
1个回答

31

我是添加类可见性支持的GCC原始补丁的作者,我的原始操作说明被GCC克隆,在http://www.nedprod.com/programs/gccvisibility.html可以找到。感谢VargaD亲自发送电子邮件告诉我有关该问题。

您观察到的行为对于最近的GCC是有效的,但并非总是如此。当我在2004年最初对GCC进行修补时,我向GCC bugzilla提交了一个请求,要求GCC异常处理运行时通过比较它们的符号名来进行抛出类型的字符串比较,而不是比较这些字符串的地址-尽管这种行为是MSVC所做的,尽管在异常抛出期间的性能通常不被认为很重要,因为它们应该很少发生。因此,我必须在我的可见性指南中添加一个特定的异常,即任何抛出类型都不能被隐藏,不管是一次还是多次,因为“隐藏”优先于“默认”,因此仅一个隐藏符号声明就可以保证覆盖给定二进制文件中所有相同符号的情况。

接下来发生的事情,我想我们都没料到-KDE公开接受了我的贡献功能。这导致几乎每个大型GCC使用项目在极短的时间内都采用了符号隐藏作为常态,而不是例外。

不幸的是,一小部分人没有正确应用我的指南来处理异常抛出类型,并且有关GCC中不正确的跨共享对象异常处理的不断错误报告最终迫使GCC维护者放弃并许多年后针对所请求的最初字符串比较做出补丁。因此,在更新的GCC中,情况要好一些。我没有更改我的指南或说明,因为该方法仍然是每个GCC自v4.0以来最安全的方法,并且尽管由于现在使用字符串比较而使新GCC更可靠地处理异常抛出,但遵循指南规则并不会对其造成伤害。

这让我们涉及到typeinfo问题。一个大问题是,在最佳实践的C++中,你需要在可抛出类型中始终使用虚拟继承,因为如果你组合了两种异常类型(假设)都从std::exception继承,那么有两个等距的std::exception基类会导致catch(std::exception&)自动调用terminate(),因为它无法确定匹配哪个基类,所以你必须永远只有一个std::exception基类,并且相同的原理适用于任何可能的可抛出类型组合。这种最佳实践在任何C++库中都是特别重要的,因为你不知道第三方用户将如何处理你的异常类型。
换句话说,这意味着在最佳实践中,所有抛出的异常类型都将带有一系列连续的RTTI,每个基类一个,而异常匹配现在是一个内部成功执行dynamic_cast<>来匹配类型的过程,这是一种O(基类数)的操作。为了使dynamic_cast<>在虚拟继承类型的链上工作,你需要确保此链中每一个都具有默认可见性。如果其中一个对正在执行catch()的代码隐藏,则整个进程将崩溃并终止。如果您重新设计上面的示例代码以进行虚拟继承并查看会发生什么,我会非常感兴趣 - 您的一个注释说它拒绝链接,这很好。但是假设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,因为它们首先不使用进程全局符号表。然而,WG21 SG2模块研究小组正在朝着实现模块导出模板的方向努力,以解决ODR违规问题,并且C ++ 17是我见过的第一个针对LLVM编写的提案标准。换句话说,C++17编译器将像clang一样将复杂的AST转储到磁盘上。这意味着可用的RTTI保证将大大增加-这正是我们有SG7 Reflection学习小组的原因,因为来自C ++模块的AST使得可能出现大量自我反射机会。换句话说,预计随着C++17的采用,上述问题很快就会消失。

因此,简而言之,现在继续遵循我的原始指南。未来十年,情况有望得到极大改善。感谢苹果资助该解决方案,由于其极其困难,这个解决方案已经出现了很长时间。

Niall


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接