使用-fno-rtti在OS X上抛出和捕获异常的问题

时间:2014-02-12 19:01:29

标签: c++ macos exception shared-libraries rtti

这个问题与this question有点类似,但接受的答案并没有真正提出解决方案或解决方法。

在我们的项目中,我们有一个dylib和主要的可执行文件。 dylib使用-fno-rtti编译,而可执行文件使用RTTI。当从dylib抛出异常(例如std::bad_alloc)并在exe中捕获时会发生此问题。

(在你喊“异常需要RTTI以便你必须打开它!”之前,请注意,无论<{1}}或{{1> 总是生成异常所需的RTTI这实际上记录在-frtti标志描述中。关于OS X的问题是它不是以相同的方式生成的)

经过一番调查,发现了以下内容:

  • 在dylib(-fno-rtti)中,有一个异常RTTI结构的本地副本;特别是-fno-rtti符号(-fno-rtti)。
  • exe(__ZTISt9bad_alloc)从typeinfo for std::bad_alloc导入typeinfo符号,但没有本地副本

由于异常处理代码依赖于比较typeinfo指针来确定异常匹配,因此匹配失败,只有-frtti成功。

到目前为止,我看到以下选项:

1)使用libstdc++.6.dylib编译所有内容,或至少编译和捕获异常的文件。这是可行的,但我不喜欢为所有东西生成RTTI的想法,即使我们不使用它;并且使用异常的文件列表很容易过时。

2)当链接dylib时,以某种方式使链接器从目标文件中删除弱异常定义并使用catch(...)中的那个。到目前为止,我没有成功。

3)???

我做了一个小测试来说明这个问题。

-frtti

运行:

libstdc++.6.dylib

涉及的文件的符号:

--- throw.cpp ---
#include <iostream>

#if defined(__GNUC__)
#define EXPORT __attribute__((visibility("default")))
#else
#define EXPORT __declspec(dllexport)
#endif

EXPORT void dothrow ()
{
   std::cout << "before throw" << std::endl;
   throw std::bad_alloc();
}

--- main.cpp ---
#include <stdio.h>
#include <iostream>

#if defined(__GNUC__)
#define IMPORT extern
#else
#define IMPORT __declspec(dllimport)
#endif

IMPORT void dothrow ();

int main (void) {
 try {
   std::cout << "trying lib->main exception" << std::endl;
   dothrow ();
 }
 catch ( const std::bad_alloc& )
 {
   std::cout << "caught bad_alloc in main - good." << std::endl;
 }
 catch (...)
 {
   std::cout << "caught ... in main - bad!" << std::endl;
 }
}

--- makefile ---
# for main exe
CFLAGS_RTTI=-m32 -frtti -fvisibility=hidden -fvisibility-inlines-hidden -shared-libgcc -funwind-tables
# for dylib
CFLAGS_NORTTI=-m32 -fno-rtti -fvisibility=hidden -fvisibility-inlines-hidden -shared-libgcc
# for linking; some switches which don't help
CLFLAGS=-Wl,-why_live,-warn_commons,-weak_reference_mismatches,error,-commons,error

all: test

test: libThrow.dylib main.o
    g++ $(CFLAGS_RTTI) -o test main.o -lthrow -L./ $(CLFLAGS)

main.o: main.cpp
    g++ $(CFLAGS_RTTI) -c -o main.o main.cpp

throw.o: throw.cpp
    g++ $(CFLAGS_NORTTI) -c -o throw.o throw.cpp

libThrow.dylib: throw.o
    g++ $(CFLAGS_NORTTI) -dynamiclib -o libThrow.dylib throw.o

clean:
    rm test main.o throw.o

注意:Linux和Windows上类似的编译选项工作正常。我可以从共享对象/ dll中抛出异常并在主exe中捕获它们,即使它们使用不同的$ ./test trying lib->main exception before throw caught ... in main - bad! / $ nm -m throw.o | grep bad_alloc 000001be (__TEXT,__textcoal_nt) weak private external __ZNSt9bad_allocC1Ev 000001be (__TEXT,__textcoal_nt) weak private external __ZNSt9bad_allocC1Ev 00000300 (__TEXT,__eh_frame) weak private external __ZNSt9bad_allocC1Ev.eh (undefined) external __ZNSt9bad_allocD1Ev 00000290 (__DATA,__const_coal) weak external __ZTISt9bad_alloc 000002a4 (__TEXT,__const_coal) weak external __ZTSSt9bad_alloc (undefined) external __ZTVSt9bad_alloc $ nm -m libThrow.dylib | grep bad_alloc 00000ce6 (__TEXT,__text) non-external __ZNSt9bad_allocC1Ev (undefined) external __ZNSt9bad_allocD1Ev (from libstdc++) 00001050 (__DATA,__const) weak external __ZTISt9bad_alloc 00000e05 (__TEXT,__const) weak external __ZTSSt9bad_alloc (undefined) external __ZTVSt9bad_alloc (from libstdc++) $ nm -m main.o | grep bad_alloc (undefined) external __ZTISt9bad_alloc $ nm -m test | grep bad_alloc (undefined) external __ZTISt9bad_alloc (from libstdc++) 选项进行编译。


