Ви можете налаштовувати видимість символічних імен для загальних бібліотек частина 1

Лю Чжіпен. розробник програмного забезпечення, IBM China

Що таке символьні імена і їх видимість

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

Хоча в різних модулях можна спільно використовувати як змінні, так і функції, в об'єктних файлах частіше зустрічається визначення області видимості змінних. Наприклад, можна оголосити змінну у файлі a.c так:

У файлі b.c оголосимо цю ж змінну так:

Чому необхідно управляти видимістю символічних імен

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

Якщо приймається рішення експортувати всі символічні імена, існує висока ймовірність виникнення конфліктів під час компонування, особливо якщо різні модулі розробляються різними людьми. Оскільки символьне ім'я є низькорівневим поняттям, до нього не застосовується область видимості. Як тільки відбудеться компоновка двох бібліотек, що містять символи з однаковими іменами, компілятор може замістити один з цих символів іншим символом з таким же ім'ям (на щастя, при цьому виводяться попередження або повідомлення про помилки). У більшості випадків з точки зору розробника бібліотеки такі символи вооблще не призначаються для використання. Тому в подібних ситуаціях значно краще створювати лише обмежена кількість зрозумілих (і добре продуманих) символьних імен.

З кожним днем ​​до швидкодії додатків, що розробляються на C ++, пред'являється все більше вимог. У зв'язку з наявністю залежностей від інших бібліотек і використанням специфічних можливостей C ++ (наприклад, шаблонів) компілятор і компонувальник використовують і генерують величезну кількість символьних імен. Як наслідок, експорт всіх символів призводить до уповільнення роботи програми і збільшення обсягу займаної пам'яті. Експорт лише обмеженого числа символів може зменшити час завантаження і компонування динамічних загальних бібліотек. Крім того, це дозволяє компілятору генерувати більш ефективний і оптимальний програмний код.

Перераховані вище недоліки пояснюють, чому необхідно визначати видимість символьних імен. У цій статті ми покажемо, як керувати символьними іменами динамічних загальних об'єктів (DSO). Цю проблему можна вирішити по-різному, і ми підкажемо вам, який із способів краще підходить для кожної платформи.

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

У цьому розділі ми будемо використовувати наступний фрагмент коду на мові C ++:

Лістинг 1. a.C

У файлі a.C ми визначили змінну з ім'ям myintvar і дві функції з іменами func0 і func1. За замовчуванням при створенні загальної бібліотеки на AIX-платформі компілятор і компонувальник з інструментом CreateExportList зроблять все три символи видимими. Це можна перевірити, звернувшись до таблиці символьних імен завантажувача за допомогою утиліти dump:

Тут значення "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

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

У цьому прикладі була експортована тільки функція func1. Проте, хоча ключове слово static і може приховувати символи, воно накладає обмеження: змінні або функції можуть використовуватися тільки в тій файлової області дії, в якій вони визначені. Таким чином, якщо визначити змінну як

а потім за допомогою файлу 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 для символьних імен. Розглянемо приклад для цього випадку:

Щоб визначити атрибут GNU, необхідно використовувати конструкцію __attribute__ і параметр, укладений в подвійні круглі дужки. Щоб приховати символьні імена, можна вказати значення visibility ( "hidden"). У нашому прикладі ми зробили це для змінної myintvar і функції func0. В результаті їх не можна буде експортувати до бібліотеки, але можна використовувати у вихідних файлах. Фактично приховані символи не будуть відображатися в динамічної таблиці символів, але будуть присутні в таблиці символів, призначеної для статичного зв'язування. Цей строго певний алгоритм роботи безумовно може вирішити нашу задачу. Очевидно, що цей метод краще методу з використанням ключового слова static.

