Skip to content

Latest commit

 

History

History
648 lines (468 loc) · 37.7 KB

File metadata and controls

648 lines (468 loc) · 37.7 KB

5. Компиляция с системными библиотеками

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

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

Программа

vector_distance_calculator

Что делает

Принимает от пользователя, в качестве аргументов, два вектора формата x:y, высчитывает расстояние между ними и выводит на экран.

Пример выполнения

./ctor_distance_calculator 25.97:13.4 99.88:-105.34
Input a: (25.970000, 13.400000), b: (99.880000, -105.340000)

Distance between them: 1764

ctor_distance_calculator.out 25.97:13.4 25.97:13.4
Input a: (25.970000, 13.400000), b: (25.970000, 13.400000)

Distance between them: 0

Что внутри

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

Исходный код

vector_distance_calculator.c

Ссылка

Пояснения

Подключение необходимых заголовков

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
  • stdio.h - библиотека ввода/вывода, используем функцию printf() для вывода сообщений.
  • stdlib.h - стандартная библиотека, используем функцию функцию strtod() для конвертации строки в вещественные числа.
  • math.h - библиотека математических функций, используем функцию pow() для возведения числа в степень, функцию sqrt для извлечения квадратного корня - две необходимые операции для вычисления расстояния между векторами.

Объявляем структуру math_vector2

Мы работаем с двумерными векторами, у вектора два числовых значения - x и y. Поэтому назовем структуру math_vector2.

Почему приставка 'math_' в имени?

В языке C++ есть такая структура данных, как динамический массив и он имеет название vector. В языке C тоже пытаются такое реализовывать, но своими руками и также называют его vector. По этим, непонятным мне причинам, простое имя vector2 может вызвать путаницу у C/C++ разработчиков, поэтому я добавил префикс math_ к названию своей структуры.

Простая структура с двумя полями X и Y :

typedef struct
{
        double x;
        double y;

} math_vector2;

Объявляем прототипы наших функций

math_vector2 str_to_vec2(char* str);
double calculate_distance(math_vector2 a, math_vector2 b);
  • str_to_vec2() - эта функция будет считывать из строки вида "5.22:3.44" и создавать на их основе объект math_vector2 со значениями x = 5.22 и y = 3.44.

  • calculate_distance() - здесь мы будем рассчитывать расстояние между двумя векторами.

Передаем строковые аргументы в функции

char* vec_str_a = argv[1];
char* vec_str_b = argv[2];

math_vector2 vec_a = str_to_vec2(vec_str_a);
math_vector2 vec_b = str_to_vec2(vec_str_b);

Преобразуем аргументы в векторы

Начинается вызов нашей функции - str_to_vec2() по преобразованию строки в объект вектора.

math_vector2 str_to_vec(char* str)

С помощью функции strtod, выделяем из строки 2 значения типа double- x и y:

char* endptr;
double x = strtod( str , &endptr );

if(*endptr != ':')
{
    fprintf(stderr, "Format error: expected ':'\n");
    exit(1);
}

double y = strtod( endptr + 1, NULL);

Сохраняем эти значения в объекте math_vector2 и возвращаем его из функции:

math_vector2 output;
output.x = x;
output.y = y;

return output;

Высчитывание расстояния между двумя векторами

Перед выходом из программы, нужно высчитать расстояние между векторами функцией calculate_distance():

double calculate_distance(double x, double y)

Внутри себя она реализовывать формулу по нахождению расстояния между векторами.

  1. Делаем разницу X и Y между векторами:
double diff_x = b.x - a.x;
double diff_y = b.y - a.y;
  1. Возводим их в степень 2:
double squared_diff_x = pow(diff_x, 2);
double squared_diff_y = pow(diff_y, 2);
  1. Получаем из их суммы квадратный корень, это и есть расстояние:
double distance = sqrt(squared_diff_x + squared_diff_y);

return distance;

Выводим результат

В конце программы (функция main()) отобразим результат в выводе и сделаем успешный выход из программы через exit(0).

double distance = calculate_distance(vec_a, vec_b);

printf("Distance between them: %d\n", distance);

exit(0);

Компиляция с библиотекой

При попытке скомпилировать программу:

