Лучшие практики написания динамических библиотек Linux

Linux Командная строка API C++

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

Однако при реализации динамических библиотек проблема немного усложняется.

На функции интерфейса динамической библиотеки могут ссылаться другие файлы в динамической библиотеке, а также на них могут ссылаться другие динамические библиотеки. На внутренние функции динамической библиотеки могут ссылаться только другие файлы в той же динамической библиотеке, и на них не могут ссылаться другие динамические библиотеки.

Для требования «как сделать, чтобы на функции могли ссылаться другие файлы в динамической библиотеке, но не другие динамические библиотеки»,staticКлючевые слова бессильны.

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

символ

Для файлов ELF все переменные и функции, встречающиеся в программе,символ.

Ячейка памяти, где находится переменная, и тело функции называются символическими.определение.

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

Привязка символов

Существует три привязки для символов, а именно:

binding имея в виду
LOCAL Локальные символы, на которые можно ссылаться только внутри файла
GLOBAL Надежные глобальные символы, на которые могут ссылаться другие файлы, и которые могут быть определены только в одном файле.
WEAK Слабые глобальные символы, на которые могут ссылаться другие файлы, но которые могут быть определены в нескольких файлах.

Local Symbol

использоватьstaticГлобальные переменные и функции, украшенные ключевыми словами, являются локальными символами.

На такие символы можно ссылаться только в том же файле, но не в других файлах. Несколько файлов могут определять локальные символы с одним и тем же именем, но эти символы не влияют друг на друга.

Локальные символы в одной динамической библиотеке и локальные символы с тем же именем в другой динамической библиотеке не будут влиять друг на друга.

Global Symbol

Не используйstaticГлобальные переменные и функции, украшенные ключевыми словами, являются глобальными символами.

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

Если во время компоновки несколько файлов определяют глобальный символ с одним и тем же именем, возникнет ошибка компоновки.

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

На уровне файлов ELF для доступа к глобальным символам в динамических библиотеках требуются PLT и GOT, к ним нельзя обращаться напрямую, поэтому скорость ниже, чем при доступе к локальным символам.

Слабый символ

В программах на C и C++ существуют следующие способы определения слабых символов:

  1. использовать__attribute__((weak))Декорированные глобальные переменные и функции являются слабыми символами;
  2. в библиотеке С++operator newа такжеoperator deleteявляется слабым символом; 3. Если встроенная функция определена, но встроенная функция генерирует независимое тело функции, то символ является слабым символом;
  3. В C++ функции-члены, определенные непосредственно в определении класса, имеют свои собственныеinlineэффект, так что это тоже слабый символ;
  4. Код после создания экземпляра шаблона функции является слабым символом.

Слабые символы могут быть определены в нескольких файлах, но при компоновке сохраняется только одно определение. Зарезервированные правила:

  1. Если имеется несколько слабых символов с одинаковым именем, будет зарезервирован символ с наибольшей длиной символа.
    1. Для переменных сохраняется определение с наибольшим размером.
    2. Для функций сохраняется определение с самым длинным телом функции.
  2. Если есть несколько слабых символов и глобальный символ с тем же именем, определение этого глобального символа будет сохранено.

Таким образом, если пользователь определяетoperator newфункция, компоновщик будет использовать определяемую пользователем реализацию вместо реализации из стандартной библиотеки.

Видимость символов

Чтобы решить проблему, связанную с тем, что глобальные символы могут мешать друг другу между динамическими библиотеками, ELF вводит видимость символов.

При связывании с динамической библиотекой или исполняемым файлом компоновщик изменяет свою привязку в зависимости от видимости символа.

Существует 7 типов видимости, но обычно используются только значения по умолчанию и скрытые. Их модификаторы:

  • __attribute__((visibility ("default")))
  • __attribute__((visibility ("hidden")))

Видимость по умолчанию установлена ​​по умолчанию, но аргументы командной строки могут быть переданы во время компиляции.-fvisibility=hiddenУстановите видимость по умолчанию на скрытую.

Default Visibility

Во время компоновки привязка символа остается неизменной.

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

Обычно видимость символов, которые необходимо экспортировать, установлена ​​по умолчанию.

Hidden Visibility

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

Следовательно, доступ к таким символам возможен только в динамических библиотеках, а не в других динамических библиотеках.

Для динамических библиотек или исполняемых программ видимость всех символов, которые не нужно экспортировать, должна быть скрыта.

Лучшие практики

При реализации динамических библиотек для C и C++ используйте-fvisibility=hiddenскомпилировать динамическую библиотеку.

При определении API рекомендуется использоватьDLL_PUBLICа такжеDLL_LOCALмакрос для управления видимостью символов, он отлично работает в Windows, Cygwin, Linux и macOS:

#if defined _WIN32 || defined __CYGWIN__
  #ifdef BUILDING_DLL
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllexport))
    #else
      // Note: actually gcc seems to also supports this syntax.
      #define DLL_PUBLIC __declspec(dllexport)
    #endif
  #else
    #ifdef __GNUC__
      #define DLL_PUBLIC __attribute__ ((dllimport))
    #else
      // Note: actually gcc seems to also supports this syntax.
      #define DLL_PUBLIC __declspec(dllimport)
    #endif
    #define DLL_LOCAL
  #endif
#else
  #if __GNUC__ >= 4
    #define DLL_PUBLIC __attribute__ ((visibility ("default")))
    #define DLL_LOCAL  __attribute__ ((visibility ("hidden")))
  #else
    #define DLL_PUBLIC
    #define DLL_LOCAL
  #endif
#endif

В C функции и переменные можно экспортировать с помощью этого макроса:

// 使用 DLL_PUBLIC 修饰需要导出的符号
DLL_PUBLIC int my_exported_api_func();
DLL_PUBLIC int my_exported_api_val;

// 不使用 DLL_PUBLIC 修饰动态库内部的符号,
// 因为默认可见性被修改为 hidden
int my_internal_global_func();

В C++ вы можете использовать этот макрос для экспорта класса:

// 使用 DLL_PUBLIC 修饰需要导出的类
class DLL_PUBLIC MyExportedClass {
 public:
  // 类里面的所有方法默认都是 DLL_PUBLIC 的
  MyExportedClass();
  ~MyExportedClass();
  int my_exported_method();
  
 private:
  int c;

  // 使用 DLL_LOCAL 修饰动态库的内部符号
  DLL_LOCAL int my_internal_method();
};

эталонное чтение

Symbol Table SectionОпределение таблицы символов в файле ELF, в котором подробно описывается привязка и видимость.

VisibilityЛучшие практики для видимости в вики GCC.