Також в інтерфейсі ELF ABI визначені наступні режими видимості:

  • STV_PROTECTED: символ є видимим за межами поточного виконуваного модуля або загального об'єкта, але не може бути заміщений. Іншими словами, якщо до захищеного (protected) символу загальної бібліотеки виконується звернення з зовнішнього коду, то цей код буде завжди посилатися на символ загальної бібліотеки навіть якщо в виконуваному файлі був визначений символ з таким же ім'ям.
  • STV_INTERNAL: символ не доступний за межами поточного виконуваного файлу або загальної бібліотеки.

Зауважимо, що в даний час цей метод не підтримується компілятором XL C / C ++ навіть на платформі PowerLinux. На щастя, є ще один вихід із ситуації.

3. Використання таблиць експорту

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

Таблиця експорту може бути автоматично згенерована компілятором (або сторонніми інструментами, наприклад, CreateExportlist) в момент створення загальної бібліотеки або написана розробником вручну. За допомогою спеціального параметра таблиця експорту передається компонувальнику у вигляді вхідних даних. Однак оскільки всю основну роботу виконує драйвер компілятора, розробники рідко приділяють увагу детальної налаштування опцій.

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

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

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

Для читання ELF-об'єкта за допомогою утиліти readelf з опцією -s використовуйте команду readelf -s mylib.so.

Ця команда покаже, що глобальний доступ можна отримати лише до функції func1 (записи в розділі .dynsym), а інші символи недоступні і є локальними.

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

Створити файл експорту дуже просто. Для цього потрібно лише перерахувати символи, які необхідно експортувати, в файлі експорту. У нашому прикладі це буде виглядати наступним чином:

Таким чином, коли ми вказуємо файл експорту за допомогою опції компоновщика, єдине символьне ім'я, яке ми хочемо експортувати, додається в "таблицю символів завантажувача" XCOFF-об'єкта, а всі інші символи залишаються неекспортіруемимі.

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

Різниця між атрибутами export і hidden очевидна, чого не можна сказати про атрибути exported і protected. Більш докладно про витіснення символів ми поговоримо в наступному розділі цієї статті.

Отже, всі перераховані вище ключові слова можна використовувати в файлі експорту. Додаючи їх через пробіл після імені символу, можна домогтися різного ступеня управління видимістю. У нашому прикладі ми вкажемо такі атрибути (для версії AIX 6.1 або вище):

Це говорить компонувальнику про те, що символьне ім'я func1__Fi (т. Е. Func1) буде експортним, а решта символів - немає.

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

Якщо коротко, якщо при виклику компілятора XL C / C ++ розробник використовує опцію -qmkshrobj. то запускається утиліта CreateExportList. в результаті чого після генерації об'єктного файлу автоматично генерується файл експорту, що містить імена декорованих символів. Потім компілятор передає файл експорту компонувальнику для обробки параметрів видимості символьних імен. Повертаючись до нашого прикладу, запустимо наступну команду:

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

В цьому випадку ви виявите все символьні імена, як показано нижче:

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

На цьому вся процедура завершена. Тепер отриманий динамічний загальний об'єкт буде містити експортовану ім'я func1__Fi (т. Е. Func1):

Інший варіант полягає в явній генерації файлу експорту за допомогою утиліти CreateExportList. як показано нижче:

У цьому прикладі все працює точно так само, як і в попередньому.

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

У разі використання таблиць експорту вся інформація зберігається в цих таблицях, а необхідність редагувати вихідний код відсутній. Це дозволяє розділити роботу з написання коду і роботу по розробці бібліотек. Однак при використанні цього методу можна зіткнутися з проблемою. Оскільки файл з вихідним кодом не змінюється, двійковий код, створений компілятором, може бути неоптимальним. Компілятору може не вистачити інформації, щоб оптимізувати ті символьні імена, що не були експортовані. Це може привести або до збільшення розміру виконуваного файлу, або уповільнити процес розв'язання символьних імен. На щастя, для більшості додатків це не дуже серйозна проблема.

У наступній таблиці наводиться порівняння всіх трьох перерахованих вище методів.

Таблиця 1. Переваги та недоліки кожного методу