Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Программирование и алгоритмические языки


Ulrich Drepper "Как писать разделяемые библиотеки"
Назад Оглавление Вперед

2.2.7. Избегайте использовать экспортируемые символы

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

Так как символ нельзя одновременно экспортировать и не экспортировать, основной подход заключается в использовании двух имен для одной и той же переменной или функции. Тогда два имени можно трактовать по-разному. Есть несколько возможностей для создания двух имен, различающихся по эффективности и усилиям.

В этом месте нужно сделать предупреждение. Из-за того, что выполняется такая оптимизация, семантика программы изменяется, поскольку оптимизация влияет на правила поиска символов. Теперь для заданного имени в программе можно использовать более одного символа. Код, находящийся не объекте DSO, может найти определение символа где-нибудь еще, а код в DSO всегда использует локальные определения. Это может привести к смешным результатам. Часто это допустимо, поскольку не разрешается использовать несколько определений. Дело в том, что из-за этого может быть нарушено одно правило ISO C. Стандарт ISO C гласит, что функции идентифицируются по их именам (идентификаторам) и что сравнение указателей функций можно использовать в качестве проверки на равенство. В реализации ELF затрачиваются большие усилия с тем, чтобы обеспечить, чтобы это правило обычно соблюдалось. Когда требуется использовать код локальных символов внутри и вне объекта DSO, то для заданного имени можно найти различные определения и поэтому указатели не совпадают. Прежде, чем выполнять эту оптимизацию, важно всегда учитывать такие побочные эффекты.

К функциям применяется методика Wrapper Functions Only — создаются функции-оболочки (т. е. альтернативные точки входа) , что является наиболее гибким, но также наиболее затратным способом решить эту проблему. Если в нашем примере, мы хотели бы экспортировать index, а также next, мы бы могли использовать следующий код:

static int last;

static int next_int (void) {
   return ++last;
}

int next (void) {
   return next_int ();
}

int index (int scale) {
   return next_int () << scale;
}

Функция next сейчас просто обертка вокруг next int. Все вызовы next int распознаются компилятором как вызовы локальной функции, посколку next int, в отличии от next, определена как static. Поэтому для вызова в index записи PLT не используются.

Недостатком этого метода является то, что требуется дополнительный код (код для новой функции next), а также вызов next, как минимум, медленнее, чем нужно во время выполнения. В случае, если другие методы не работают, то в качестве запасного варианта это лучше, чем ничего.

Если воспользоваться методикой добавления алиасов Aliases Introducing, то можно с помощью создания алиасов для существующих объектов получить два имени без добавления кода. В gcc есть поддержка этой методики; gcc не только созданиет алиасы, он также знает тип алиаса и, когда алиас используется, может выполнять соответствующие проверки. Здесь задачей является создать алиас и сообщить gcc и/или компоновщику о том, что символ экспортировать не нужно. То есть, мы применяем те же самые методы, описанные в предыдущих разделах, но теперь для алиаса. Различие лишь в том, что определение алиаса как static, работать не будет. Поэтому лучшим способом является использование атрибутов видимости. Также будут работать другие ранее рассмотренные методы, но мы не будет здесь их подробно рассматривать.

Если в нашем примере мы хотим экспортировать last и next, то мы можем переписать пример следующим образом:

int last;
extern __typeof (last) last_int
   __attribute ((alias ("last"),
	visibility ("hidden")));

int next (void) {
   return ++last_int;
}
extern __typeof (next) next_int
   __attribute ((alias ("next"),
	visibility ("hidden")));

int index (int scale) {
   return next_int () << scale;
}

Это целая коллекция нестандартных расширений gcc для языка C, так что, возможно, потребуется некоторое объяснение. Фактические определения всех трех объектов таких же, как в исходном коде. Все эти объекты экспортируются. Различие в определениях в том, что next использует внутренний алиас last int вместо last и аналогично для index и next. То, что выглядит как два объявления, является механизмом, посредством которого gcc сообщают об алиасах. Это в основном объявление extern для объекта того же самого типа (мы используем здесь typeof для того, чтобы это обеспечить), у которого есть добавленный алиас. Атрибут alias именует объект как алиас.

Что достичь таких результатов, которые мы хотим, а именно, чтобы алиасы не экспортировались и чтобы gcc получал сообщение об этом, мы должны добавить атрибут видимости hidden. Если снова заглянуть в разделы 2.2.2 и 2.2.3, то должно стать ясно, что использование этого атрибута является эквивалентным.

