Подтвердить что ты не робот

Проблемы с бросанием и улавливанием исключений в OS X с -fno-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) импортирует символ типаinfo из libstdc++.6.dylib и не имеет локальной копии

Поскольку код обработки исключений основан на сравнении указателей типаinfo для определения соответствия исключения, соответствие не выполняется, и только 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 и поэтому всегда выдает правильный тип.

4b9b3361

Ответ 1

Хорошо, хотя я принял ответ, он не решает всех проблем. Поэтому я записываю решение, которое действительно работает в конце.

Я сделал небольшой инструмент для последующей обработки объектных файлов и маркировки локальных символов как UNDEF. Это заставляет компоновщик использовать определения из libstdc++, а не локальные из файла. Основной подход инструмента:

  • загрузить заголовок Mach-O
  • Пройдите команды загрузки и найдите команду LC_SYMTAB
  • загрузите список символов (struct nlist) и строки
  • пройдите по символам и найдите те, которые нам нужны (например, __ZTISt9bad_alloc)
  • установите тип найденных символов на N_UNDF|N_EXT.
  • после обработки, напишите измененную таблицу символов обратно в файл.

(Я также сделал аналогичную реализацию для ELF)

Я отправляю после обработки любой файл, использующий исключения std, либо для броска, либо для ловли. Чтобы убедиться, что список файлов не устарел, я добавил проверку ссылок на нежелательные локальные символы, используя nm.

Это, кажется, разрешает все проблемы, которые у меня были до сих пор.

Ответ 2

Вы можете просто переместить всю свою инфраструктуру "исключений" в вспомогательную библиотеку с включенным -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) { ... }

Если есть проблема с инкапсуляцией (частные классы исключений), вы можете подружиться с металическими функциями.


Если вы все еще хотите использовать (throw/catch) исключения внутри библиотеки non-rtti, вам необходимо сделать разницу между внутренними исключениями и исключениями, используемыми в вашем API-интерфейсе библиотеки.

Хорошим способом является использование встроенного С++ 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());
  }
}

Ответ 3

Начать редактирование 4 марта 2014 года
Я думаю, что компилятор Clang++ имеет больше шансов получить желаемую обработку исключений. Я нашел этот пост: Clang и компилятор по умолчанию в OS X Lion. Сообщение имеет полезные script строки для изменения ~/.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++ напрямую, либо добавляйте ссылку С++ по желанию (например,

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

). Рекомендуется проверить все символические ссылки в /usr/bin, запустив:

ls -l `which lynx` | more


На моей установке инструментов командной строки Mavericks c++ указывает на clang++ и cc указывает на clang. В версии компилятора g++ говорится:

$ 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++ complier version:

$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
Лучшее решение, которое я могу найти для получения любой версии компилятора Clang++ для любой версии OS X, - это использовать инструмент с открытым исходным кодом под названием MacPorts. Существует обширная документация в Руководство MacPorts. Приложение называется port и может быть установлено из установочного пакета OS X или получить исходный код и скомпилировать локально. Ниже приведена установка MacPorts на Snow Leopard. Другие версии OS X должны быть похожими. После получения MacPorts для Snow Leopard запустите команду поиска порта, чтобы наблюдать за всеми доступными портами, связанными с 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

Существуют пакеты установки MacPorts для многих версий OS X (например, Leopard, Snow Leopard, Lion, Mountain Lion, Mavericks и т.д.). Я не пошел дальше, чем Леопард, со своим поиском. Если вы используете OS X старше Leopard, внимательно ознакомьтесь с сайтом MacPorts.


Если вам интересно узнать, где найти Xcode 4.2 (или использовать его для получения), я нашел это сообщение относительно получения Xcode 4.2 для Snow Leopard Xcode 4.2 скачать для Snow Leopard. Затем эти два дополнительных: Могу ли я использовать последние возможности С++ 11 в XCode 4 или OSX Lion? [duplicate] и Можно ли использовать С++ 11 с Xcode?. Попробовав пару ссылок, чтобы узнать, доступен ли Xcode 4.2 для Snow Leopard, нет радости.


Скорее всего, потребуется установка MacPorts libС++ для полной поддержки С++ 11. Чтобы установить более новую версию, выполните sudo port install libcxx. Содержимое /usr/lib будет перезаписано текущими библиотеками С++ 11 (по необходимости на билет MacPorts # 42385: libcxx/libcxxabi: обновление ОС может сделать систему непригодной

Если libС++ по-прежнему отсутствует, попробуйте следующее: "libС++" Стандартная библиотека С++. Затем используйте это:

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

from Как создать libС++ с LLVM/Clang 3.3 в Mac OS X 10.6 "Snow Leopard" .

В OS X Lion, Mountain Lion и Mavericks у всех из них есть последние независимые загрузки инструментов командной строки на сайте Apple Developer. Версия Clang может быть старше, чем требуется, поэтому обязательно проверьте, какие функции С++ 11 необходимы при использовании инструментов командной строки сайта Developer Clang.

End Edit 4 марта 2014 года

Вышеуказанное обнаружение макросов может потребоваться изменить с __GNUC__ на __clang__ или __clang_version__. Все зависит от того, какие предопределенные макросы компилятора для каждого компилятора OS X, и лучший способ обнаружить по мере необходимости здесь. Ответ на переполнение стека: Какой предопределенный макрос я могу использовать для обнаружения clang?, должен быть полезен при настройке командной строки для их получения (например, clang++ -dM -E -x c /dev/null).

Я заметил при запуске предыдущей команды примера, что существует предопределенный макрос с именем __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__. На Mavericks clang++ значение макроса 1090. Возможно, потребуется семейство логики #ifdef для установки соответствующего макроса EXPORT для каждого компилятора OS X clang++.