Видимость символов, исключения, ошибки времени выполнения

Я стараюсь лучше понять видимость символа. GCC Wiki (http://gcc.gnu.org/wiki/Visibility) имеет раздел «Проблемы с исключениями C ++». Согласно GCC Wiki возможно иметь время выполнения ошибка из-за неэкспортированных исключений. Ошибки во время выполнения без ошибок / предупреждений во время компиляции довольно опасны, поэтому я попытался лучше понять проблему. Я провел несколько экспериментов, но до сих пор не могу их воспроизвести. Есть идеи, как воспроизвести проблему?

В вики упоминаются три библиотеки, использующие друг друга, поэтому я создал три маленькие библиотеки.

Я запускаю следующие команды:

Класс исключения без vtable (работает как положено):

make
./dsouser

Класс исключения с vtable, но он не экспортируется (даже не компилируется):

make HAS_VIRTUAL=1

Исключенный класс экспортируется в vtable (работает как положено):

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

Спасибо,
Дани

19

Решение

Я являюсь автором оригинального патча для GCC, добавляющего поддержку видимости классов, и мое оригинальное руководство, которое клонировал GCC, находится на http://www.nedprod.com/programs/gccvisibility.html. Я благодарю VargaD за то, что он лично написал мне об этом вопросе.

Поведение, которое вы наблюдаете, действительно для последних GCC, однако это не всегда было так. Когда я в 2004 году исправил GCC, я отправил в GCC bugzilla запрос на выполнение среды обработки исключений GCC, чтобы сравнить выброшенные типы путем сравнения строк их искаженных символов вместо сравнения адреса из тех строк — это было отклонено в то время сопровождающими GCC как неприемлемые затраты времени выполнения, несмотря на то, что это поведение — то, что делает MSVC, и, несмотря на то, что производительность во время бросков исключений, как правило, не считается важной, поскольку они должны быть редкими. Следовательно, я должен был добавить конкретное исключение к своему руководству по видимости, чтобы сказать, что любой брошенный тип никогда не должен быть скрыт, ни разу, в двоичном виде, так как «скрытность» превосходит «по умолчанию», так что только одно объявление скрытого символа гарантирует переопределение всех случаев тот же символ в данном двоичном файле.

То, что произошло дальше, я полагаю, никто из нас не ожидал — KDE публично поддержал мою функцию. Это произошло за невероятно короткое время практически во всех крупных проектах с использованием GCC. Внезапно прятание символов стало нормой, а не исключением.

К сожалению, небольшое количество людей не применили мое руководство правильно к типам исключений, и постоянные сообщения об ошибках о некорректной обработке исключений между объектами в GCC в конечном итоге привели к тому, что сопровождающие GCC отказались, и спустя много лет исправление в сравнении строк для сопоставления брошенного типа, как я первоначально просил. Следовательно, в новых GCC ситуация несколько лучше. Я не изменил ни свое руководство, ни инструкции, потому что этот подход по-прежнему наиболее безопасен для каждого GCC начиная с версии 4.0, и хотя более новые GCC более надежны в обработке исключений из-за того, что теперь используется сравнение строк, соблюдение правил руководства не повредит тот.

Это приводит нас к проблеме typeinfo. Большая проблема в том, что передовая практика C ++ требует от вас всегда унаследовать фактически в бросаемых типах, потому что, если вы создаете два типа исключений, оба наследуют (скажем, от std :: exception), наличие двух равноотстоящих базовых классов std :: exception вызовет catch (std :: exception)&) для автоматического вызова terminate (), поскольку он не может определить, какой базовый класс сопоставить, поэтому у вас должен быть только один базовый класс std :: exception, и такое же обоснование применимо к любой возможной композиции бросаемого типа. Эта рекомендация особенно необходима в любой библиотеке C ++, потому что вы не можете знать, что сторонние пользователи будут делать с вашими типами исключений.

Другими словами, это означает, что все выброшенные типы исключений в наилучшей практике всегда будут иметь цепочку последовательных RTTI для каждого базового класса, и что сопоставление исключений теперь является случаем внутреннего успешного выполнения dynamic_cast<> для сопоставляемого типа — операция O (количество базовых классов). И для dynamic_cast<> Для работы над цепочкой виртуально унаследованных типов, как вы уже догадались, нужно каждый из этой цепочки, чтобы иметь видимость по умолчанию. Если хотя бы один из них скрыт от кода, выполняющего catch (), весь карамель не работает, и вы получаете terminate (). Мне было бы очень интересно, если бы вы переработали свой пример кода выше, чтобы виртуально наследовать и посмотреть, что произойдет — в одном из ваших комментариев говорится, что он отказывается ссылаться, и это здорово. Но допустим, что DLL A определяет тип A, подклассы DLL B типа A в B, подклассы DLL C типа B в C, и программа D пытается перехватить исключение типа A, когда выдается тип C. Программа D будет иметь доступную информацию о типе A, но при попытке получить RTTI для типов B и C может произойти сбой. Может быть, однако, недавние GCC также исправили это? Я не знаю, мое внимание в последние годы на clang, так как это будущее для всех компиляторов C ++.

Очевидно, что это беспорядок, но это беспорядок, специфичный для ELF — ничего из этого не влияет на PE или MachO, оба из которых правильно понимают все вышеперечисленное, не используя в первую очередь таблицы глобальных символов процесса. Однако исследовательская комиссия WG21 SG2 Modules, работающая над C ++ 17, должна эффективно реализовывать экспортированные шаблоны для модулей, чтобы работать для устранения нарушений ODR, и C ++ 17 — это первый предложенный стандарт, который я написал для LLVM в разум. Другими словами, компиляторы C ++ 17 должны будут выгружать сложный AST на диск, как это делает clang. И это подразумевает огромное увеличение гарантий того, что RTTI доступен, — действительно, именно поэтому у нас есть исследовательская группа SG7 Reflection, потому что AST из модулей C ++ обеспечивает огромное увеличение возможных возможностей саморефлексии. Другими словами, ожидайте, что вышеупомянутые проблемы скоро исчезнут с принятием C ++ 17.

Короче говоря, продолжайте следовать моему оригинальному руководству. И, надеюсь, все станет намного лучше в следующем десятилетии. И поблагодарить Apple за финансирование этого решения, он очень долго приходил из-за того, насколько он тяжел.

Найл

24

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]