编辑:以下是我为-frtti的具体案例解决问题的方法:

-fno-rtti

函数bad_alloc是从#if defined(__GLIBCXX__) || defined(_LIBCPP_VERSION) #define throw_nomem std::__throw_bad_alloc #else #define throw_nomem throw std::bad_alloc #endif EXPORT void dothrow () { std::cout << "before throw" << std::endl; throw_nomem(); } 导入的,因此始终会抛出正确的类型。

3 个答案:

答案 0 :(得分:4)

您可以简单地将所有“抛出异常”基础架构移动到启用了-frtti的帮助程序库中 - 并将其链接到其他内容。如果没有实际代码,很难说这种分解是否可行。

以下是一些示例代码:

// Thrower.cc
void DoThrow() {
  throw std::bad_alloc;
}

// LibraryNoRTTI.cc
void f() {
  DoThrow();
}

// main.cc
int main() {
  try {
    f();
  }
  catch(std::bad_alloc&) {}
  return 0;
}

最简单的方法是将所有throw次调用移至具有相应类型的单独函数,例如:throw std::logical_error("message");转到void ThrowLogicError(const std::string& message) { ... }

如果封装(私有异常类)存在问题,那么你可以与投掷函数交朋友。


如果您仍想在非rtti库中使用(throw / catch)异常,则必须在库API中使用的内部异常和异常之间进行分离。

好的方法是将内部C ++ throw - catch用于内部目的 - 然后根据您的界面将一些使用基于rtti的库函数的异常重新抛出到外部 -

// Thrower.cc
void Rethrow(const std::exception& e) {
  throw e;
}

// LibraryNoRTTI.cc
namespace {

void internal_stuff() {
  throw std::logical_error("something goes wrong!");
}

}  // namespace

// You even may explicitly specify the thrown exceptions in declaration:
void f() throw(std::logical_error) {
  try {
    internal_stuff();
  }
  catch(std::exception& e) {
    Rethrow(std::logical_error(std::string("Internal error: ") + e.what());
  }
}

答案 1 :(得分:2)

开始编辑2014年3月4日
我认为Clang ++编译器有更好的机会获得所需的异常处理。我找到了这个Stack Overflow帖子:Clang and the default compiler in OS X Lion。该帖子提供了有用的脚本行,用于修改~/.bashrc以更改Snow Leopard上的系统默认编译器设置以及如何使用LLVM GCC。对于Clang,请在~/.bashrc

中添加
# Set Clang as the default compiler for the system
export CC=clang
export CFLAGS=-Qunused-arguments
export CPPFLAGS=-Qunused-arguments


如果c++符号链接不存在,可以直接调用clang ++或根据需要添加c ++链接(例如

ln -s /usr/bin/clang++ c++ 

)。最好通过运行:

来检查/usr/bin中的所有符号链接
ls -l `which lynx` | more


在我的Mavericks命令行工具安装c++指向clang++cc指向clangg++编译器版本说:

$ g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-   include-dir=/usr/include/c++/4.2.1
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.0.0
Thread model: posix


clang++编译器版本说:

$clang++ --version
Apple LLVM version 5.0 (clang-500.2.79) (based on LLVM 3.3svn)
Target: x86_64-apple-darwin13.0.0
Thread model: posix


请注意,g++包含目录路径设置为/usr/include/c++/4.2.1,可能不是解决问题所需的包含路径。

MacPorts:希望是任何OS X版本的答案
我可以找到任何OS X版本的任何Clang ++编译器版本的最佳解决方案是使用名为MacPorts的开源工具。 MacPorts Guide中有大量文档。该应用程序名为port,可以从OS X安装包安装,也可以获取源代码并在本地编译。以下是从安装MacPorts到Snow Leopard。其他OS X版本应该类似。获取Snow Leopard的MacPorts后,运行port search命令以观察所有可用的clang相关端口。例如,它看起来像这样:

$port search clang 

来自Snow Leopard 10.6.8的部分搜索结果列表是:

clang-2.9 @2.9_13 (lang)
    C, C++, Objective C and Objective C++ compiler

clang-3.0 @3.0_12 (lang)
    C, C++, Objective C and Objective C++ compiler

clang-3.1 @3.1_7 (lang)
    C, C++, Objective C and Objective C++ compiler

clang-3.2 @3.2_2 (lang)
    C, C++, Objective C and Objective C++ compiler

clang-3.3 @3.3_2 (lang)
    C, C++, Objective C and Objective C++ compiler

clang-3.4 @3.4 (lang)
    C, C++, Objective C and Objective C++ compiler

clang-3.5 @3.5-r202097 (lang)
    C, C++, Objective C and Objective C++ compiler

clang_select @0.1 (sysutils)
    common files for selecting default clang version


然后我成功安装了clang-3.3和sudo port install clang-3.3。完成此操作后,请键入port select --list clang以查看可用版本。然后运行

sudo port select --set clang mp-clang-3.3

或类似的。当我执行clang++ --version时,它(如预期的那样):

clang version 3.3 (tags/RELEASE_33/final)
Target: x86_64-apple-darwin10.8.0
Thread model: posix


与执行clang --version命令时相同(关闭并重新启动终端后):

clang version 3.3 (tags/RELEASE_33/final)
Target: x86_64-apple-darwin10.8.0
Thread model: posix

许多OS X版本都有MacPorts安装包(例如Leopard,Snow Leopard,Lion,Mountain Lion,Mavericks等)。我的搜索没有比Leopard更进一步。如果使用的是比Leopard更早的OS X,请仔细阅读MacPorts网站。

如果对于在哪里找到Xcode 4.2(或曾经能够获得它)的细节感到好奇,我发现这篇关于获取Snow Leopard Xcode 4.2 download for Snow Leopard的Xcode 4.2的帖子。然后是另外两个:Can i use the latest features of C++11 in XCode 4 or OSX Lion? [duplicate]Can I use C++11 with Xcode?。尝试了几个链接,看看4.2 Xcode是否仍然适用于Snow Leopard,没有任何乐趣。


为了获得完整的C ++ 11支持,很可能需要安装MacPorts libc ++。要安装更新版本,请执行sudo port install libcxx/usr/lib的内容将被当前的C ++ 11库覆盖(根据需要,按照MacPorts Ticket#42385:libcxx/libcxxabi: OS update can render system unusable

如果仍然缺少libc ++,请尝试:"libc++" C++ Standard Library。然后使用:

$ export TRIPLE=-apple-
$ export MACOSX_DEPLOYMENT_TARGET=10.6
$ ./buildit

来自How to build libc++ with LLVM/Clang 3.3 on Mac OS X 10.6 "Snow Leopard"

在OS X Lion,Mountain Lion和Mavericks上,他们最近都在Apple Developer网站上下载了独立的命令行工具。 Clang版本可能比您需要的版本旧,因此在使用Developer站点命令行工具'Clang时,请务必确认需要哪些C ++ 11功能。

结束编辑2014年3月4日

上述宏检测可能需要从__GNUC__更改为__clang____clang_version__。这完全取决于每个OS X编译器的预定义编译器宏是什么,以及这里根据需要检测的最佳方法。在What predefined macro can I use to detect clang?处的堆栈溢出答案应该有助于配置命令行以获取它们(例如clang++ -dM -E -x c /dev/null)。

我注意到在运行前面的示例命令时,有一个名为__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__的预定义宏。在小牛clang++上,宏值为1090。可能需要有一个#ifdef逻辑系列来为每个OS X clang ++编译器设置适当的EXPORT宏。

答案 2 :(得分:1)

好吧,即使我已经接受了答案,但并没有解决所有问题。所以我写下了最终有效的解决方案。

我制作了一个小工具,用于对目标文件进行后期处理,并将本地符号标记为UNDEF。这会强制链接器使用libstdc++中的定义,而不是文件中的本地定义。该工具的基本方法是:

  1. 加载Mach-O标题
  2. 执行load命令并找到LC_SYMTAB命令
  3. 加载符号列表(struct nlist)和字符串
  4. 走符号并寻找我们需要的符号(例如__ZTISt9bad_alloc
  5. 将找到的符号类型设置为N_UNDF|N_EXT
  6. 处理后,将修改后的符号表写回文件。
  7. (我也为ELF做了类似的实现)

    我对任何使用std异常的文件进行后处理,无论是用于抛出还是用于捕获。为确保文件列表不会过时,我使用nm为不需要的本地符号添加了链接后检查。

    这似乎解决了我迄今为止遇到的所有问题。