gcc vector_distance_calculator.c -o vector_distance_calculator.out

Мы получим ошибки о undefined reference to XXX , где XXX - имя незнакомой компилятору функции.

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

Чтобы это сделать и компиляция прошла успешно, нужно добавить флаг -lm . Префикс l означает library после l идет название библиотеки, в нашем случае она называется просто m (libm.so).

gcc vector_distance_calculator.c -lm -o vector_distance_calculator.out

И программа скомпилировалась вместе с библиотекой math !

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

  • libpthread - библиотека для работы с потоками.
  • libm - нужная нам math.
  • и так далее...

А как вообще понять, требуется ли явная компоновка для определенной функции из стандартной библиотеки или нет?

Хороший вопрос, четкого списка нет. Почему, например unistd.h или stdlib.h не требует явного указания компоновки , а math.h требует? Так исторически сложилось, не всем программам по умолчанию нужен math.h или pthreads.h.

Если вы столкнулись с ошибкой, связанной с undefined reference to XXX, где XXX имя функции из стандартной библиотеки, например:

  • sqrt из math.h
  • pthread_create из pthread.h Откройте документацию к этой функции через команду man, например для функции sqrt:
man sqrt

И вы увидете, что в первых строках описания этой функции указано из какой она библиотеки и как ее подключить при компиляции:

sqrt(3)

NAME
      sqrt, sqrtf, sqrtl - square root function

LIBRARY
      Math library (libm, -lm)

. . . . . . 

строка

Math library (libm, -lm)

дает имя библиотеки , где находится эта функция - libm и аргумент компилятору, для подключения этой библиотеки - -lm

6. Создание собственной библиотеки

Чтобы выделять блоки кода и к ним имели доступ другие программы, чтобы пользоваться уже созданной логикой, можно создавать библиотеки самому. Здесь мы создадим свою библиотеку custom_logger и клиентскую программу, которая будет с ней работать (вызывать функции из этой библиотеки) - client_custom_logger.

Библиотека и программа

  • custom_logger - библиотека с тремя функциями, которые выводят сообщения на экран.
  • client_custom_logger - исполняемая программа, которая вызывает функции из библиотеки custom_logger.

Что делает

Библиотека custom_logger имеет три функции:

void log_error(char* context, char* message);
void log_message(char* context, char* message);
void log_warning(char* context, char* message);

которые выводят сообщения в стандартный вывод.

Программа client_custom_logger вызывает эти функции, передавая им параметры. Библиотечные функции выводят сообщения, пример вывода:

ERROR -> context: MAIN_CTX > > > message: MY ERROR IS HERE.
MESSAGE -> context: MAIN_CTX > > > message: MY MESSAGE IS HERE.
WARNING -> context: MAIN_CTX > > > message: MY WARNING IS HERE.

Что внутри

  • Простейшие функции вывода сообщений у библиотеки.
  • Сборка библиотеки и размещение ее в папке пользовательских библиотек.
  • Простейшие вызовы этих функций в программе-клиенте.
  • Сборка программы с компоновкой библиотеки.

Исходный код

Библиотека custom_logger

code_sources/linux-c-system-programming-essentials/5-creating-custom-library/custom_logger/...

  • src - папка с кодом на Си.
  • include - папка с заголовком функций.
  • lib - папка со сборкой библиотеки.

Ссылка

Исполняемая программа client_custom_logger

code_sources/linux-c-system-programming-essentials/5-creating-custom-library/client_custom_logger/...

  • client_custom_logger.c - код программы.
  • include/custom_logger.h - заголовок из библиотеки, для подключения функций.

Ссылка

Пояснения

Подключение необходимых для библиотеки

#include <stdio.h>
  • stdio.h - обработка ввода/вывода , используется функция printf() для вывода.

Написание кода для библиотеки

Библиотека имеет всего три функции, которые выводят разные сообщения, с параметрами.

Объявление функций библиотеки происходит в файлах *.h:

include/custom_logger.h:

#ifndef CUSTOM_LOGGHER_H
#define CUSTOM_LOGGER_H

void log_error(char* context, char* message);
void log_message(char* context, char* message);
void log_warning(char* context, char* message);

#endif

Реализация этих функций будет находится в файлах *.c:

src/custom_logger.c:

void log_error(char* context, char* message)
{
    printf("ERROR -> context: %s > > > message: %s\n", context, message);
}
// ... и так далеее

Организация файлов для библиотек

  • папка ./include/... для заголовков, где объявляются функции библиотеки. Они нужны для програм-пользователей этой библиотеки (им же нужно знать какие функции там объявлены).
  • папка ./src/... для кода на Си, где будут размещаться реализации функций из заголовков.
  • папка ./lib/... для сборок библиотеки.

Итого, для распространения библиотеки нужны папки с их содержимым - include и lib, чтобы программы-клиенты могли их использовать.

Сборка библиотеки

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

Порядок сборки библиотеки:

  1. Сборка объектного файла
gcc -Wall -Wextra -pedantic -fPIC -c custom_logger.c

Параметры для gcc:

  • -Wall - полезные предупреждения от компилятора о подзрительном коде (анализ).
  • -Wextra - дополнительный анализ кода, для еще несколько полезных предупреждений.
  • -pedantic - ругаться если что-то выходит из стандарта Си.
  • -fPIC - генерация позиционно-независимого кода (PIC - Position-Independed Code). Это очень важно для подключения библиотек, этот параметр обязателен для генерации библиотеки.
  • -c компиляция только объектного файла (o файл), без линковки.
  1. Сборка библиотеки
gcc -shared -Wl,-soname,libcustomlogger.so -o libcustomlogger.so custom_logger.o

Параметры для gcc:

  • -shared - указание что нам нужно собрать разделяемую библиотеку (so - shared library).
  • -Wl,-soname,libcustomlogger.so - указание для компоновщика, чтобы он добавил в метаданные библиотеки ее SONAME. Это нужно для того, чтобы когда программа искала нужную библиотеку по имени, динамический загрузчик библиотек в системе быстро нашел ее по имени libcustomlogger.so.
  • -o libcustomlogger.so - указание имени выходного файла, который получится в результате.
  • custom_logger.o - входной объект, из которого собирается библиотека (собирали в шаге 1).

Размещение библиотеки в каталоге библиотек

Чтобы программы находили нашу библиотеку, ее нужно скопировать в одно из мест:

  • В системную папку - /usr/lib64/.
  • В папку с программой, которая от нее зависит и указывать при сборке программы через -rpath относительный путь к библиотеке. (Портируемый способ).
  • В какую либо другую папку и указать ее в переменной LD_LIBRARY_PATH.

В итоге, какой бы способ не выбрали, там должнен лежать файл сборки нашей библиотеки - libcustomlogger.so.

Для этой статьи, мы выберем самый простой способ, копирование в системную папку - /lib/lib64.

Заметка - переменная LD_LIBRARY_PATH

LD_LIBRARY_PATH содержит в себе дополнительные папки, где искать библиотеки. Эта переменная нужна для системного динамического загрузчика библиотек - ld.so , в ней он дополнительно ищет, собственно библиотеки. Как и любую переменную, ее можно определить в текущем сеансе оболочки:

export LD_LIBRARY_PATH="/opt/mylib/lib:$LD_LIBRARY_PATH"

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

Написание и сборка программы-клиента

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

#include "include/custom_logger.h"

Далее, можно использовать эти функции в коде программы:

int main(void)
{
    log_error("MAIN_CTX", "MY ERROR IS HERE.");
    log_message("MAIN_CTX", "MY MESSAGE IS HERE.");
    log_warning("MAIN_CTX", "MY WARNING IS HERE.");
    return 0;
}

При попытке собрать программу обычным способом:

gcc client_custom_logger.c -o client_custom_logger.out

Мы получим ошибки undefined reference с указанием на функции из библиотеки. Компилятор просто не знает откуда взять реализацию этих функций.

Чтобы указать нужную нам библиотеку, нужно проставить аргумент -l<имя_библиотеки>:

gcc client_custom_logger.c -lcustomlogger -o client_custom_logger.out

Обратите внимание, что вместо полного имени libcustomlogger.so мы заменяем строку lib на -l.

