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

Igo*_*sky 18 c++ macos exception shared-libraries rtti

问题与此问题有些类似,但接受的答案并未真正提出解决方案或解决方法.

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

(在你大喊"异常需要RTTI所以你必须有它!",请注意,必要的例外RTTI总是产生不管-frtti-fno-rtti设置.这实际上是在记录-fno-rtti标志的说明.在OS X上的问题是,它不是以相同的方式生成的)

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

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

由于异常处理代码依赖于比较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
Run Code Online (Sandbox Code Playgroud)

运行:

$ ./test
trying lib->main exception
before throw
caught ... in main - bad!
Run Code Online (Sandbox Code Playgroud)

涉及的文件的符号:

$ 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++)
Run Code Online (Sandbox Code Playgroud)

注意: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();
}
Run Code Online (Sandbox Code Playgroud)

该函数__throw_bad_alloc是从中导入的libstdc++.6.dylib,因此始终抛出正确的类型.

aby*_*s.7 6

您可以简单地将所有“抛出异常”基础设施移动到-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;
}
Run Code Online (Sandbox Code Playgroud)

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

如果封装(私有异常类)有问题,那么你可能会与抛出函数交朋友。


如果您仍然想在非 rtti 库中使用 ( throw/ ) 异常,那么您必须将内部异常和库 API 中使用的异常分开。catch

好的方法是使用本机 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());
  }
}
Run Code Online (Sandbox Code Playgroud)


Igo*_*sky 2

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

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

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

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

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

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