Что такое статическая библиотека
Перейти к содержимому

Что такое статическая библиотека

  • автор:

Пошаговое руководство. Создание и использование статической библиотеки

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

В этом пошаговом руководстве рассматриваются следующие задачи:

  • Создание проекта статической библиотеки
  • Добавление класса в статическую библиотеку
  • Создание консольного приложения C++, ссылающегося на статическую библиотеку
  • Использование функциональности из статической библиотеки в приложении
  • Запуск приложения

Необходимые компоненты

Для работы необходимо владеть основами языка C++.

Создание проекта статической библиотеки

Инструкции по созданию проекта зависят от используемой версии Visual Studio. Чтобы ознакомиться с документацией по предпочтительной версии Visual Studio, используйте селектор Версия. Он находится в верхней части оглавления на этой странице.

Создание проекта статической библиотеки в Visual Studio

  1. В строке меню выберите «Файл>нового проекта«>, чтобы открыть диалоговое окно «Создать проект».
  2. В верхней части диалогового окна для параметра Язык установите значение C++, для параметра Платформа — значение Windows, а для параметра Тип проекта — значение Библиотека.
  3. В отфильтрованном списке типов проектов выберите пункт Мастер классических приложений Windows, а затем нажмите кнопку Далее.
  4. На странице Настроить новый проект введите MathLibrary в поле Имя проекта. В поле Имя решения введите StaticMath. Нажмите кнопку Создать, чтобы открыть диалоговое окно Проект классического приложения Windows.
  5. В диалоговом окне Проект классического приложения Windows в разделе Тип приложения выберите Статическая библиотека (.lib).
  6. В разделе Дополнительные параметры снимите флажок Предварительно откомпилированный заголовок, если он установлен. Установите флажок Пустой проект.
  7. Нажмите кнопку ОК, чтобы создать проект.

Создание проекта статической библиотеки в Visual Studio 2017

  1. В строке меню выберите Файл >Создать >Проект.
  2. В диалоговом окне Новый проект выберите Установленные>Visual C++>Классическое приложение для Windows. На центральной панели выберите Мастер классических приложений Windows.
  3. Укажите имя для проекта, например MathLibrary, в поле Имя. Укажите имя для решения, например StaticMath, в поле Имя решения. Нажмите кнопку ОК.
  4. В диалоговом окне Проект классического приложения Windows в разделе Тип приложения выберите Статическая библиотека (.lib).
  5. В разделе Дополнительные параметры снимите флажок Предварительно откомпилированный заголовок, если он установлен. Установите флажок Пустой проект.
  6. Нажмите кнопку ОК, чтобы создать проект.

Создание проекта статической библиотеки в Visual Studio 2015

  1. В строке меню выберите Файл >Создать >Проект.
  2. В диалоговом окне Новый проект выберите Установленные>Шаблоны>Visual C++>Win32. В центральной области выберите Консольное приложение Win32.
  3. Укажите имя для проекта, например MathLibrary, в поле Имя. Укажите имя для решения, например StaticMath, в поле Имя решения. Нажмите кнопку ОК.
  4. В мастере приложений Win32 нажмите кнопку Далее.
  5. На странице Параметры приложения в разделе Тип приложения выберите Статическая библиотека. В разделе Дополнительные параметры снимите флажок Предварительно откомпилированный заголовок. Чтобы создать проект, нажмите кнопку Готово.

Добавление класса в статическую библиотеку

Добавление класса в статическую библиотеку

  1. Чтобы создать файл заголовка для нового класса откройте контекстное меню проекта MathLibrary в обозревателе решений, а затем выберите Добавить>Новый элемент.
  2. В диалоговом окне Добавление нового элемента выберите пункт Visual C++>Код. В центральной области выберите Заголовочный файл (.h). Укажите имя для файла заголовка, например MathLibrary.h, и нажмите кнопку Добавить. Отобразится почти пустой файл заголовка.
  3. Добавьте объявление класса с именем Arithmetic для выполнения обычных арифметических операций, таких как сложение, вычитание, умножение и деление. Код должен выглядеть примерно так:
// MathLibrary.h #pragma once namespace MathLibrary < class Arithmetic < public: // Returns a + b static double Add(double a, double b); // Returns a - b static double Subtract(double a, double b); // Returns a * b static double Multiply(double a, double b); // Returns a / b static double Divide(double a, double b); >; > 
// MathLibrary.cpp // compile with: cl /c /EHsc MathLibrary.cpp // post-build command: lib MathLibrary.obj #include "MathLibrary.h" namespace MathLibrary < double Arithmetic::Add(double a, double b) < return a + b; >double Arithmetic::Subtract(double a, double b) < return a - b; >double Arithmetic::Multiply(double a, double b) < return a * b; >double Arithmetic::Divide(double a, double b) < return a / b; >> 

Примечание. При выполнении сборки из командной строки Visual Studio программа собирается в два этапа. Сначала выполните компиляцию cl /c /EHsc MathLibrary.cpp кода и создайте файл объекта с именем MathLibrary.obj. (Команда cl вызывает компилятор, Cl.exe и /c параметр указывает компиляцию без связывания. Дополнительные сведения см. в разделе Параметр /c (компиляция без связывания). Во-вторых, запустите файл lib MathLibrary.obj , чтобы связать код и создать статическую библиотеку MathLibrary.lib. (Команда lib вызывает диспетчер библиотек, Lib.exe. Дополнительные сведения см. в справочнике по LIB.)

Создание консольного приложения C++, ссылающегося на статическую библиотеку

Создание консольного приложения C++, ссылающегося на статическую библиотеку, в Visual Studio

  1. В обозревателе решений щелкните правой кнопкой мыши узел верхнего уровня Решение StaticMath, чтобы открыть контекстное меню. Выберите пункты Добавить>Новый проект, чтобы открыть диалоговое окно Добавить новый проект.
  2. В верхней части диалогового окна задайте для фильтра Тип проекта значение Консоль.
  3. В отфильтрованном списке типов проектов щелкните Консольное приложение, а затем нажмите кнопку Далее. На следующей странице в поле Имя введите имя проекта MathClient.
  4. Нажмите кнопку Создать, чтобы создать клиентский проект.
  5. После создания консольного приложения будет создана пустая программа. Имя исходного файла будет совпадать с ранее выбранным именем. В этом примере он имеет имя MathClient.cpp .

Создание консольного приложения C++, ссылающегося на статическую библиотеку, в Visual Studio 2017

  1. В обозревателе решений щелкните правой кнопкой мыши узел верхнего уровня Решение StaticMath, чтобы открыть контекстное меню. Выберите пункты Добавить>Новый проект, чтобы открыть диалоговое окно Добавить новый проект.
  2. В диалоговом окне Добавление нового проекта выберите Установленные>Visual C++>Классическое приложение для Windows. На центральной панели выберите Мастер классических приложений Windows.
  3. Укажите имя для проекта, например MathClient, в поле Имя. Нажмите кнопку ОК.
  4. В диалоговом окне Проект классического приложения Windows в разделе Тип приложения выберите Консольное приложение (EXE).
  5. В разделе Дополнительные параметры снимите флажок Предварительно откомпилированный заголовок, если он установлен.
  6. Нажмите кнопку ОК, чтобы создать проект.
  7. После создания консольного приложения будет создана пустая программа. Имя исходного файла будет совпадать с ранее выбранным именем. В этом примере он имеет имя MathClient.cpp .

Создание консольного приложения C++, ссылающегося на статическую библиотеку, в Visual Studio 2015

  1. В обозревателе решений щелкните правой кнопкой мыши узел верхнего уровня Решение StaticMath, чтобы открыть контекстное меню. Выберите пункты Добавить>Новый проект, чтобы открыть диалоговое окно Добавить новый проект.
  2. В диалоговом окне Добавление нового проекта выберите Установленные>Visual C++>Win32. В центральной области выберите Консольное приложение Win32.
  3. Укажите имя для проекта, например MathClient, в поле Имя. Нажмите кнопку ОК.
  4. В диалоговом окне Мастер приложений Win32 нажмите кнопку Далее.
  5. На странице Параметры приложения выберите в поле Тип приложения пункт Консольное приложение. В разделе Дополнительные параметры снимите флажок Предварительно откомпилированный заголовок, а затем установите флажок Пустой проект. Чтобы создать проект, нажмите кнопку Готово.
  6. Чтобы добавить исходный файл в пустой проект, щелкните правой кнопкой мыши, чтобы открыть контекстное меню проекта MathClient в Обозреватель решений, а затем нажмите кнопку «Добавить >новый элемент».
  7. В диалоговом окне Добавление нового элемента выберите пункт Visual C++>Код. В центральной области выберите Файл C++ (.cpp). Укажите имя исходного файла, например MathClient.cpp, и нажмите кнопку Добавить. Отобразится пустой исходный файл.

Использование функциональности из статической библиотеки в приложении

Использование функциональности из статической библиотеки в приложении

  1. Для использования математических процедур из статической библиотеки необходимо сослаться на эту библиотеку. В обозревателе решений откройте контекстное меню проекта MathClient, а затем выберите команду Добавить>Ссылка.
  2. В диалоговом окне Добавление ссылки перечислены библиотеки, на которые можно создать ссылку. На вкладке Проекты перечислены проекты текущего решения и все библиотеки, на которые они ссылаются. На вкладке Проекты установите флажок MathLibrary, а затем нажмите кнопку ОК.
  3. Для создания ссылки на файл заголовка MathLibrary.h необходимо изменить путь к каталогам включаемых файлов. В обозревателе решений щелкните правой кнопкой мыши проект MathClient, чтобы открыть контекстное меню. Выберите пункт Свойства, чтобы открыть диалоговое окно Страницы свойств MathClient.
  4. В диалоговом окне Страницы свойств MathClient в раскрывающемся списке Конфигурация выберите пункт Все конфигурации. В раскрывающемся списке Платформа выберите пункт Все платформы.
  5. Перейдите на страницу свойств Свойства конфигурации>C/C++>Общие. В свойстве Дополнительные каталоги включаемых файлов укажите путь к каталогу MathLibrary или найдите этот каталог. Чтобы найти путь к каталогу, выполните указанные ниже действия.
    1. Откройте раскрывающийся список значений свойства Дополнительные каталоги включаемых файлов, а затем выберите Изменить.
    2. В диалоговом окне Дополнительные каталоги включаемых файлов дважды щелкните в верхней части текстового поля. Нажмите кнопку с многоточием (. ) в конце строки.
    3. В диалоговом окне Выбор каталога перейдите на уровень вверх и выберите каталог MathLibrary. Затем нажмите кнопку Выбрать папку, чтобы сохранить выбор.
    4. В диалоговом окне Дополнительные каталоги включаемых файлов нажмите кнопку ОК.
    5. В диалоговом окне Страницы свойств нажмите кнопку OK, чтобы сохранить изменения в проекте.
    // MathClient.cpp // compile with: cl /EHsc MathClient.cpp /link MathLibrary.lib #include #include "MathLibrary.h" int main() < double a = 7.4; int b = 99; std::cout 
    

    Выполнить приложение

    Запуск приложения

    1. Убедитесь в том, что проект MathClient выбран в качестве проекта по умолчанию. Чтобы выбрать его, в обозревателе решений откройте контекстное меню проекта MathClient и выберите команду Назначить запускаемым проектом.
    2. Чтобы запустить проект, в строке меню выберите Отладка>Запуск без отладки. Выходные данные должны выглядеть примерно так:
    a + b = 106.4 a - b = -91.6 a * b = 732.6 a / b = 0.0747475 

    в чём разница между статической и динамической библиотекой?

    В общем я подключил динамическую библиотеку.

    Статическая представляет собой собрание *.obj-файлов библиотеки в *.lib, который мы можем, указав линкеру, прицепить к нашей программе в момент компиляции. Содержимой библиотеки, как всегда, описывается в хэдерах, которые распространяются вместе с *.lib . На выходе мы получим одинокий исполняемый файл вашей программы (.exe). А ещё статические библиотеки увеличивают размер кода в двоичном формате. Они всегда загружаются, и любая версия кода, скомпилированного вами, - это версия запускаемого кода :3.

    Динамическая линковка выполняется средствами платформы (операционной системы) в процессе работы программы. Все так же у нас в руках *.lib и *.h файлы, однако, теперь к ним добавляется .dll (.so для Linux). *.lib-файл теперь содержит только вызовы к *.dll, где лежат непосредственно алгоритмы и которые вызываются уже на ходу, а не компилируются. Потому теперь у нас *.exe + *.dll . Несколько программ могут использовать один *.dll одновременно, тем самым не занимая оперативную память одинаковыми кусками и сами программы меньше размером. Так, например, работают многие драйверы и графические библиотеки (DirectX и OpenGL). Однако, сейчас это не такая актуальная проблема, тянут недостатки - несовместимости версий, отсутствие нужных библиотек, ад зависимостей для установки приложений (работая в Linux с графическим окружением Gnome (основанной на библиотеке GTK+) если скачать малюсенький текстовый редактор Kate для ГО KDE (основанной на Qt), то придется тянуть этот-самый Qt на десятки мегабайт). Потому, сейчас рекомендуют не увлекаться динамической линковкой и стараться связывать программы статически. Да и *.dll придется таскать с собой :3

    Статические библиотеки (C++/CX)

    Статическая библиотека, используемая в приложении универсальная платформа Windows (UWP), может содержать код C++ стандарта ISO, включая типы STL, а также вызовы API Win32, которые не исключены из платформы приложений среда выполнения Windows. Статическая библиотека использует компоненты среда выполнения Windows и может создавать компоненты среда выполнения Windows с определенными ограничениями.

    Создание статических библиотек

    Инструкции по созданию проекта зависят от установленной версии Visual Studio. Чтобы ознакомиться с документацией по предпочтительной версии Visual Studio, используйте селектор Версия. Он находится в верхней части оглавления на этой странице.

    Создание статической библиотеки UWP в Visual Studio

    1. В строке меню выберите "Файл>нового проекта">, чтобы открыть диалоговое окно "Создать проект".
    2. В верхней части диалогового окна задайте для языка C ++, установите для Платформы значение "Платформа" и задайте тип проекта uWP.
    3. В отфильтрованном списке типов проектов выберите статическую библиотеку (универсальная windows — C++/CX), а затем нажмите кнопку "Далее". На следующей странице укажите имя проекта и укажите расположение проекта при необходимости.
    4. Нажмите кнопку Создать, чтобы создать проект.

    Создание статической библиотеки UWP в Visual Studio 2017 или Visual Studio 2015

    1. В строке меню выберите Файл >Создать >Проект. В разделе Visual C++>Windows universal выберите статическую библиотеку (универсальная windows).
    2. В области Обозреватель решенийоткройте контекстное меню для проекта и выберите пункт Свойства. В диалоговом окне "Свойства" на странице свойств>конфигурации C/C++ установите для параметра "Использовать расширение среда выполнения Windows" значение "Да" (/ZW).

    При компиляции новой статической библиотеки при вызове API Win32, исключенном для приложений UWP, компилятор вызовет ошибку C3861 , "Идентификатор не найден". Чтобы найти альтернативный метод, поддерживаемый для среда выполнения Windows, см. статью "Альтернатива API Windows" в приложениях UWP.

    При добавлении проекта статической библиотеки C++ в решение приложения UWP может потребоваться обновить параметры свойств проекта библиотеки, чтобы свойство поддержки UWP было задано как "Да". Без этого параметра код создает и ссылки, но возникает ошибка при попытке проверить приложение для Microsoft Store. Статическая библиотека должна компилироваться с теми же параметрами компилятора, что и проект, в котором она используется.

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

    предупреждение LNK4264: архивирование файла объектов, скомпилированного с помощью /ZW в статическую библиотеку; обратите внимание, что при создании типов среда выполнения Windows не рекомендуется связываться со статической библиотекой, содержащей метаданные среда выполнения Windows.

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

    Динамические и статические библиотеки.

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

    Статические библиотеки.

    // sum.cpp int sum(int a, int b)
    // four.cpp #include int sum(int a, int b); int main()
    #include int sum(int a, int b); int main()

    И мы зачем-то пытаемся вычленить sum.cpp как библиотеку. Тогда сделать надо вот что: Компилируем:

    g++ -c sum.cpp -o sum.o ar rcs libsum.a sum.o 

    Что тут происходит?

    • ar — сокращение от «archive».
    • rcs — это некоторая магия (читайте man).
    • libsum.a — название библиотеки.

    Чтобы скомпилировать каждый из файлов выше с этой библиотекой, делаем так:

    g++ four.cpp -lsum -L. -o four g++ five.cpp -lsum -L. -o five 

    А что происходит тут?

    • -L говорит, в каком каталоге искать библиотеку (в нашем случае в каталоге . — в текущем).
    • -lsum говорит, что нам нужна библиотека, которая называется libsum.a (т.е. к тому, что идёт после -l спереди приписывается lib», а сзади — «.a»).

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

    Динамические библиотеки.

    Пусть у вас есть библиотека, которая используется везде вообще. Например, libc. Если она статическая, то код библиотеки есть в каждой из программ. А значит в каждой программе они занимают место и на диске, и в памяти. Чтобы этого избежать, применяют динамические библиотеки.
    Идея динамических библиотек в том, что мы ссылаемся как-то на внешнюю библиотечку, а потом на этапе исполнения грузим по надобности её части. Тогда она на диске лежит всего одна, и в память мы можем загрузить её один раз.

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

    g++ -fpic -c sum.cpp -o sum.o g++ -shared sum.o -o libsum.so g++ four.cpp -lsum -L. -o four g++ five.cpp -lsum -L. -o five 

    Что значат все консольные опции тут, уже пояснить намного сложнее, и мы поясним их в следующем параграфе.
    А пока обратим внимание на то, что когда мы запустим four или five, нам на этапе исполнения скажут, что библиотека libsum.so не найдена. Хотя, казалось бы, вот она рядом лежит. Дело в том, что по умолчанию Linux ищет библиотеки только по системным путям. (Windows ищет и в текущей директории.) Чтобы проверить, от каких библиотек зависит ваша программа, запустите ldd ./four, и вам скажут, что нужна libsum.so, но её нет.

    Есть два способа поправить сию оказию:

    Первый — явно при запуске прописывать путь до библиотек.
    Для этого существует переменная окружения LD_LIBRARY_PATH , если присвоить ей точку, всё сработает

    LD_LIBRARY_PATH=. ./four 

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

    Второй — записать в саму программу, где искать библиотеки.
    Это можно посмотреть при помощи objdump в секции Dynamic Section , где есть RUNPATH . Чтобы записать туда что надо, делается вот что:

    g++ four.cpp -lsum -L. -Wl,-rpath= -o four 

    -Wl говорит, что опцию после него (т.е. -rpath) надо передать линковщику. Линковщику эта опция говорит, что в тот самый RUNPATH надо записать тот путь, который вы попросили.
    А какой надо просить? Не «.» ведь, потому что это путь, из которого вы запускаете программу, а не то место, где сама программа.
    И тут вам на помощи приходит псевдо-путь $ORIGIN , который и ссылается на место программы. Используя его Вы можете свободно написать что-нибудь по типу -rpath='$ORIGIN/../lib/'.

    Впрочем, есть ещё и третий путь — использовать CMake, который будет делать всю грязную работу за вас, если написать ему команду add_library .

    Кстати, в Windows это работает иначе. В-нулевых, динамическая библиотека там называется не shared object, а dynamic load library. Во-первых, DLL-ки сразу же ищутся в текущем каталоге. Во-вторых, чтобы понять, что вы ссылаетесь на динамическую библиотеку, в Linux вы пишете -L. -lsum, а в Windows компиляция DLL создаёт вам специальный .lib-файл, который называется import-библиотекой, и с которым вы компилируете вашу программу, чтобы она сама поняла, откуда какие функции брать.

    Причины нестандартной компиляции динамических библиотек.

    g++ -fpic -c sum.cpp -o sum.o g++ -shared sum.o -o libsum.so 

    Нас интересуют магические слова -fpic и -shared. Зачем на как-то особенно компилировать динамические библиотеки?

    А дело вот в чём — при запуске программы, она первая загружается в адресное пространство, и она сама может выбрать, куда ей хочется. Динамические библиотеки такого же по понятным причинам позволить себе не могут. Возникает вопрос — и что теперь? А то, что при наличии глобальных переменных, мы не можем впаять им фиксированные адреса.

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

    Самый простой способ жить с динамическими библиотеками был у Microsoft на 32-битной Windows. У каждой библиотеки был base-address — то куда библиотеке хочется загрузиться. Если там свободно — туда она и загружается, а если нет, то библиотеку загружают туда, где есть место, а в специальной отдельной секции (.reloc) хранится список адресов, которые надо исправить. Разумеется, в случае релокаций умирает переиспользование библиотеки, но Windows вам же полностью предоставляют, там можно расположить системные библиотеки так, как хочется, поэтому в проприетарных системах всё будет хорошо.

    В Linux же это реализовано следующим образом. Смотрите как можем:

     call next next: pop ABX lea EAX, [EBX + (myvar - next)] 

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

    Вообще мы не очень хорошо написали (процессор не любит непарные call и pop ), поэтому обычно это выглядит так:

    get_pc: mov EBX, [ESP] ret get_variable: call get_pc next: lea EAX, [EBX + (myvar - next)] 

    Этот код называется position-independent code, и ключ -fpic именно генерацией такого кода и занимается. Вопрос — почему для этого не сделали специальную инструкцию? А вот сделали, но в 64-битном режиме. Всё что с квадратными скобками стало уметь обращаться в память начиная со смещения текущей инструкции. И называется это RIP-relative positioning.

    GOT/IAT. PLT.

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

    Например, библиотека для работы с JSON хочет делать fopen . То есть нужно подружить библиотеки друг с другом. Самый простой вариант — когда мы делаем call , в файл мы кладём нулевой адрес, а в секцию релокаций записываем, что вместо него нужно положить fopen , после чего при запуске динамический загрузчик всё разложит по местам. То есть то же самое, что с линковщиком. Почему так не делают? Потому что мы от' mmap 'или нашу библиотеку, а в ней дырки. И во все места дырок нужно что-то подставить. И опять вы не можете записать библиотеку в память один раз, что вам очень хочется.

    Поэтому вместо этого просто заводят табличку со смещениями, и теперь все call 'ы обращаются туда, и туда же динамический загрузчик подставляет истинные адреса функций. Эта таблица в Linux называется global offset table, а в Windows — import address table.

    Но на самом деле есть ещё проблема. Давайте посмотрим, что происходит, когда мы делаем

    void foo(); void bar()

    Как мы обсуждали, тут будет call и пустой адрес (до линковки пустой). А что будет, если foo — это внешняя функция из какой-то библиотеки? Тогда надо бы вместо простого call 'а сделать call qword [got_foo] . Но есть проблема — мы узнаём, откуда эта функция, только на этапе линковки, а компилировать надо раньше. Поэтому компилятор call foo , а потом, если это было неправильно, просто создаёт свою функцию foo , которая является прослойкой для jmp qword [got_foo] . Такие заглушки, которые просто совершают безусловный переход по глобальной таблице смещений имеют название. В Linux их называют PLT (procedure linkage table), а в Windows как-то по-другому.

    Но в Linux PLT используется ещё для одной цели. Рассмотрим, скажем, LibreOffice, в котором сотни динамических библиотек с тысячами функций в каждой. Поэтому заполнение GOT — это долго. И нам не хочется смотреть, где лежит каждая функция, после чего записывать её в таблицу. Поэтому эту операцию сделали ленивой:
    GOT заполняется специальными заглушками, которые динамически ищут в хэш-таблице настоящий адрес функции, после чего записывают его в GOT вместо себя, и вызывают эту функцию, чтобы она отработала. В Microsoft по-умолчанию отложенная загрузка не используется, но его можно включить (delayed DLL loading или как-то так называется). Это фича загрузчика, а не самой Windows, и делает эта фича примерно то же самое. Однако есть разница. В Linux отсутствие библиотеки не позволяет запустить программу. В Windows же библиотека подгружается при первом вызове функции оттуда, что, по их словам, сделано чтобы вы могли за' if 'ать ситуацию, когда библиотеки нет.

    Офф-топ на тему «Как страшно жить».

    Поговорим про изменение so-файлов. Давайте возьмём и во время работы программы поменяем библиотечку на диске, втупую вписав туда другой текст. Результат поразителен — работа программы также изменится. Почему? Мы же, вроде как, исполняем библиотеку из оперативки, а не с диска. А дело в том, как работает copy-on-write в операционных системах. Когда вы пишете в некоторую страницу, вам копируют её. Но когда кто-то извне пишет в страницу, вам не дают копию старых данных. С исполняемым файлом такое не прокатывает, кстати. Это потому, что вашу программу загружает ядро, и оно может запретить изменять бинарники, а библиотеку загружает ваша программа, которая такого механизма не имеет.

    Кстати, изменение и перекомпиляция — разные вещи. И если вы во время работы программы перекомпилируете библиотеку, она не обновится. Связано это с тем, что перекомпилированная библиотека — это новый файл, а не старый. По сути вы удалили старую библиотеку и создали новую, вместо того, чтобы библиотеку изменить. А в Linux пока кто-то имеет доступ к файлу, файл не удаляется до конца. И поэтому в вашей программе всё ещё есть та самая библиотека, которую вы загружали (а не новая).

    Детали работы с динамическими библиотеками в Windows.

    Никто не удивиться, что набор

     call foo@PLT foo@PLT: jmp qword [got_foo] 

    не очень эффективен (три обращения в память вместо одного). Поэтому в Windows есть спецификатор __declspec(dllimport) , который сразу вместо call 000000 и замены нулей на foo@plt вставляет call qword [got_foo] .

    Ещё в Windows есть такая штука как .def-файл — линковщик экспортирует из вашей DLL-ки только то, что нужно, и в .def-файле указывается, что именно. Это хорошо работает в C, где имена символов и имена функций совпадают, но не очень хорошо в C++, где вам придётся писать сложные декорируемые имена. Поэтому есть второй вариант — написать на самой функции __declspec(dllexport) .

    И вроде бы всё хорошо, вы метите функции, которые экспортируете как __declspec(dllexport) , которые импортируете — как __declspec(dllimport) и всё классно работает. Но есть проблема: вы и в библиотеке, и в коде, который её использует, подключаете один заголовочный файл, где объявлена функция. И непонятно, что там писать: __declspec(dllexport) или __declspec(dllimport) . Для этого заводится специальный макрос под каждую библиотеку, которым отличают, саму DLL вы компилируете или кого-то с её использованием.

    Есть ещё одна проблема. Непонятно, что делать с глобальными переменными. Там проблема ещё более страшная: вы сначала читаете адрес переменной из GOT (извините, IAT), а потом по полученному адресу обращаетесь. Тут уже никакую функцию-прослойку не написать, увы. Поэтому если вы не пометите глобальную переменную как __declspec(dllimport) , тот тут вы уже точно совсем проиграете, у вас линковка не получится.

    А ещё реализация DLL в Windows нарушает правила языка: если вы напишете inline -функцию в заголовочном файле. Она просто откопируется в каждую библиотеку, где вы этот заголовок подключился. С этим вы ничего не сделаете, тут вы просто проиграли.

    Детали работы с динамическими библиотеками в Linux.

    Если вы думаете, что в Windows проблемы с динамическими библиотеками, потому что Windows — какашка, то сильно заблуждаетесь, потому что в Linux нюансов тоже выше крыши.

    Итак, мем первый и основной — interposition. Есть такая переменная окружения, как LD_PRELOAD . Она завставляет динамический загрузчик сначала обшарить в поисках динамических библиотек то, что вы в LD_PRELOAD написали, а уже потом смотреть какие-нибудь RUNPATH 'ы и всё остальное. В частности, так можно подменить аллокатор (и мы так и делали, когда экспериментировали с mmap 'ом и munmap 'ом). Такая подмена и называется interposition. Теперь что же у него, собственно, за нюансы есть.

    int sum(int a, int b) < return a + b; >int test(int x, int y)

    Тут при обычной компиляции вторая функция просто вернёт свой второй аргумент. А при компиляции с -fpic, вы сможете подменить sum , а значит оптимизации не будет. Чтобы это пофиксить, можно пометить sum как static (тогда эта функция будет у вас только внутри файла, а значит его не поменять извне) или как inline (потому что inline полагается на ODR, а значит функция должна быть везде одинаковой). Но есть ещё способ.
    Linux по-умолчанию считает, что все функции торчат наружу (т.е. как __declspec(dllexport) в Windows). А можно их пометить, как не торчащие наружу, а нужные только для текущей компилируемой программы/библиотеки: __attribute__((visibility("hidden"))) .

    На самом деле атрибут visibility может принимать несколько различных значений ( "default" , "hidden" , "internal" , "protected" ), где пользоваться сто́ит только вторым, потому что первый и так по-умолчанию, третий заставляет ехать все адреса, а четвёртый добавляет дополнительные аллокации.
    При этом также есть различные ключи компиляции (типа -B symbolic), которые тем или иным образом немного меняют поведение, и пояснить разницу между ними всеми вам могут только избранные. И каждый из них может поменять вам поведение так, что вы легко выстрелите себе в ногу. То есть глобально в Linux поведение по умолчанию делаем вам хорошо, но, возможно, немного неоптимизированно, а когда вы начинаете использовать опции, вы погружаетесь в такую бездну, что ускорение заставляет вас очень много думать. Причём замедление от динамических библиотек может быть достаточно сильным: если взять компилятор clang-LLVM и компилировать при помощи его ядро Linux’а, то в зависимости от того, сложен ли clang-LLVM в один большой файл или разбит по библиотечкам, время компиляции отличается на треть. Поэтому ключи использовать придётся.
    Один из самых безопасных из них — -fno-semantic-interposition. Это не то же самое, что и -fno-interposition потому, что бинарнику всё равно можно дать LD_PRELOAD , однако в нашем случае функция test будет оптимизирована.
    Ещё один полезный ключ — -fno-plt. Он по сути вешает оптимизацию такую же, как __declspec(dllimport) , но на весь файл, поэтому функции, написанные в нём же, замедляются. Чтобы не замедлялись — visibility("hidden") . Вообще всё это детально и подробно рассказано не будет, если вам интересно, гуглите и читайте/смотрите по теме.
    Впрочем, всякие -fno-plt и прочие штуки нужны нам тогда и только тогда, когда мы не включили linking-time оптимизации. В GCC все наборы ключей нафиг не нужны, если включить -flto. Так что в перспективе -flto и -fno-semantic-interposition — это единственное, что вам может быть нужно. Но только в перспективе.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *