【问题标题】:Problems throwing and catching exceptions on OS X with -fno-rtti使用 -fno-rtti 在 OS X 上引发和捕获异常的问题
【发布时间】:2014-03-11 07:48:53
【问题描述】:

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

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

(在您大喊“例外需要 RTTI,因此您必须启用它!”之前,请注意,无论-frtti-fno-rtti 设置,无论,总是会生成例外所需的 RTTI。这实际上记录在-fno-rtti 标志描述中。OS X 上的问题是它不是以相同的方式生成的)

经过一番调查,发现如下:

  • 在dylib (-fno-rtti) 中,有异常的RTTI 结构的本地副本;特别是 __ZTISt9bad_alloc 符号 (typeinfo for std::bad_alloc)。
  • exe (-frtti) 从 libstdc++.6.dylib 导入 typeinfo 符号,并且没有本地副本

由于异常处理代码依赖于比较typeinfo指针来判断异常匹配,匹配失败,只有catch(...)匹配成功。

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

1) 使用-frtti 编译所有内容,或者至少编译抛出和捕获异常的文件。这是可行的,但我不喜欢为所有内容生成 RTTI,即使我们不使用它;并且处理异常的文件列表很容易过时。

2) 链接 dylib 时,以某种方式使链接器丢弃目标文件中的弱异常定义并使用来自libstdc++.6.dylib 的异常定义。到目前为止,我还没有成功。

3) ???

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

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

跑步:

$ ./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++)

注意:Linux 和 Windows 上的类似编译选项可以正常工作。我可以从共享对象/dll 中抛出异常并在主 exe 中捕获它们,即使它们是使用不同的 -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();
}

函数__throw_bad_alloc 是从libstdc++.6.dylib 导入的,因此总是抛出正确的类型。

【问题讨论】:

  • (1) 如果您有办法为您不需要的所有内容(例如依赖于平台的属性)压制 rtti,则至少是可行的,或以上述方式打开它。有趣的问题。由于这些原因,我避免在 dylib 之间抛出异常,但这可能不是你的选择。
  • 会不会是-fvisibility 标志导致了这种行为?
  • 你能用-fno-rtti 编译包含throw 语句的代码吗?我的 GCC(诚然不是 OSX,但仍然)根本不允许我这样做(立即因错误而退出),这让我很伤心。此外,它为任何翻译单元中的每个类型添加 RTTI,无论该类型是否被抛出。
  • @Damon:是的,它适用于我在 Linux 上的 GCC 4.2 和 4.4 中(目前没有任何更新的要测试)。
  • 我不明白为什么throw 在有 strong 时会在libThrow.dylib 中使用 weak __ZTISt9bad_alloc 符号在libstdc++.dylib 中使用它的版本(尽管间接来自libstdc++abi),catch 可能正在使用它。这对我来说似乎是ld 的“错误”(或至少是“怪癖”)。这似乎是怎么回事?

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


【解决方案1】:

您可以简单地将所有“抛出异常”基础架构移动到启用 -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&amp; 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());
  }
}

【讨论】:

  • 谢谢。看来我们将不得不采用这种方法。对于bad_alloc 的具体情况,我将使用更优雅的解决方案来编辑​​我的帖子。
  • 但是,现在我们遇到了相反的问题:dylib 本身内部的catch 语句无法捕获异常:/
【解决方案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 后,运行端口搜索命令以观察所有不同的 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 版本(例如 Leopard、Snow Leopard、Lion、Mountain Lion、Mavericks 等)都有 MacPorts 安装包。在搜索过程中,我没有比 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 之后,不高兴。


MacPorts libc++ 安装很可能需要完整的 C++11 支持。要安装更新的版本,请执行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 版本可能比需要的版本旧,因此在使用开发者站点命令行工具的 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 宏。

【讨论】:

  • 切换到 clang++ (Apple clang version 3.0 (tags/Apple/clang-211.10.1) (based on LLVM 3.0svn)) 不起作用。是的,它在 10.6 上不可用,所以我们无论如何也不能使用它。
  • 我不太相信有一个简单的 10.6 解决方案。今天完成任务后,我将运行我的 Snow Leopard 系统并进一步研究。我想找到一个解决所有 OS X 版本的答案。当我找到一个有希望的解决方案时,我会更新和/或重新发布。
  • @IgorSkochinsky 有关如何使用 MacPorts 为任何 OS X 版本获取任何 Clang 版本的详细信息,请参见上文。以雪豹为例。
  • 嗯,这是一个很好的指南,但正如我所提到的,切换到 clang 并不能解决问题。对于-fno-rtti,它的行为仍然相同。
  • @IgorSkochinsky 是的,自从 abyss.7 发布以来,我还没有花时间尝试更多的编译标志和链接。 abyss.7 的更新解决方案确实解决了这个问题,对吧?
【解决方案3】:

好吧,即使我接受了一个答案,它也没有解决所有问题。所以我正在写下最终确实有效的解决方案。

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

  1. 加载 Mach-O 标头
  2. 遍历加载命令并找到LC_SYMTAB 命令
  3. 加载符号列表 (struct nlist) 和字符串
  4. 遍历符号并寻找我们需要的符号(例如__ZTISt9bad_alloc
  5. 将找到的符号类型设置为N_UNDF|N_EXT
  6. 处理后,将修改后的符号表写回文件中。

(我也为ELF做了类似的实现)

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

这似乎解决了我目前遇到的所有问题。

【讨论】:

    猜你喜欢
    • 2011-04-07
    • 2021-03-27
    • 1970-01-01
    • 1970-01-01
    • 2011-03-16
    • 2015-11-13
    • 2012-10-28
    • 2012-12-26
    • 2013-11-10
    相关资源
    最近更新 更多