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

UnixForum





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

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


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

2.2.5. Использование таблиц символов экспорта

Если по тем или иным причинам ни одно из предыдущих двух решения неприменимо, то следующая наилучшая возможность это проинструктировать компоновщик что-нибудь сделать. Известно, что эта возможность поддерживается только в компоновщиках GNU и Solaris, по крайней мере в рамках синтаксиса, представленного здесь. К таблицам символов экспорта можно обращаться не только для обсуждаемой здесь цели. Когда в главе 3 рассматривается поддержка интерфейсов API и ABI, используется точно такой же входной файл. Это не значит, что предыдущим двум методам не следует отдавать предпочтение. Напротив, таблицы символов экспорта (и таблицы символов) могут и должны всегда использоваться в дополнение к другим описываемым методам.

Концепция таблиц символов экспорта (export maps) состоит в том, чтобы явно указывать компоновщику, какие символы следует экспортировать в созданном объекте. Каждый символ может принадлежать к одному из двух классов: экспортируемых или неэкспортируемых символов. Символы могут перечисляться по-отдельности, можно использоваться выражения, выполняющие сравнение с образцом, или можно использовать специальный универсальный символ *, обобщающий в себе все образцы. Последний вариант используется только один раз. Файл таблицы символов для нашего примера кода, может выглядеть следующим образом:

{
   global: index;
   local: *;
};

Здесь компоновщику сообщается, что символ index должен экспортироваться, а все остальные (помеченные как *) являются локальными. Мы в списке local: могли бы перечислить символы last и next:, но для того, чтобы пометить все, что явно не упоминается как локальные символы, рекомендуется, как правило, всегда пользоваться универсальным символом *. Это позволит избежать неожиданностей, разрешив доступ только к тем символам, которые упоминаются явно. В противном случае также может возникнуть проблема с символами, которые не помечены ни как global:, ни как local:, что в результате может привести к неопределенности в поведении. Другой вариант неопределенного поведения возможен в случае, если имя фигурирует в обоих списках, либо в обоих списках находится совпадение с образцом.

Чтобы с помощью этого метода сгенерировать объект DSO, пользователь должен с помощью параметра –version-script передать в компоновщик имя файла таблиц символов. Из названия параметра предполагается, что, кроме того, можно использовать скрипты. Мы вернемся к этому, когда в следующей главе будем обсуждать интерфейсы ABI.

$ gcc -shared -o foo.so foo.c -fPIC \
   -Wl,--version-script=foo.map

Предполагается, что в файле foo.map находится текст, подобный тому, что был приведен ранее.

Конечно, также можно использовать таблицы символов экспорта с кодом C++. В этом случае есть две возможности: явно указывать символы, используя их трансформированные имена, или понадеяться на сравнение трансформированных имен с образцом. Использование транформированных имен использовать достаточно просто. Просто используйте идентификаторы, такие как в примерах языка C. Использование запрошенные имен потребует поддержки в компоновщике. Предположим, что в файле определены следующие функции:

int foo (int a) { ... }
int bar (int a) { ... }
struct baz {
   baz (int);
   int r () const;
   int s (int);
};

Объект DSO, содержащий определения для всех этих функций — членов класса, должен экспортировать только функцию foo и деструктор (ы) для baz и baz::s. Таблица символов экспорта, которая предназначена для этого, может выглядеть следующим образом:

{
   global:
	extern "C++" {
	   foo*;
	   baz::baz*;
	   baz::s*
	};
   local: *;
};

Использование extern "C++" указывает компоновщику найти соответствие следующих шаблонов с запрашиваемыми именами C++. Первая запись foo* соответствует первой глобальной функции в этом примере. Вторая запись соответствует конструктору (конструкторам) baz, а третья запись соответствует функции baz::s. Обратите внимание, что во всех случаях используются образцы. Это необходимо, поскольку foo, baz::baz и baz::s не являются полными именами. Параметр функции также закодирован и его следует искать при помощи сравнения с образцом. Зпрашиваемые имена C++ нельзя найти по полному совпадению, поскольку в текущей реализации компоновщика нельзя использовать не алфавитно-цифровые символы. Использования образца может иметь нежелательные последствия. Если в baz есть еще одна функция- член класса, начинающаяся с буквы "s", то она также будет экспортирована. И нужно отметить еще одну последнюю странность: в настоящее время компоновщик требует, чтобы после последней записи в блоке C++ не было точки с запятой.

Оказывается, использование таблиц символов экспорта очень подходящее решение. Исходный код не должен стать менее читаемым из-за использования объявлений атрибутов или, по существу, директив pragma. Все сведения об интерфейсе ABI хранятся локально в файле таблиц символов экспорта. Но в этом процессе есть одна фундаментальная проблема: именно потому, что исходный код не был изменен, окончательный код не будет оптимальным. Компоновщик используется только после того, как компилятор уже выполнил свою работу и сгенерированный код уже не может быть существенно оптимизирован.

В нашем текущем примере компилятор должен генерировать код для функции next с учетом наихудшего сценария, предполагающего, что переменная last экспортируется. Это означает, что нельзя генерировать последовательность кода, в котором используется @GOTOFF, о чем говорилось ранее. Вместо этого должны быть сгенерирована обычная последовательность из двух команд, использующая @GOT.

Это то, что компоновщик увидит, когда получит указание скрыть символ last. Компоновщик не будет иметь дело с реальным кодом. Более простой код, который здесь используется, потребует более сложного анализа последующего кода, который в теории возможен, но не реализован. Но компоновщик не будет генерировать обычной запись о перемещении R 386 GLOB DAT. Поскольку символ не экспортируется, вмешательство не допустимо. Положение локального определения относительно началаобъекта DSO известно, и поэтому компоновщик будет генерировать относительное перемещение.

Что касается вызовов функции, то результат часто настолько хорош, насколько это получится. Код, генерируемый компилятором для перехода относительно регистра PC и для перехода через таблицу PLT, идентичен. Разница лишь в том, что этот код вызывает (целевую функцию или код в таблице PLT). Код не оптимален только в одном случае: если вызов функции является единственной причиной загрузки регистра PIC. Для вызова локальной функции в этом нет необходимости и загрузка PIC является просто пустой тратой времени и кода.

Итак, что касается переменных, то использование таблиц символов создает более или менее эффективный код, добавляя запись в таблицу GOT и добавляя относительное перемещение. Что касается функций, то сгенерированный код иногда содержит ненужную загрузку регистра PIC. Одно обычное перемещение преобразуется в относительное перемещение и одна запись в таблице PLT удаляется. Это одно относительное перемещение хуже, чем предыдущие методы. Эти недостатки являются причиной того, что гораздо предпочтительнее сообщить компилятору, что происходит, поскольку после того, как компилятор завершит свою работу определенные решения отменить уже не удастся.


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