После этого программа скомпилируется и будет работать как нужно:

./client_custom_logger.out 
ERROR -> context: MAIN_CTX > > > message: MY ERROR IS HERE.
MESSAGE -> context: MAIN_CTX > > > message: MY MESSAGE IS HERE.
WARNING -> context: MAIN_CTX > > > message: MY WARNING IS HERE.

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

Ранее был рассмотрен процесс сборки и подключения динамической библиотеки , теперь рассмотрим статические библиотеки.

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

Плюсы и минусы статической библиотеки

Плюсы:

[+] Простая сборка - нет сложных операций с компоновщиком, библиотека всего лишь является частью кода программы.

[+] Легко распространять вашу сборку - так как нет зависимости от наличия того или иного файла динамической библиотеки. Все необходимое вшито в вашу программу.

[+] Чистота кода - можно делить части программы на статические библиотеки, по принципу "разделяй и властвуй". Этот процесс легче чем с динамической библиотекой, так как - два плюса выше, упрощают процесс разделения логики на библиотеки.

Минусы:

[-] Жирнее размер сборки - так как статическая библиотека становится частью вашей программы, то и размер ее, увеличится.

[-] Необходимость пересборки - если вы внесли правку в библиотеку, то нужно пересобирать все приложение, ведь она часть приложения.

[-] Больше затрат времени на сборку - если внесли правку в приложение и хотите его пересобрать, то время увеличится, так как часть сборки уходит на статические библиотеки.

Плюсы и минусы динамической библиотеки

Во многом плюсы и минусы разных типов библиотек инверсивны , то есть если у статической библиотеки есть [-], то это [+] у динамической и так далее.

Плюсы:

[+] Меньше размер сборки - динамическая библиотека является независимым файлом, и не является частью вашей сборки, поэтому ваша сборка будет весить меньше, при наличии динамической библиотеки, относительно статической.

[+] Динамические обновления - если вы внесли правку в библиотеку, то не нужно пересобирать все приложение, так как динамическая библиотека независима.
Таким образом вы сможете обновлять логику в библиотеках, не пересобирая приложение, но пересобирая библиотеки.

[+] Меньше затрат времени на сборку - вы собираете только то что есть в вашей программе, динамические библиотеки живут отдельной жизнью.

Минусы:

[-] Сложный процесс сборки библиотеки - (см. гайд по сборке выше) этапы сборки нетривиальны, нужно прокидывать разные флаги компилятору и компоновщику.
Далее нужно вшить зависимость от библиотеки в само приложение, тоже добавляя всякие флаги.

[-] Меньше переносимость приложения - если пользователь захочет воспользоваться вашей программой, то ему нужно убедиться,
что динамические библиотеки у него имеются, если их нет - он получит ошибку и будет искать как запустить ваше приложения (если не забьет на него).

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

Пример статической библиотеки

Статическая библиотека и программа которая ее использует:

Также имеется заголовок функций библиотеки mymathlib.h , который используется программой main.c .

Этапы сборки библиотеки:

  1. Сборка объектного файла С библиотеки нам нужен только объектный файл. Указываем для gcc флаг -c, который
    означает - проделать только компиляцию, без компоновщика.
gcc -c mymathlib.c -o mymathlib.o 
  1. Архивирование объектного файла
    Статическая библиотека - это всего лишь архив, с расширением *.a, внутри которого объектные файлы библиотеки.
ar rcs libmymathlib.a mymath.o 

ar - утилита для создания архивов статических библиотек
rcs - набор флагов, где r = вставка объектных файлов, c = создание архива если его нет, s = установить
индекс для поиска.

В итоге получается файл libmymathlib.a .

Всего два шага и статическая библиотека готова.

Сборка клиента с библиотекой:

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

gcc main.c -L. -lmymathlib -o main
  • -L. - устанавливаем поиск библиотеки в текущей папке.
  • -lmymathlib - подключение файла libmymathlib.a .

7. Пошаговое рассмотрение процесса компиляции

Программа

mult_prog

Что делает

Выводит результат умножения 25 на 4.

Пример выполнения

./mult_prog.out 
mult(25,4) = 100

