本文介绍了符号可见性,异常,运行时错误的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我尝试更好地了解符号的可见性。 GCC Wiki()有一个部分关于C ++异常问题。根据GCC Wiki,由于没有导出的异常,可能有运行时错误。没有编译时错误/警告的运行时错误是相当危险的,所以我试图更好地了解问题。我做了一些实验,但我仍然无法重现。任何想法如何重现问题?



维基提到三个图书馆互相使用,所以我做了三个小图书馆。



我运行以下命令:



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

  make 
./dsouser

带有vtable的异常类但是它没有导出(甚至不编译):

  make HAS_VIRTUAL = 1 

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

  make HAS_VIRTUAL = 1 EXCEPTION_VISIBLE = 1 
./dsouser

Makefile: p>

  CXX = g ++  -  4.7.1 
CFLAGS = -ggdb -O0 -fvisibility = hidden
ifdef EXCEPTION_VISIBLE
CFLAGS + = - DEXCEPTION_VISIBLE
endif
ifdef HAS_VIRTUAL
CFLAGS + = - DHAS_VIRTUAL
endif
全部: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)))
命名空间dso
{

#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> 
#includemydso.h
命名空间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(默认)))
命名空间dso2
{
void SYMBOL_VISIBLE some_func();
}
#endif

mydso2.cpp:

  #include< iostream> 
#includemydso.h
#includemydso2.h
命名空间dso2
{
void some_func()
{
throw dso :: MyException();
}
}

mydso3.h:

  #ifndef DSO3_H_INCLUDED 
#define DSO3_H_INCLUDED
#define SYMBOL_VISIBLE __attribute__((visibility(default)))
命名空间dso3
{
void SYMBOL_VISIBLE some_func();
}
#endif

mydso3.cpp:

  #include< iostream> 

#includemydso2.h
#includemydso3.h

#include< iostream>

命名空间dso3
{

void some_func()
{
try
{
dso2 :: some_func ();
} catch(std :: exception e)
{
std :: cout<<< 有异常
}
}

}

dsouser。 cpp:

  #include< iostream> 
#includemydso3.h
int main()
{
dso3 :: some_func();
return 0;
}

谢谢,
Dani

解决方案

我是GCC的原始补丁的作者,添加了类可见性支持,而我原来的GCC如何克隆的是。我感谢VargaD给我个人电子邮件告诉我这个SO问题。



你观察到的行为对最近的GCC有效,但并不总是如此。当我原来在2004年修补GCC时,我向GCC的bugzilla提交了GCC异常处理运行时的请求,以便通过比较它们的符号符号的字符串比较来比较抛出的类型,而不是比较这些字符串的地址虽然这种行为是MSVC所做的,尽管GCC维护者在当时被GCC维护者拒绝,尽管在异常抛出期间的性能一般不被认为是重要的,因为它们应该是罕见的。因此,我不得不向我的可见性指南添加一个特定的异常,说任何抛出的类型永远不会被隐藏,而不是一次,在二进制中,隐藏胜过默认,所以只有一个隐藏的符号声明保证覆盖所有的在给定的二进制文件中的相同符号。



接下来发生了什么我想我们没有人希望 - KDE非常公开地接受了我的贡献功能。几乎每个大型GCC使用项目都在惊人的短暂时间内进入。突然间的符号隐藏是规范,而不是例外。



不幸的是,少数人没有正确地应用我的引导异常抛出类型,而常量错误报告关于GCC中不正确的交叉共享对象异常处理最终导致GCC维护者放弃和多年后补丁的字符串比较,抛出类型匹配,正如我最初请求的。因此,在新的海湾合作机构中,情况有所好转。我没有改变我的指南和说明,因为这种方法在v4.0之后的每个GCC上仍然是最安全的,而由于现在使用字符串比较,较新的GCC更可靠地处理异常抛出,遵循指南的规则不会受到伤害那个。



这使我们遇到了typeinfo问题。一个大问题是,最佳实践C ++要求您始终在可抛出类型中继承虚拟,因为如果组合两个异常类型,则从std :: exception继承(让我们说) ,有两个等距的std :: exception基类会导致catch(std :: exception&)自动调用terminate(),因为它不能解析要匹配的基类,所以你必须只有一个std :: exception基础类,并且相同的理由适用于任何可投掷类型的组合。这种最佳做法在任何C ++库中都是特别需要的,因为您不知道第三方用户将如何处理异常类型。



换句话说,这意味着所有抛出的最佳实践中的异常类型将始终带有每个基类的连续RTTI链,并且该异常匹配现在是内部对匹配类型执行一个成功的dynamic_cast的情况,O(基类数)操作。而对于dynamic_cast<>来处理几乎继承类型的链,您猜测到,您需要每个链 具有默认可见性。如果从执行catch()的代码中隐藏了一个,那么整个caboodle都会被打开,你会得到一个terminate()。如果您重写上面的示例代码,我将非常感兴趣,以便几乎继承并看到会发生什么 - 您的评论之一表示拒绝链接,这是非常好的。但是我们来说,DLL A定义了类型A,DLL B子类型A到B,DLL C子类型B转换为C,程序D试图在类型C被抛出时捕获类型A的异常。程序D将具有A类型信息可用,但是当尝试为B类和C类获取RTTI时,应该出现故障。尽管如此,最近的GCC也修复了?我不知道,我近年来的注意事项是对于所有C ++编译器的未来。嗯,显然,这是一个混乱,但它是一个ELF-特定的混乱 - 这些都不影响PE或MachO,这两个都通过不使用进程全局符号表来获得上述所有权利。然而,对于C ++ 17工作的WG21 SG2模块研究组必须有效地实现模块的导出模板,以解决ODR违规问题,而C ++ 17是我看到使用LLVM编写的第一个提出的标准心神。换句话说,C ++ 17编译器将不得不将一个复杂的AST转储到光盘上,就像clang一样。这意味着RTTI可用的担保大幅增加 - 实际上,这就是为什么我们有SG7反思研究组,因为来自C ++模块的AST可以大大增加可能的自我反省机会。换句话说,期望上述问题很快就会被C ++ 17采用。



