Содержание


Управление видимостью символических имен для общих библиотек

часть 1 - введение

Comments

Серия контента:

Этот контент является частью # из серии # статей: Управление видимостью символических имен для общих библиотек

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Управление видимостью символических имен для общих библиотек

Следите за выходом новых статей этой серии.

Что такое символьные имена и их видимость

Символическое имя (символ) – это одно из основных понятий, когда речь идет об объектных файлах, компоновке кода и т. д. Фактически в языке C/C++ символ является соответствующим объектом для большинства пользовательских переменных, имен функций, декорированных в пространстве имен, типов class/struct/name и т. д. Например, компилятор C/C++ может генерировать символы внутри объектного файла в случаях, когда были определены нестатические глобальные переменные или нестатические функции, позволяющие компоновщику решать, будут ли одни и те же данные или программный код использоваться в различных модулях (объектных файлах, динамических общих библиотеках, исполняемых файлах) или нет.

Хотя в различных модулях можно совместно использовать как переменные, так и функции, в объектных файлах чаще встречается определение области видимости переменных. Например, можно объявить переменную в файле a.c так:

extern int shared_var;

В файле b.c объявим эту же переменную так:

int shared_var;

Теперь после компиляции символическое имя shared_var будет присутствовать в объектах a.o и b.o, а после компоновки символическое имя объекта a.o и символическое имя объекта b.o могут иметь общий адрес. Тем не менее разработчики редко создают переменные, которые могут использоваться несколькими общими библиотеками или исполняемыми файлами. Как правило, общими для нескольких программных модулей делаются только функции. Иногда такие функции называются API-интерфейсами, поскольку предполагается, что содержащий их модуль должен предоставлять доступ к ним из других модулей. Такие символьные имена также называются экспортируемыми, поскольку они доступны из других программных модулей. Заметим, что такая видимость обеспечивается только во время динамической компоновки, поскольку общие библиотеки обычно загружаются как часть образа памяти во время запуска программы. Таким образом, видимость символов становится атрибутом всех глобальных символов при динамической компоновке.

Почему необходимо управлять видимостью символических имен

На различных платформах компилятор XL C/C++ может работать по разному: он может делать символы всех модулей либо экспортируемыми, либо не экспортируемыми. Например, при создании общих библиотек в ELF-формате (Executable and Linking Format) на платформе IBM PowerLinux™ по умолчанию все символы экспортируются. При создании XCOFF-библиотек в AIX на платформе POWER текущая версия компилятора XL C/C++ может не экспортировать символы, если не используются дополнительные инструменты. При этом разработчики кода могут воспользоваться другими способами, чтобы сделать видимым каждый конкретный символ индивидуально (об этом мы расскажем во второй статье серии). Однако обычно не рекомендуется экспортировать все символьные имена, содержащиеся в программных модулях. Можно экспортировать только необходимые символы. Это не только повысит безопасность библиотеки, но уменьшит время ее динамической компоновки.

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

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

Вышеперечисленные недостатки объясняют, почему необходимо определять видимость символьных имен. В этой статье мы покажем, как управлять символьными именами динамических общих объектов (DSO). Эту проблему можно решить по-разному, и мы подскажем вам, какой из способов лучше подходит для каждой платформы.

Методы управления видимостью символьных имен

В этом разделе мы будем использовать следующий фрагмент кода на языке C++:

Листинг 1. a.C
int myintvar = 5;

int func0 () {
  return ++myintvar;
}

int func1 (int i) {
  return func0() * i;
}

В файле a.C мы определили переменную с именем myintvar и две функции с именами func0 и func1. По умолчанию при создании общей библиотеки на AIX-платформе компилятор и компоновщик с инструментом CreateExportList сделают все три символа видимыми. Это можно проверить, обратившись к таблице символьных имен загрузчика при помощи утилиты dump:

$ xlC -qpic a.C -qmkshrobj -o libtest.a
$ dump -Tv libtest.a

                        ***Loader Symbol Table Information***
[Index]      Value      Scn     IMEX Sclass   Type           IMPid Name

[0]     0x20000280    .data      EXP     RW SECdef        [noIMid] myintvar
[1]     0x20000284    .data      EXP     DS SECdef        [noIMid] func0__Fv
[2]     0x20000290    .data      EXP     DS SECdef        [noIMid] func1__Fi