Что внутри

  • Использование внешнего файла с функцией mult().
  • Вывод результата mult(25,4) на экран.

Исходный код

  • Исполняемая программа - public/code_sources/linux-c-system-programming-essentials/6-full-compilation-process/mult_prog.c
    Ссылка

  • Реализация функции mult() - public/code_sources/linux-c-system-programming-essentials/6-full-compilation-process/mult.c
    Ссылка

  • Заголовок с функцией mult() - public/code_sources/linux-c-system-programming-essentials/6-full-compilation-process/mult.h
    Ссылка

Пояснения

Компиляция нескольких файлов

Программы могут иметь больше одного файла исходного кода .c . В данном примере, у нас есть два файла исходного кода:

  • mult.c - реализация функции mult(), которая объявлена в заголовке mult.h.
  • mult_prog.c - использование функции mult() из заголовка mult.h в функции main() , то есть это наша исполняемая программа.

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

Почему компилятору не передаются заголовки .h ?

Заголовки автоматически встраиваются компилятором в исходный код .c файлов, на место где они используются, директивой препроцессора include "header.h". То есть на место include "mult.h" вставится объявление функции mult() из файла заголовка.

Указание файлов для компиляции идет просто, как и раньше, просто вместо одного файла передаем несколько: mult.c и mult_prog.c:

gcc -Wall -Wextra -pedantic mult.c mult-prog.c -o mult_prog.out 

Сборка пошагово

Мы можем скомпилировать программу пошагово, чтобы увидеть весь процесс сборки программы.

Сборка программы состоит из четырех шагов:

  1. Препроцессор
  2. Компиляция
  3. Сборка
  4. Компоновка

1. Препроцессор

Первым шагом идет препроцессор, именно поэтому конструкция #include и ```#define```` называется директивой препроцессора.

Препроцессор просто замещает директиву #include на содержимое файла из заголовка .h и для других директив работает точно также, наприер #define - подставляет использование макроса в коде на значение из #define, и так далее.

Так как файлов исходного кода у нас несколько, то и выполнить нужно несколько раз:

gcc -E -P mult_prog.c -o mult_prog.i

gcc -E -P mult.c -o mult.i

В итоге получится два файла , обработанные препроцессором, куда вставлены нужные значения из заголовков: mult_prog.i и mult.i.

2. Компиляция

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

Чтобы собрать файлы ассемблера из файлов обработанных препроцессором, нужно выполнить следующее:

gcc -S mult_prog.i -o mult_prog.s

gcc -S mult.i -o mult.s

В итоге получится два файла с кодом ассемблера: mult_prog.s и mult.s.

3. Сборка

Третий шаг называется сборкой (assembly) . На этом шаге файлы исходного кода ассемблера, созданные на предыдущем шаге, собираются в объектные файлы (object files) .

Итого нам нужно взять два файла ассемблера и создать из них два объектных файла:

gcc -c mult_prog.s -o mult_prog.o

gcc -c mult.s -o mult.o

Это уже двоичные файлы (binary files), и мы не можем их открыть в текстовом редакторе. Можно воспользоваться коммандой file чтобы увидеть метаинформацию про эти двоичные файлы:

file mult_prog.o

mult_prog.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

4. Компоновка

Последний шаг - объеденить все объектные файлы в единый файл. Этим занимается компоновщик (linker):

gcc mult.o mult_prog.o -o mult_prog.out

В итоге мы собрали из двух объектных файлов, один двоичный файл, который и является нашей исполняемой программой - mult_prog.out.

./mult_prog.out

mult(25,4) = 100

Заключение

Сборка программы занимает 4 шага, сборку программы mult_prog можно описать так:

  1. Препроцессором транслируем файлы mult_prog.c, mult.c в -> mult_prog.i, mult.i.

  2. Компилятором транслируем файлы mult_prog.i, mult.i в язык ассемблера для целевой платформы -> mult_prog.s, mult.s.

  3. Сборщиком транслируем файлы ассемблера mult_prog.s, mult.s в двоичные объектные файлы -> mult_prog.o, mult.o.

  4. Компоновщиком объединяем объектные файлы mult_prog.o, mult.o в единый двоичный файл -> mult_prog.out.

Следующая статья