Если по какой-то причине атрибуты видимости использовать нельзя, то необходимо использовать почти такой же код, но только нужно удалить

, visibility ("hidden")

Это позволит создать нормальный алиас с той же областью видимости, что и оригинальный символ. Затем алиас можно скрыть с помощью экспорта таблиц символов. Результирующий двоичный модуль не будет использовать эффективную последовательность кода (смотрите раздел 2.2.5), но всегда будет использоваться локальное определение.

Внимательный читатель может решить, что, может быть, можно избежать некоторых сложностей, если для next написать следующий код:

static int next_int (void) {
   return ++last_int;
}
extern __typeof (next_int) next
   __attribute ((alias ("next_int")));

Определение next int, как определение static, не экспортируется и его можно записать как inline, а next определяется как extern и, следовательно, экспортируется. Даже если это иногда работает, нет никаких гарантий, что это будет работать всегда. Компилятору разрешается в качестве функций и переменных вида static использовать произвольные имена символов, поскольку имена не являются частью интерфейса ABI объектного файла. Это нужно в некоторых случаях для того, чтобы избежать конфликтов имен. Результатом является то, что часть alias("next int") может не обеспечить создание правильного названия символа и, следовательно, определение алиаса указать не удастся. По этой причине обязательно создание алиасов только для нестатических функций и переменных.

Рассмотрим, как в программах на языке C++ определения алиасов зависят от имен. Проблема в том, что атрибут alias требует, чтобы ассемблерное имя определяемого символа было строковым параметром. Для кода на языке C++ это означает трансформирование имени. Для простой функции C++ мы можем справиться с этим с помощью такого же трюка, который использовался в примере на языке C.

int
add (int a, int b)
{
   return a + b;
}
extern __typeof (add) add_int
  __attribute ((alias ("_Z3addii"),
	visibility ("hidden")));

Здесь только два трюка. Первый — поиск правильного трансформированного имени. Для локально используемого компилятора имя определить достаточно просто — нужно просто скомпилировать код без определения алиаса и посмотреть в таблицу символов созданного файла. К сожалению трансформирование имен традиционно не очень хорошо стандартизировано. Есть несколько различных схем трансформирования имен, что означает, что строка алиаса должна быть скорректирована в соответствие с компилятором, использованным при компиляции.

Второй проблемой является использование typeof в случае, если имя функции перегружается. В этом случае компилятор не знает, насколько много потенциально может быть версий функций и здесь выручает следующее.

DF SYMBOLIC Разработчики, которые начинали разработку формата ELF, предполагали, что более предпочтительным может оказаться использование локальных определений. Они добавили механизм, который помогает в этом. Если в записи DT FLAGS в динамической секции установлен флаг DF SYMBOLIC (или для более старых двоичных файлов ELF: если в динамической секции есть запись DT SYMBOLIC), то динамический компоновщик должен отдавать предпочтение использованию локальных определений.

Такой подход имеет многочисленные недостатки. Во-первых, он влияет на все интерфейсы. В других подходах, описанных здесь, есть разграничение интерфейсов. Обработка всех интерфейсов, как в этом подходе, обычно считается неправильным. Вторым недостатком является то, что компилятор не получает информацию об использовании локальных символов и, следовательно, не может оптимизировать их использование как случае использования таблиц символов. И, что еще хуже, вызовы локальных функций по-прежнему используют записи таблицы PLT. Записи таблиц PLT и GOT по-прежнему создаются и переход является косвенным. В некоторых ситуациях это может быть полезным (например, при использовании LD PROFILE), но обычно это означает большое количество упущенных возможностей оптимизации.

Наконец, третья проблема в том, что область поиска изменяется таким образом, что это может привести к непредсказуемому использованию зависимостей. Для обеспечения эффективности DF SYMBOLIC помещает свой собственный объект в начало своей области поиска, поскольку есть ряд других объектов DSO, поиск которых осуществляется перед поиском зависимостей. В этом нет ничего нового, но тот факт, что объект DSO, помеченный как DF SYMBOLIC, расположен в необычном месте, может привести к тому, что будут выбраны неожиданные версии.

Мы здесь советуем никогда не использовать DF SYMBOLIC. Это не улучшает код, заставляя, чтобы все символы обрабатывались одинаково, и может вызвать проблемы при поиске символов. Здесь этот метод упоминается лишь для полноты картины и в качестве предупреждения.


Предыдущий раздел:   Следующий раздел:
Назад Оглавление Вперед