Здесь значение “EXP” означает, что символьное имя является “экспортируемым” (exported); имена функций func0 и func1 декорированы в соответствии с правилами декорирования C++ (о чем нетрудно догадаться). Параметр -T команды dump выводит на экран содержимое таблицы символьных имен загрузчика (Loader Symbol Table Information), которая должна быть использована динамическим компоновщиком. В данном примере были экспортированы все символьные имена файла a.C. Однако разработчик библиотеки может решить экспортировать только функцию func1. Глобальное символьное имя myintvar и функция func0 могут сохранять или изменять только свое внутреннее состояние или просто использоваться локально. Таким образом, разработчику важно сделать их невидимыми.

Для решения этой задачи существует по крайней мере три способа: использование ключевого слова static, определение атрибутов видимости для компилятора GNU и использование таблиц экспорта. Каждый из них обладает своими уникальными особенностями и возможными недостатками. Перейдем к рассмотрению всех трех методов.

1. Использование ключевого слова static

Ключевое слово static в языке C/C++ может являться перегруженным ключевым словом, поскольку оно может обозначать как область действия, так и класс памяти переменной. Можно сказать, что для области действия это ключевое слово отключает внешнее связывание для символьного имени. Это означает, что для символьного имени с ключевым словом static никогда не будет выполняться связывание, поскольку компилятор не оставляет для компоновщика никакой информации о нем. Этот метод реализуется на уровне языка программирования и является самым простым способом скрыть символьное имя.

Добавим ключевое слово static в предыдущий пример:

Листинг 2. b.C
static int myintvar = 5;

static int func0 () {
  return ++myintvar;
}

int func1 (int i) {
  return func0() * i;
}

Теперь если мы посмотрим в таблицу символьных имен загрузчика после создания общей библиотеки, то увидим ожидаемый результат:

$ xlC -qpic a.C -qmkshrobj -o libtest.a
$ dump -Tv libtest.a

                        ***Loader Symbol Table Information***
[Index]      Value      Scn     IMEX Sclass   Type           IMPid Name

[0]     0x20000284    .data      EXP     DS SECdef        [noIMid] func1__Fi

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

extern int myintvar;

а затем с помощью файла b.C попытаться создать библиотеку libtest.a из объектов a.o и b.o, то компоновщик выдаст ошибку о невозможности связывания переменной myintvar, определенной в файле b.C, поскольку ее определение не найдено где-либо еще. Это препятствует совместному использованию данных или кода в пределах одного модуля, что обычно требуется разработчику. Таким образом, данный метод является скорее средством управления видимостью переменных и функций внутри файла, нежели средством управления видимостью низкоуровневых символьных имен. На практике большинство разработчиков не используют ключевое слово static для управления видимостью символьных имен, поэтому перейдем к рассмотрению второго метода.

2. Определение атрибутов видимости (только для компилятора GNU)

Следующий метод управления видимостью символов заключается в использовании атрибута видимости. Этот атрибут устанавливается при помощи двоичного интерфейса приложений (Application Binary Interface, ABI) ELF-формата. Вообще этот интерфейс определяет четыре класса, но в большинстве случаев широко используются только два из них:

  • STV_DEFAULT – указывает, что символы являются экспортируемыми, т. е. видимы везде.
  • STV_HIDDEN – указывает, что символы не являются экспортируемыми и не могут использоваться в других объектах.

Обратите внимание на то, что этот ABI-интерфейс является расширением компилятора GNU C/C++. В настоящий момент пользователи решений PowerLinux могут использовать его в качестве атрибута GNU для символьных имен. Рассмотрим пример для этого случая:

int myintvar __attribute__ ((visibility ("hidden")));
int __attribute__ ((visibility ("hidden"))) func0 () {
  return ++myintvar;
}
...

Чтобы определить атрибут GNU, необходимо использовать конструкцию __attribute__ и параметр, заключенный в двойные круглые скобки. Чтобы скрыть символьные имена, можно указать значение visibility(“hidden”). В нашем примере мы сделали это для переменной myintvar и функции func0. В результате их нельзя будет экспортировать в библиотеку, но можно использовать в исходных файлах. Фактически скрытые символы будут отсутствовать в динамической таблице символов, но будут присутствовать в таблице символов, предназначенной для статического связывания. Этот строго определенный алгоритм работы определенно может решить нашу задачу. Очевидно, что этот метод лучше метода с использованием ключевого слова static.

Обратите внимание на то, что если для переменной задан атрибут visibility, то ее объявление при помощи ключевого слова static может привести к ошибке компилятора, который выдаст соответствующее предупреждение.

Также в интерфейсе ELF ABI определены следующие режимы видимости:

  • STV_PROTECTED: символ является видимым за пределами текущего исполняемого модуля или общего объекта, но не может быть замещен. Другими словами, если к защищенному (protected) символу общей библиотеки выполняется обращение из внешнего кода, то этот код будет всегда ссылаться на символ общей библиотеки даже если в исполняемом файле был определен символ с таким же именем.
  • STV_INTERNAL: символ не доступен за пределами текущего исполняемого файла или общей библиотеки.

Заметим, что в настоящее время этот метод не поддерживается компилятором XL C/C++ даже на платформе PowerLinux. К счастью, есть еще один выход из ситуации.

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

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

Таблица экспорта может быть автоматически сгенерирована компилятором (или сторонними инструментами, например, CreateExportlist) в момент создания общей библиотеки либо написана разработчиком вручную. При помощи специального параметра таблица экспорта передается компоновщику в виде входных данных. Однако поскольку всю основную работу выполняет драйвер компилятора, разработчики редко уделяют внимание детальной настройке опций.

Принцип работы таблиц экспорта заключается в том, что они явно указывают компоновщику, какие символы могут быть экспортированы из объектных файлов через внешний файл. Пользователи GNU называют такие внешние файлы "картами экспорта". Для нашего примера мы можем создать следующую карту экспорта:

{
global: func1;
local: *;
};

Данная конструкция говорит компоновщику о том, что экспортировано будет только символьное имя func1, а все остальные символы (обозначенные знаком *) являются локальными. Локальные символы func0 и myintvar можно определить и явным образом (local:func0;myintvar;), но очевидно, что удобнее использовать обобщающий знак звездочки (*). Вообще, крайне рекомендуется использовать символ звездочки для обобщения локальных символов и явно указывать только те символы, которые должны быть экспортируемыми, поскольку такой способ является более безопасным. Так вы никогда не забудете о том, что те или иные имена должны быть локальными, и, кроме того, будет исключена возможность дублирования символьных имен в различных таблицах, что может привести к непредвиденным последствиям.

Для создания общего объекта с помощью этого метода необходимо указать файл карты экспорта с помощью опции компоновщика --version-script:

$ gcc -shared -o libtest.so a.C -fPIC -Wl,--version-script=exportmap

Для чтения ELF-объекта с помощью утилиты readelf с опцией –s используйте команду readelf -s mylib.so.

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

Для компоновщика операционной системы IBM AIX используются похожие таблицы экспорта. Если быть точнее, то в AIX они называются файлами экспорта.

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

func1__Fi  // symbol name

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

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

  • export: символьное имя экспортируется с глобальным атрибутом экспорта.
  • hidden: символьное имя не экспортируется.
  • protected: символьное имя экспортируется, но не может быть вытеснено даже при компоновке во время исполнения.
  • internal: символьное имя не экспортируется. Адрес символа не должен передаваться другим программам или общим объектам, но компоновщик не проверяет это условие.

Разница между атрибутами export и hidden очевидна, чего нельзя сказать об атрибутах exported и protected. Более подробно о вытеснении символов мы поговорим в следующем разделе этой статьи.

Итак, все вышеперечисленные ключевые слова можно использовать в файле экспорта. Добавляя их через пробел после имени символа, можно добиться различной степени управления видимостью. В нашем примере мы укажем следующие атрибуты (для версии AIX 6.1 или выше):

func1__Fi export
func0__Fv hidden
myintvar hidden

Это говорит компоновщику о том, что символьное имя func1__Fi (т. е. func1) будет экспортируемым, а остальные символы – нет.

Вы могли заметить, что, в отличие от карты экспорта GNU, символы в файле экспорта являются декорированными. Декорированные символьные имена могут иметь неудобный вид, если программисты не позаботятся о правилах декорирования. Тем не менее, это помогает компоновщику быстро выполнять разрешение имен. Устранить это неудобство в ОС AIX помогает специальная утилита.

Вкратце, если при вызове компилятора XL C/C++ разработчик использует опцию –qmkshrobj, то запускается утилита CreateExportList, в результате чего после генерации объектного файла автоматически генерируется файл экспорта, содержащий имена декорированных символов. Затем компилятор передает файл экспорта компоновщику для обработки параметров видимости символьных имен. Возвращаясь к нашему примеру, запустим следующую команду:

$ xlC -qpic a.C -qmkshrobj -o libtest.a

В результате мы получим библиотеку libtest.a, в которой все символы являются экспортируемыми (действие по умолчанию). Хотя мы не достигли нашей цели, весь процесс является прозрачным для разработчика. Вместо этого мы можем создать файл экспорта при помощи утилиты CreateExportList и иметь возможность редактировать файл экспорта вручную. Если, например, мы хотим присвоить файлу экспорта имя exportfile, то компилятору XL C/C++ нужно передать опцию qexpfile=exportfile.

$ xlC -qmkshrobj -o libtest.a a.o -qexpfile=exportfile

В этом случае вы обнаружите все символьные имена, как показано ниже:

func0__Fv
func1__Fi
myintvar

В зависимости от наших требований мы можем либо просто удалить строки с именами myintvar и func0, либо добавить к ним ключевое слово hidden. После этого можно сохранить файл экспорта и передать итоговый файл компоновщику при помощи опции -bE:exportfile.

$ xlC -qmkshrobj -o libtest.a a.o -bE:exportfile

На этом вся процедура завершена. Теперь полученный динамический общий объект будет содержать экспортируемое имя func1__Fi (т. е. func1):

$ dump -Tv libtest.a

                        ***Loader Symbol Table Information***
[Index]      Value      Scn     IMEX Sclass   Type           IMPid Name

[0]     0x20000284    .data      EXP     DS SECdef        [noIMid] func1__Fi

Другой вариант заключается в явной генерации файла экспорта при помощи утилиты CreateExportList, как показано ниже:

$ CreateExportList exportfile a.o

В этом примере все работает точно так же, как и в предыдущем.

При использовании нового формата в AIX версии 6.1 и старше добавление ключевого слова для каждого символьного имени может потребовать много времени и усилий. В связи с этим в следующих версиях компилятора XL C/C++ запланирован ряд улучшений, облегчающих жизнь разработчикам (об этом мы расскажем во второй статье этой серии).

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

В следующей таблице приводится сравнение всех трех вышеперечисленных методов.

Таблица 1. Преимущества и недостатки каждого метода
МетодПреимуществаНедостатки
Использование ключевого слова static
  • Простота
  • Поддержка на уровне языка
  • Ключевое слово static накладывает следующее ограничение: переменные или функции могут использоваться только в той файловой области действия, в которой они определены.
Таблицы экспорта
  • Устраняют ограничение ключевого слова static
  • Не требуется исходный код
  • Могут включать в себя информацию о версии
  • Обеспечивают различную детальность управления видимостью символов (AIX)
  • Редактирование файлов экспорта требует дополнительных усилий
  • Необходимо обладать базовыми знаниями о декорировании символьных имен
  • Не содержат информации для оптимизации
Задание атрибутов видимости
  • Устраняет ограничение ключевого слова static
  • Четыре варианта для управления экспортируемостью символов
  • Для управления видимостью символьных имен требуется писать дополнительный код

Вытеснение символов

Как уже было отмечено, существует тонкое различие между ключевыми словами export и protected, которое связано с вытеснением символов. Вытеснение происходит тогда, когда адрес символа, полученный на этапе компоновки, замещается другим адресом, полученным во время выполнения программы (хотя следует заметить, что в AIX компоновка во время выполнения не обязательна). Концептуально компоновка во время выполнения разрешает все неопределенные и не отложенные символы общих модулей после начала выполнения программы. Этот механизм позволяет определять символы во время выполнения программы (эти определения функций недоступны во время компоновки) и выполнять перекомпоновку символов. Если в операционной системе AIX компоновка основной программы выполняется с флагом -brtl или если в переменной среды LDR_CNTRL указаны предварительно загружаемые библиотеки, программа может использовать возможности компоновки во время выполнения. Компиляция с флагом -brtl добавляет в программу ссылку на динамический компоновщик, который может быть вызван в ее коде инициализации (/lib/crt0.o) при запуске. Входные файлы общих объектов перечисляются в качестве зависимых в разделе загрузчика программы в том же порядке, в котором они указываются в командной строке. Когда программа начинает работать, системный загрузчик загружает эти общие объекты таким образом, что их определения становятся доступными для динамического компоновщика.