所以,简而言之,现在继续关注我的原始指南。在未来的十年中,事情有望得到更好的发展。并感谢苹果为这个解决方案提供资金,由于恶意的恶化已经很久了。



Niall


I try to understand symbol visibility better. The GCC Wiki (http://gcc.gnu.org/wiki/Visibility) has a section about "Problems with C++ exceptions". According to GCC Wiki it is possible the have runtime error because of not exported exceptions. Runtime errors without compile time error/warning is quite dangerous so I tried to understand the problem better. I made some experiments but I still can't reproduce it. Any ideas how to reproduce the problem?

The Wiki mentions three library using each other, so I made three small library.

I run the following commands:

Exception class without vtable (works as expected):

make
./dsouser

Exception class with vtable but it does not exported (does not even compile):

make HAS_VIRTUAL=1

Exception class exported vtable (works as expected):

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

Thanks,Dani

解决方案

I'm the author of the original patch to GCC adding class visibility support, and my original howto which GCC cloned is at http://www.nedprod.com/programs/gccvisibility.html. My thanks to VargaD for emailing me personally to tell me about this SO question.

The behaviour you observe is valid for recent GCCs, however it was not always so. When I originally patched GCC back in 2004 I submitted a request to GCC bugzilla for the GCC exception handling runtime to compare thrown types by string comparison of their mangled symbols instead of comparing the addresses of those strings - this was rejected at that time by the GCC maintainers as an unacceptable runtime cost, despite that this behaviour is what MSVC does, and despite that performance during exception throws is generally not considered important given they're supposed to be rare. Hence I had to add a specific exception to my visibility guide to say that any thrown type must never be hidden, not once, in a binary as "hiddenness" trumps "default" so just a single hidden symbol declaration guarantees to override all cases of the same symbol in a given binary.

What happened next I suppose none of us expected - KDE very publicly embraced my contributed feature. That cascaded into almost every large GCC-using project in an amazingly short time. Suddenly symbol hiding was the norm, not the exception.

Unfortunately, a small number of people didn't apply my guide correctly for exception thrown types, and the constant bug reports about incorrect cross-shared object exception handling in GCC eventually caused the GCC maintainers to give up and many years later patch in string comparison for thrown type matching, as I had originally requested. Hence in newer GCCs the situation is somewhat better. I haven't changed my guide nor the instructions because that approach is still safest on every GCC since v4.0, and while newer GCCs are more reliable in handling exception throws due to now using string comparison, following the guide's rules doesn't hurt that.

This brings us onto the typeinfo problem. A big problem is that best practice C++ requires you to always inherit virtually in throwable types, because if you compose two exception types both inheriting (let's say) from std::exception, having two equidistant std::exception base classes will cause a catch(std::exception&) to auto call terminate() because it can't resolve which base class to match, so you must only ever have one std::exception base class ever, and the same rationale applies to any possible composition of throwable type. This best practice is especially required in any C++ library, because you can't know what third party users will do with your exception types.

In other words, this means that all thrown exception types in best practice will always come with a chain of successive RTTI for each base class, and that exception matching is now a case of internally doing a successful dynamic_cast<> to the type being matched, an O(number of base classes) operation. And for dynamic_cast<> to work over a chain of virtually inherited types, you guessed it, you need every single one of this chain to have default visibility. If even one is hidden from the code executing the catch(), the whole caboodle goes belly up and you get a terminate(). I'd be very interested if you reworked your example code above to virtually inherit and see what happens - one of your comments says it refuses to link, which is great. But let's say DLL A defines type A, DLL B subclasses type A into B, DLL C subclasses type B into C, and program D tries to catch an exception of type A when type C was thrown. Program D will have the type info of A available, but should fault when trying to fetch RTTI for types B and C. Maybe, though, recent GCCs have fixed this too? I don't know, my attention in recent years is on clang as that's the future for all C++ compilers.

Obviously, this is a mess, but it's an ELF-specific mess - none of this affects PE or MachO, both of which get all of the above right by not using process-global symbol tables in the first place. However the WG21 SG2 Modules study group working towards C++17 must effectively implement exported templates for modules to work in order to resolve ODR violations, and C++17 is the first proposed standard I've seen to be written with a LLVM in mind. In other words, C++17 compilers will have to dump a complex AST onto disc like clang does. And that implies a huge increase in the guarantees of what RTTI is available - indeed, that's why we have the SG7 Reflection study group, because the AST from C++ Modules enables a huge increase in possible self-reflection opportunities. In other words, expect the above problems to go away soon with C++17 adoption.

So, in short, keep following my original guide for now. And things will hopefully get vastly better in the next decade. And give thanks to Apple for funding that solution, it's been a very long time in coming due to how wicked hard it is.

Niall

这篇关于符号可见性,异常,运行时错误的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-19 15:23
查看更多