Итак, переопределение элементов в общих объектах во время выполнения программы называется вытеснением символов. Вытеснение символов в AIX возможно только при использовании компоновки во время выполнения. Импорты, привязанные к модулю во время компоновки, могут быть перепривязаны к другому модулю во время выполнения. Возможность вытеснения локального определения импортированным экземпляром зависит от того, каким способом был скомпонован модуль. Однако вытеснение неэкспортируемых символов во время выполнения невозможно. При загрузке компонента во время выполнения все символы внутри этого компонента, для которых используются параметры видимости по умолчанию, будут вытеснены символами с аналогичными именами, находящимися внутри уже загруженных компонентов. Обратите внимание на то, что поскольку основная программа загружается всегда первой, все определенные в ней символы никогда не вытесняются (не переопределяются).

Защищенные символы являются экспортируемыми, но не вытесняемыми. Экспортируемые символы, наоборот, экспортируются и могут быть вытеснены (если используется компоновка во время выполнения).

В Linux ® и AIX существуют отличия в используемых по умолчанию параметрах видимости символов. Компиляторы GNU и файловый формат ELF по умолчанию устанавливают признак видимости в значение default, которое применяется для экспортируемых вытесняемых символов. Это соответствует значению exported, устанавливаемому по умолчанию в AIX.

В следующем примере представлен код для платформы AIX.

Листинг 3. func.C
#include <stdio.h>
void func_DEFAULT(){
        printf("func_DEFAULT in the shared library, Not preempted\n");
}

void func_PROC(){
        printf("func_PROC in the shared library, Not preempted\n");
}
Листинг 4. invoke.C
extern void func_DEFAULT();
extern void func_PROC();

void invoke(){
        func_DEFAULT();
        func_PROC();
}
Листинг 5. main.C
#include <stdio.h>

extern void func_DEFAULT();
extern void func_PROC();
extern void invoke();

int main(){
        invoke();
        return 0;
}

void func_DEFAULT(){
        printf("func_DEFAULT redefined in main program, Preempted ==> EXP\n");
}

void func_PROC(){
        printf("func_PROC redefined in main program, Preempted ==> EXP\n");
}

В представленных выше примерах мы определили функции func_DEFAULT и func_PROC в файлах func.C и main.C. Эти функции имеют одинаковые имена, но работают по-разному. Функция invoke из файла invoke.C поочередно вызывает функции func_DEFAULT и func_PROC. Чтобы посмотреть, какие символы были экспортированы и каким образом, используем следующий код из файла exportlist.

Листинг 6. exportlist
func_DEFAULT__Fv export
func_PROC__Fv protected
invoke__Fv

При работе с компоновщиком AIX версии ниже 6.1 ключевое слово export можно опустить, а вместо ключевого слова protected использовать слово symbolic. Команды для генерации библиотеки libtest.so и исполняемого файла main представлены в следующем листинге:

/* generate position-independent code suitable for use in shared libraries. */
$ xlC -c func.C invoke.C -qpic

/* generate shared library, exportlist is used to control symbol visibility */
$ xlC -G -o libtest.so func.o invoke.o -bE:exportlist

$ xlC -c main.C

/* -brtl enable runtime linkage. */
$ xlC main.o -L. -ltest -brtl -bexpall -o main

По существу, мы собираем библиотеку libtest.so из файлов func.o и invoke.o. Мы используем файл exportlist, чтобы сделать функцию func_DEFAULT из файла func.C и функцию func_PROC из файла func.C экспортируемыми, но при этом защищенными. Таким образом, библиотека libtest.so содержит два экспортируемых символьных имени и одно защищенное. В случае основной программы мы экспортируем все символьные имена из файла main.C, но связываем его с библиотекой libtest.so. Обратите внимание на использование флага -brtl, разрешающего выполнять динамическую компоновку библиотеки libtest.so.

Следующий шаг – это запуск основной программы.

$ ./main
func_DEFAULT redefined in main program, Preempted ==> EXP
func_PROC in the shared library, Not preempted

Здесь мы видим нечто интересное: func_DEFAULT является функцией из файла main.C, а func_PROC – функцией библиотеки libtest.so (файл func.C). Символьное имя func_DEFAULT является вытесненным, поскольку его локальная версия (назовем ее так, поскольку она вызывается из файла invoke.C, находящегося в том же модуле, что и функция func_DEFAULT из файла func.C) из библиотеки libtest.so замещена версией из другого модуля. То же самое происходит с функцией func_PROC, для которой в файле экспорта установлен флаг protected.

Обратите внимание на то, что символ, который может вытеснять другие символы, всегда должен быть экспортируемым. Если не использовать опцию -bexpall при компоновке исполняемого файла main, мы получим следующий результат:

$ xlC main.o -L. -ltest -brtl -o main; //-brtl enable runtime linkage.
$ ./main
func_DEFAULT in the shared library, Not preempted
func_PROC in the shared library, Not preempted

В этом случае вытеснения символов не произошло. Все символы остались теми же, что и в модуле.

Чтобы проверить, является ли символ экспортируемым или защищенным во время выполнения, можно воспользоваться утилитой dump:

$ dump -TRv libtest.so
libtest.so:

                        ***Loader Section***

                        ***Loader Symbol Table Information***
[Index]      Value      Scn     IMEX Sclass   Type           IMPid Name

[0]     0x00000000    undef      IMP     DS EXTref   libc.a(shr.o) printf
[1]     0x2000040c    .data      EXP     DS SECdef        [noIMid] func_DEFAULT__Fv
[2]     0x20000418    .data      EXP     DS SECdef        [noIMid] func_PROC__Fv
[3]     0x20000424    .data      EXP     DS SECdef        [noIMid] invoke__Fv

                        ***Relocation Information***
             Vaddr      Symndx      Type      Relsect    Name
        0x2000040c  0x00000000   Pos_Rel      0x0002     .text
        0x20000410  0x00000001   Pos_Rel      0x0002     .data
        0x20000418  0x00000000   Pos_Rel      0x0002     .text
        0x2000041c  0x00000001   Pos_Rel      0x0002     .data
        0x20000424  0x00000000   Pos_Rel      0x0002     .text
        0x20000428  0x00000001   Pos_Rel      0x0002     .data
        0x20000430  0x00000000   Pos_Rel      0x0002     .text
        0x20000434  0x00000003   Pos_Rel      0x0002     printf
        0x20000438  0x00000004   Pos_Rel      0x0002     func_DEFAULT__Fv
        0x2000043c  0x00000006   Pos_Rel      0x0002     invoke__Fv

В этом примере представлен результат проверки библиотеки libtest.so. Мы видим, что символьные имена func_DEFAULT__Fv и func_PROC__Fv являются экспортируемыми. Тем не менее для символа func_PROC__Fv не выполнялись никакие перемещения в памяти. Это означает, что загрузчик не смог найти способ заменить адрес символа func_PROC адресом из таблицы содержания. Адрес функции func_PROC в таблице содержания – это тот адрес, по которому функция выполняет команду передачи управления. Таким образом, вытеснения func_PROC не произошло. Мы наглядно видим, что она является защищенной.

На самом деле разработчики редко используют вытеснение символов. Тем не менее оно позволяет динамически замещать символы во время выполнения, создавая при этом определенные бреши в системе безопасности. Если вам не требуется использовать вытеснение символов в ваших библиотеках (но тем не менее требуется экспортировать их), в целях безопасности делайте их защищенными.

Благодарности

Авторы выражают благодарность Цзинь Цзи (Dr. Jinsong Ji) за ценные замечания и рецензирование этой статьи.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=AIX и UNIX
ArticleID=1002501
ArticleTitle=Управление видимостью символических имен для общих библиотек: часть 1 - введение
publish-date=04032015