LinuxCamp | DevOps
Обо мне: C/C++/Linux эксперт. Говорим про разработку, Linux, DevOps, сети и администрирование. Админ (реклама): @XoDefender Чат: @linuxcamp_chat Менеджер: @Spiral_Yuri Биржа: https://telega.in/c/linuxcamp_tg РКН: https://clck.ru/3RWA3C
Mostrar más📈 Análisis del canal de Telegram LinuxCamp | DevOps
El canal LinuxCamp | DevOps (@linuxcamp_tg) en el segmento lingüístico de Ruso es un actor destacado. Actualmente la comunidad reúne a 13 918 suscriptores, ocupando la posición 9 160 en la categoría Tecnologías y Aplicaciones y el puesto 47 344 en la región Rusia.
📊 Métricas de audiencia y dinámica
Desde su creación el невідомо, el proyecto ha mostrado un crecimiento acelerado, reuniendo a 13 918 suscriptores.
Según los últimos datos del 09 julio, 2026, el canal mantiene una actividad estable. En los últimos 30 días la variación de miembros fue de -116, y en las últimas 24 horas de -26, conservando un alto alcance.
- Estado de verificación: No verificado
- Tasa de interacción (ER): El promedio de interacción de la audiencia es 21.35%. Durante las primeras 24 horas tras publicar, el contenido suele obtener 11.55% de reacciones respecto al total de suscriptores.
- Alcance de las publicaciones: Cada publicación recibe en promedio 2 974 visualizaciones. En el primer día suele acumular 1 608 visualizaciones.
- Reacciones e interacción: La audiencia responde de forma activa: el promedio de reacciones por publicación es 24.
- Intereses temáticos: El contenido se centra en temas clave como linuxcamp, ядро, linux, диск, docker.
📝 Descripción y política de contenido
El autor describe el recurso como un espacio para expresar opiniones subjetivas:
“Обо мне: C/C++/Linux эксперт. Говорим про разработку, Linux, DevOps, сети и администрирование.
Админ (реклама): @XoDefender
Чат: @linuxcamp_chat
Менеджер: @Spiral_Yuri
Биржа: https://telega.in/c/linuxcamp_tg
РКН: https://clck.ru/3RWA3C”
Gracias a la alta frecuencia de actualizaciones (últimos datos recibidos el 10 julio, 2026), el canal mantiene la vigencia y un amplio alcance. La analítica demuestra que la audiencia interactúa activamente con el contenido, lo que lo convierte en un punto de referencia dentro de la categoría Tecnologías y Aplicaciones.
source <filename>.build
Мне сразу стало интересно, что лежит в .build файле... Там был прописан обычный bash скрипт, который определял ряд переменных и функций для последующей компиляции и установки приложения:
#!/bin/bash
BDIR="Release"
src_config() {
cmake -DCMAKE_BUILD_TYPE=${BDIR}
}
src_compile() {
make
}
Следующий вопрос - почему именно source? Можно же выполнить скрипт внутри командной оболочки через обращение к файлу через путь:
./<filename>.build
Как оказалось, есть несколько причин, по которым данная команда существует и используется, давайте разбираться.
Принцип работы команды source:
Данная команда является полным аналогом оператора . - встроенным в оболочку инструментом, который позволяет выполнять ряд инструкций целевого файла в рамках текущего "shell environment", что, как минимум, сохраняет доступ ко всем, локально определенным, переменным оболочки:
$ MY_VAR=123
$ cat script.sh
echo $MY_VAR
$ source script.sh
123
$ . script.sh
123
Когда вы вызываете source и передаете ему файл на чтение, все команды внутри будут последовательно выполнены так, будто вы их вручную прописали в терминале. Файл тут можно передавать несколькими способами:
1) Через абсолютный/относительный путь:
source ./XTouch/xtouch.sq
2) По имени. В данном случае утилита попытается найти файл в одном из каталогов, указанных для переменной $PATH. Если попытка окончилась провалом, программа попытается взять его из текущей активной директории:
source xtouch.sq
Отличия в запуске скрипта по пути "./" и через команду source:
Как говорилось ранее, команда source выполняет инструкции в рамках текущей оболочки, не отпочковываясь и не создавая новый процесс, что позволяет использовать локальные переменные.
Запуск скрипта по пути, в свою очередь, создает дочерний процесс и запускает дополнительную оболочку со своим пуллом переменных:
$ ps -auxf
\_ /bin/bash
\_ /bin/bash ./test.sh
Если переменная оболочки не была переведена в переменную окружения через ключевое слово "export", дочерний процесс не сможет к ней обратиться. В таком случае, необходимо явно передать и определить ее при запуске:
$ MY_VAR=123
$ source script.sh
123
$ MY_VAR="$MY_VAR" script.sh
123
Также, нужно помнить, что, когда вы вызываете скрипт по имени, для корректной работы следует прописывать shebang "#!<path to app>" в начале файла. Таким образом, системе явно будет понятно то, каким приложением необходимо обработать указанные инструкции:
#!/usr/bin/make -f
install:
dh_installdirs
dh_install
В случае с source это не имеет смысла - тут мы работаем на уровне текущей оболочки. Операционной системой не будет запущен очередной интерпретатор, как следствие, данная строка будет проигнорирована.
Пример использования команды source:
Как говорилось в начале поста, я наткнулся на source, пока шарился на сборочном сервере. Стояла задача дебианизировать пакет, который должен был при инсталляции корректно встроить в систему ряд медиа библиотек и конфигов для декодирования видео на GPU.
Как правило, проекты такой сложности требуют выполнения немалого количества действий на разных этапах: при конфигурации нужно передать "миллион" флагов в cmake, при установке построить глубокую иерархию директорий, перенести файлы по путям и т.д.
Все эти процессы могли бы быть отображены и проработаны в файле rules, но тогда в нем было бы безумно тяжело ориентироваться. Было принято решение разделить логику на группу функций: src_install(), src_compile(), src_config(). После чего, локально, не в рамках новой оболочки, их определять и вызывать.
Определения были скомпонованы в отдельном файле, что позволило красиво и локанично оформить rules:
```shell
install:
source debian/pipeline.build && src_config && src_install
$ lsof
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
init 1 root cwd DIR 253,0 4096 2 /
init 1 root txt REG 253,0 145180 147164 /sbin/init
init 1 root 4w FIFO 0,8 0t0 8449 pipe
...
В результате мы получаем пулл информации про активные утилиты и используемые ими ресурсы (файлы, каталоги, сокеты и т.д.).
Спектр возможных значений для каждого поля тут довольно широк, поэтому за детальной информацией предлагаю обратиться к мануалу.
Давайте на конкретном примере посмотрим, о чем нас проинформирует lsof:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
prog_wr 9957 xodefender 3w REG 253,0 46442 1445679 /home/xodefender/input.txt
1. COMMAND - название утилиты, которая использует ресурс .
2. PID - идентификатор процесса для программы prog_wr.
3. USER - имя пользователя, которому принадлежит процесс.
4. FD - поле, предоставляющее информацию о ресурсе, который использует процесс. Значение поля может состоять из нескольких частей. В случае принадлежности к зарезервированному типу, значениями могут быть: cwd, rtd, txt, mem и т.д.
Если же, как в данном случае, речь идет о ресурсе, к которому можно обратиться по локальному дескриптору, структура может быть следующей: порядковый номер дескриптора (3) + права доступа (w - открыт на запись).
5. TYPE - тип ресурса. В данном случае используется обычный файл (REG). Также, ресурсами могут быть: директории (DIR), каналы передачи данных (FIFO), сетевые сокеты (INET) и т.д.
6. DEVICE - номер устройства, к которому относится ресурс (в формате major:minor). Обычно, информация берется об устройствах из каталога dev.
7. SIZE/OFF - размер файла input.txt в байтах. Если размер ресурса получить не удается, значением поля будет его отступ в памяти.
8. NODE - номер ресурса в таблице индексного дескриптора "inode". В нашем случае, файл input.txt числится под номером 1445679 в общесистемной таблице.
9. NAME - имя ресурса, представленное абсолютным путем.
Примеры использования команды lsof:
Вот и мне эта программка пригодилась для того, чтобы понять, какие процессы используют интересующую меня динамическую библиотеку.
Если у вас уже есть подозрения и вы знаете PID процесса, можно воспользоваться флагом '-p', который отобразит информацию только для указанного процесса:
$ lsof -p 23708
vlc 23708 root mem REG 8,2 264888 2365462 /usr/lib64/vlc/plugins/codec/libomxil_plugin.so
Если же у вас 0 идей, можете прямо прописать путь к целевому ресурсу и утилита вам покажет все процессы, которые его используют:
$ lsof ./libomxil_plugin.so
vlc 23708 root mem REG 8,2 264888 2365462 ./libomxil_plugin.so
dpkg -i packageName.deb
Также существует ряд утилит семейства dpkg, которые входят в пакет dpkg-dev и необходимых для создания и администрирования архивов формата .deb:
$ dpkg -L dpkg-dev | grep -i bin
/usr/bin/dpkg-architecture
/usr/bin/dpkg-buildapi
/usr/bin/dpkg-buildflags
...
А что же такое apt (Advanced Package Tool)? Apt - это набор утилит высокого уровня, необходимых для администрирования .deb пакетов и работы с внешними репозиториями.
Apt, в отличии от dpkg, позволяет подтягивать из локальных либо удаленных репозиториев необходимые зависимости, мониторить список доступных пакетов, следить за обновлениями и много-много чего еще:
$ apt-cache search mesa
libd3dadapter9-mesa - state-tracker for Direct3D9
mesa-vdpau-drivers - Mesa VDPAU video acceleration drivers
mesa-vulkan-drivers - Mesa Vulkan graphics drivers
...
Прописать список источников, с которыми менеджер apt будет работать, можно в файл "/etc/apt/sources.list":
$ cat /etc/apt/sources.list
Types: deb
URIs: http://ports.ubuntu.com/ubuntu-ports/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
RPM - это расширение для пакетов, используемых в операционных системах, основанных на Red Hat, это вся ветка дистрибутивов: Fedora, OpenSUSE, Red Hat, CentOS и т д.
Изначально это пакетное расширение и одноименный менеджер были разработаны в компании Red Hat еще в 1997 году и только для их дистрибутива, но затем все это пошло дальше и распространилось в другие операционные системы.
По аналогии с dpkg, утилита rpm используется для того, чтобы локально обслуживать .rpm пакеты. Для более высокоуровневого взаимодействия и возможности работать с репозиториями, необходимо использовать ряд пакетных менеджеров: zypper (OpenSUSE), dnf (Fedora), urpmi (Mageia), yum - для многих дистрибутивов, основанных на Fedora:
rpm -i package.rpm
yum install package.rpm
dnf install package.rpm
Тема пакетирования и распространения софта очень обширная: существует множество техник и практик, которым нужно следовать для того, чтобы качественно разворачивать программы на системах. Пишите, что бы вы хотели узнать в рамках топика, разгоним всю эту историю до серии постов.
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int fd = open("input.txt", O_WRONLY);
char* buffer = "Why am I doing this?";
while(1) {
write(fd, buffer, strlen(buffer));
}
close(fd);
return 0;
}
Этот код просто занимается тем, что постоянно пишет в файл один и тот же буфер с данными.... Да, супер полезная работа, но нам для примера подойдет. Как думаете, к чему это приведет? Правильно, к потреблению CPU в 100%!
Не всегда у пользователя есть возможность и желание копаться в исходниках программы. Для начала хочется базово продебажить софт и проверить, лежит ли проблема на поверхности: в этом нам поможет команда "strace -c", через которую мы поймем, на что тратятся ресурсы системы.
Для того, чтобы получить статистику, нужно просто запустить утилиту с указанными параметрами, выждать необходимое количество времени и прервать выполнение через "ctrl-c":
$ strace -c ./prog
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ------
99.74 0.245607 8 28230 write
0.06 0.000145 24 6 mmap
0.05 0.000111 27 4 mprotect
0.03 0.000073 24 3 openat
0.01 0.000033 16 2 close
.....
Вывод дает нам следующую информацию про каждый системный вызов:
1) time - процент процессорного времени, затраченного на выполнение вызовов (от общего времени);
2) seconds - фактическое процессорное время, затраченное на выполнение вызовов;
3) usecs/call - усредненное количество микросекунд, затраченных на выполнение 1 вызова;
4) calls - количество обращений к вызову;
5) errors - количество вызовов, вернувших ошибку;
6) syscall - название системного вызова;
В результате мы пониманием, что что-то не так: почти все время уходит на выполнение одной и той же операции, причем количество этих операций аномальное...
Дополнительно хочется акцентировать внимание на том, что не все системное процессорное время уходит только на выполнение вызовов. По этой причине временные показатели команды time могут отличаться от того, что покажет strace, так как последний учитывает исключительно то время, которое затрачено ядром на обработку вызовов для целевого процесса:
$ strace -c ./prog
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----
99.95 1.188828 8 140343 write
0.01 0.000157 26 6 mmap
....
------ ----------- ----------- --------- ---------
100.00 1.189413 8 140375 1 total
$ time ./prog
real 0m7.520s
user 0m1.048s
sys 0m6.274s
#include <stdio.h>
int main(int argc, char **argv)
{
printf("Hello, world\n");
return 0;
}
Теперь давайте прогоним это через strace и посмотрим на результат:
$ strace ./hello_world
execve("./hello_world", ["./hello_world"], [/* 50 vars */]) = 0
brk(0) = 0xa7e000 access("/etc/ld.so.nohwcap", F_OK) = -1
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY) = 3
Воу, это лишь малая часть того, что вывелось в консоль... Как говорилось ранее, strace дает нам список всех системных вызовов, сделанных нашей программой.
Сейчас давайте обратим внимание на один конкретный вызов, который произошел входе выполнения и подробно разберем структуру, по которой strace формирует вывод информации:
write(1, "Hello, world\n", 13Hello, world ) = 13
Во-первых, можно увидеть то, что наша программа где-то, вероятно, через библиотечную функцию printf(), делает обращение к вызову write() для того, чтобы записать строку "Hello, world" в файл по дескриптору 1, также известному, как "standard output" (13 - это размер буфера на вывод):
write(1, "Hello, world\n", 13
Во-вторых, так как вывод команды strace и вывод нашей программы произошел в один и тот же терминал, строка "Hello, world" прилипла к информации про сам вызов:
write(1, "Hello, world\n", 13Hello, world
В-третьих, было напечатано возвращаемое значение 13, которое передается нашей программе и говорит о том, что вызов успешно записал 13 символов по указанному дескриптору:
write(1, "Hello, world\n", 13Hello, world ) = 13
Несколько сценариев использования:
1. Выяснить, какие конфиги читает программа на старте. Бывало же у вас такое: запускаете софт, ожидаете получить один результат, а тут Оппа, либо ничего не работает либо происходит то, чего вы вообще не ожидали...
strace может показать вам перечень интересующих вызовов, по которым вы сможете отследить код возврата и понять, в чем дело. В данном случае, нас интересует вызов open(), на который мы указываем через флаг '-e':
$ strace -e open php 2>&1 | grep php.ini
open("/usr/local/bin/php.ini", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/local/lib/php.ini", O_RDONLY) = 4
В результате видно, что процессу не получилось открыть "/usr/local/bin/php.ini" и он пошел к "/usr/local/lib/php.ini", с которым проблем не возникло. Вызов успешно отработал и вернул файловый дескриптор 4.
2. Узнать вызов, на котором процесс завис. Если программа по какой-то причине перестала отвечать на запросы, возможно, она в блокирующем режиме ожидает получения данных или что-то в этом духе. Найти причину можно попробовать через запуск с флагом "-p <pid>":
$ strace -p 15427
Process 15427 attached - interrupt to quit
futex(0x402f4900, FUTEX_WAIT, 2, NULL
Process 15427 detached
Да, мало информации, известно только то, что завис на вызове futex(). Если вы не обладаете дополнительной информацией, то, скорее всего, придется лезть в исходники. Однако, базовое представление о проблеме получить можно.
3. Понять, на что уходит процессорное время. Иногда бывает полезно быстро пристроить strace к программе и посмотреть, на полезную ли работу тратится наше CPU или мы просто чего-то ждем... Запуститься в режиме профилирования можно через флаг '-c':
$ strace -c -p 11084
% time seconds usecs/call calls errors syscall
94.59 0.001014 48 21 select
....
extern long int syscall (long int __sysno, ...) __THROW;
Таким образом, системный вызов write() можно инициировать по разному:
#include <unistd.h>
#include <sys/syscall.h>
int main(void) {
write(1, "hello, world!\n", 14)
syscall(SYS_write, 1, "hello, world!\n", 14);
return 0;
}
Что такое библиотечный вызов?
Библиотечным вызовом можно считать вызов функции стандартной библиотеки C, в которой не содержится прямой реализации системного вызова: системного прерывания, переноса аргументов в определенные, необходимые ядру, регистры, копирования номера системного вызова в регистр (%eax) и т.д.
Эти функции предназначены для решения широкого круга разнообразных задач: открытия файлов, преобразования времени в формат, понятный человеку, сравнения двух символьных строк и т.д.
Многие библиотечные функции вообще не используют системные вызовы (например, функции для работы со сроками). С другой стороны, некоторые библиотечные функции являются надстройками над системными вызовами с платформозависимой реализацией.
Например, библиотечная функция fopen() использует для открытия файла системный вызов open().
Зачастую библиотечные функции разработаны для предоставления более удобного интерфейса вызова по сравнению с тем, что имеется у исходного системного вызова.
Например, функция printf() предоставляет форматирование вывода и буферизацию данных, а внутренний системный вызов write() просто выводит блок байтов.
Аналогично этому функции malloc() и free() выполняют различные вспомогательные задачи, существенно облегчающие выделение и высвобождение оперативной памяти по сравнению с использованием исходного системного вызова brk().
#linuxcore #linux #cpp #cppcore~$ ./prog 100 23 43 69Далее продемонстрированы различные типы переменных в коде и комментарии, указывающие на сегменты их размещения (в случае оптимизаций, часто используемые переменные могут быть помещены в регистры или вообще исключены):
/* Сегмент неинициализированных данных */
char globBuf[65536];
/* Сегмент инициализированных данных */
int primes[] = { 2, 3, 5, 7 };
/* Размещается в фрейме для square() */
static int square(int x)
{
/* Размещается в фрейме для square() */
int result;
result = x * x;
/* Возвращаемое значение передается через регистр */
return result;
}
/* Размещается в фрейме для doCalc() */
static void doCalc(int val)
{
square(val)
if (val < 1000) {
/* Размещается в фрейме для doCalc() */
int t;
t = val * val * val;
}
}
/* Размещается в фрейме для main() */
int main(int argc, char *argv[])
{
printf("File name = %s\n", argv[0]);
printf("Number of arguments = %d\n", argc - 1);
/* Сегмент инициализированных данных */
static int key = 9973;
/* Сегмент неинициализированных данных */
static char mbuf[10240000];
/* Размещается в фрейме для main() */
char *p;
/* Указывает на память в сегменте кучи */
p = malloc(1024);
doCalc(key);
}
В комменты я отправил несколько изображений для визуализации материала.
#linuxcore #linux #cpp #cppcore
$ gdb ./prog
Dump of assembler code for function foo:
=> 0x0000fffff7fa059c <+8>: bl 0xfffff7fa0480 <bar@plt>
0x0000fffff7fa05a8 <+20>: ret
Такая система вызовов:
1. Исключает возможные оптимизации на этапе генерации кода компилятором. Как пример, то же встраивание функций (inline) не будет отработано.
2. Приводит к большему размеру файла библиотеки за счет дополнительных машинных инструкций, необходимых для заполнения таблиц PLT и GOT.
3. Нагружает CPU instruction cache и приводит к более длительному исполнению кода.
4. Приводит к неоднозначности по отношению к используемой реализации: будет вызвана функция, определенная компоновщиком первой. Не факт, что это будет именно тот экземпляр, который реализован внутри библиотеки. Функции, которые определены в исполняемом файле, имеют больший приоритет.
Хммм, для чего тогда нужна такая система, раз уж столько в ней недостатков? По другому не получится, если нужен функционал перехвата функций через переменную LD_PRELOAD. В таком случае можно подменять реализации символов на те, которые указаны в передаваемой библиотеке:
$ gcc -Wall -o prog main.c ./libdemo.so
$ LD_PRELOAD=./libdemo2.so ./prog
Function "bar" is called from libdemo2.so
Если мы соберем зависимость с флагом -Bsymbolic, все вызовы внутренних функций будут происходить напрямую, без PLT заглушек, что с одной стороны значительно уменьшит программный overhead, с другой исключит возможность перехвата символов в рантайме:
$ gdb ./prog
Dump of assembler code for function foo:
=> 0x0000fffff7fa0628 <+8>: bl 0xfffff7fa05f4 <bar>
0x0000fffff7fa0634 <+20>: ret
Таким образом, данный флаг не только приводит к однозначному использованию библиотечных реализаций вложенных функций, но и способствует лучшей производительности программ на этапе выполнения.
Как добиться лучшей производительности?
Все бы хорошо, но хочется полностью развязать компилятору руки и позволить эффективно оптимизировать код. Для данной цели предусмотрен флаг -fno-semantic-interposition, который исключает возможность перехвата символов на этапе компиляции и позволяет применять к функциям ряд различных оптимизаций. В GCC этот флаг по дефолту отключен и активируется под параметром -Ofast.
Для лучшей производительности, рассмотренные флаги лучше использовать вместе, тогда получится не только избавиться от PLT вызовов, но и сгенерировать более оптимальный код:
gcc -Wl,-Bsymbolic -Ofast -shared -o libdemo.so mod1.c// Выводим содержимое main.c
$ cat ./main.c
void xyz(){
printf("main-xyz");
}
void main(){
func();
}
// Выводим содержимое libdemo.c
$ cat libdemo.c
void xyz(){
printf("libdemo-xyz");
}
void func(){
xyz();
}
Собрав разделяемую библиотеку и исполняемый файл и затем запустив полученную программу, мы увидим следующее:
$ gcc -g -c -fPIC -Wall -c libdemo.c
$ gcc -g -shared -o libdemo.so libdemo.o
$ gcc -g -o prog main.c libdemo.so
$ LD_LIBRARY_PATH=./ ./prog
main-xyz
В последней строчке мы видим, что определение хуz() из главной программы переопределяет (перекрывает) одноименную функцию в разделяемой библиотеке.
Главная проблема такого механизма - несовместимость с принципом, согласно которому разделяемая библиотека должна быть реализована в качестве самодостаточной подсистемы. В результате такого подхода, разделяемая библиотека не гарантирует, что ссылка на один из ее собственных глобальных символов будет привязана к ее же определению этого символа.
Следовательно, свойства библиотеки могут измениться при включении ее в более крупный модуль. Это может привести к непредвиденным сбоям в приложении и усложнить раздельную отладку (например, когда вы пытаетесь воспроизвести проблему, используя другие разделяемые библиотеки или уменьшая их количество).
Для гарантии того, что в вышеописанном сценарии вызов хуz() в разделяемой библиотеке приведет к запуску именно той функции, которая в ней определена, на этапе сборки компоновщику можно передать параметр -Bsymbolic:
$ gcc -g -c -fPIC -Wall -c libdemo.c
$ gcc -g -shared -Wl,-Bsymbolic -o libdemo.so libdemo.c
$ gcc -g -o prog main.c libdemo.so
$ LD_LIBRARY_PATH=./ ./prog
libdemo-xyz
Параметр компоновщика -Bsymbolic делает так, что ссылки на глобальный символ внутри разделяемой библиотеки в первую очередь должны привязываться к определению из этой библиотеки (если таковое существует).
Стоит отметить: вне зависимости от данного параметра, вызов хуz() главной программы всегда приводит к запуску той версии функции, которая в ней определена.
#linux
$ gcc -fno-plt -o prog main.c
Использование данного флага:
1) уменьшает размер исполняемого файла: каждая заглушка добавляет, как минимум, 32 байта к объему (для x86/x86_64 архитектур)
2) уменьшает загруженность регистров и "instruction cache" для CPU
3) ускоряет вызовы библиотечных функций и, как следствие, процесс выполнения кода
По данным разработчика yugr, использование флага в clang дало +10% к приросту производительности для большого проекта (автор деталей не уточнял). Это действительно хороший и значимый результат, особенно, для высоконагруженных сервисов, где каждая микросекунда на счету.
Интересный факт: во многих дистрибутивах сборка пакетов выполнена с внедрением флага "-fno-plt". Некоторые утилиты, в качестве исключений, могут обходить стороной этот флаг при компиляции. Так, например, для xorg, glibc, valgrind, openjdk (внутри Arch Linux) он не используется.
Cуществует еще 2 способа отключить ленивое связывание: переменная окружения "LD_BIND_NOW" и флаг компоновщика "-z now". При таком подходе влияние оказывается на загрузчик, из-за чего может не получится выжать максимум из производительности, т.к. вызов функций все также будут происходить через plt заглушки:
$ LD_BIND_NOW=1 ./prog
$ gcc -o prog main.c -Wl,-z,now
Результат использования флага и переменной окружения аналогичен:
1) plt заглушки все еще являются частью кода
2) все адреса символов определяются на этапе запуска программы
3) не вызывается динамический компоновщик для связывания функций при первом использовании
4) при вызове функции, переход происходит сразу на конкретный адрес в таблице GOT
Таким образом, однозначно добиться дополнительной производительности от приложения можно через использование флага компилятора "-fno-plt", который исключит из бинарного файла все plt заглушки и приведет к вызову функций через прямое обращение к GOT.
$ readelf -d prog
Dynamic section at offset 0xd78 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdemo.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
При старте программы, загрузчик проходится по всем известным ему путям, проверяет кэш ld.so.cache и определяет, доступна ли указанная библиотека для дальнейшей работы. Если зависимость не найдена, приложение упадет и выведется следующий лог:
./prog: error in loading shared libraries: libdemo.so:
cannot open shared object file: No such file or directory
На данном этапе никакого связывания не происходит, сейчас загрузчику нужно просто определить библиотеку для текущего процесса: выгрузить ее в виртуальное адресное пространство программы и, при первом использовании, в физическую память (RAM).
Для связывания в ELF существует специальная секция .dynsym, в которой ключевым словом UND (undefined) помечены названия всех символов, которые необходимо определить:
$ readelf --dyn-syms ./prog
Symbol table '.dynsym' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND calculate_smth
Как и когда происходит разрешение имен?
Для того, чтобы ответить на данный вопрос, рассмотрим две дополнительные сущности ELF файла: PLT и GOT таблицы. Эти таблицы расположены в адресном пространстве процесса и отвечают за определение адресов динамически подгружаемых символов. На каждый символ в этих таблицах существует точка входа:
$ readelf -SW ./prog
Section Headers:
[Nr] Name Type Address
[12] .plt PROGBITS 00000000000006a0
[21] .got PROGBITS 0000000000010f78
При статическом связывании программы, все вызовы динамически подгружаемых символов записываются компоновщиком следующим образом "<name>@plt". Это говорит о том, что в рантайме будет выполнена не сама функция, а определенная заглушка, которая, по таблице GOT выяснит, найден ли соответствующий адрес. Вот так выглядит команда на уровне ассемблера:
call 0x401060 <puts@plt>Если адрес неизвестен, код внутри заглушки попросит загрузчик его определить и записать в соответствующее поле таблицы GOT, после чего будет выполнена команда jump на адрес и начнется выполнению кода. При последующих вызовах функции заглушка проверит адрес в таблице GOT и так как он будет определен, пропустит вызов компоновщика и сразу перейдет на jump до функции:
jump 0x404018 <puts@got.plt>Так вот, что же такое Lazy Binding? Это процесс определения адресов на этапе выполнения. Такой подход призван ускорить старт программы, так как не требуется при загрузке ее в память полностью заполнять все точки входа таблицы GOT: проходиться по всем библиотекам из секции .dynamic и определять адреса для каждого символа секции .dynsym. Это не значит, что библиотеки, от которых зависит исполняемый файл, не будут загружены в память. Отложенная загрузка библиотек - это чуть другая история (Lazy Loading). В Linux, к сожалению, данный функционал отсутствует и реализован только сторонними утилитами, допустим, imlib.so. Lazy binding обладает как плюсами, так и минусами. С одной стороны, ускоряется старт программы, с другой, приложение может неожиданно упасть, если на этапе выполнения не будет найден какой-то символ (как мы помним, это может произойти из-за того, что таблица GOT инициализируется не сразу, а по мере необходимости).
$ cat ld.so.conf
include /etc/ld.so.conf.d/*.conf
/usr/lib/mysql
Это значит, что содержимое всех файлов с сигнатурой <filename>.conf в каталоге "/etc/ld.so.conf.d/" будет рассмотрено и включено в итоговый кэш:
$ ls /etc/ld.so.conf.d/
libc.conf
$ cat /etc/ld.so.conf.d/libc.conf
/usr/local/lib
Соответственно, для того, чтобы компоновщик смог найти библиотеку, с которой ваше приложение связано, необходимо записать полный путь до директории либо в ld.so.conf, либо в отдельный файл, который следует включить в список поиска через ключевое слово include.
После внесения изменений в конфиг, необходимо обязательно обновить кэш, иначе пути, которые вы прописали, останутся без внимания компоновщика:
$ sudo ldconfig
Подводя черту, можно сказать, что динамические библиотеки, необходимые программе, ищутся в нескольких местах со следующим приоритетом:
1. (только для ELF) Если исполняемый файл содержит запись DT_RPATH с какими-либо каталогами, то поиск будет выполнен по данным каталогам.
2. Если определена переменная среды LD_LIBRARY_РАТH, то поиск будет выполнен последовательно по каждому каталогу, который в ней указан. Если исполняемый файл устанавливает пользовательский (SUID) или групповой (SGID) идентификатор, то переменная игнорируется. Это делается в целях безопасности, чтобы не дать пользователю обмануть динамический компоновщик, заставив его загрузить вместо требуемой библиотеки ее приватную версию с тем же именем.
3. (только для ELF) Если в записи DT_RUNPATH указаны какие-либо каталоги, то они используются во время поиска.
4. Проверяется файл /etc/ld.so.cache, чтобы узнать, содержится ли в нем запись для соответствующей зависимости. Если бинарный файл был связан с опцией компановщика "-z nodeflib", библиотеки в путях по умолчанию (lib и /usr/lib), будут пропущены.
5. Выполняется поиск по каталогам lib и /usr/lib (именно в таком порядке). Если бианрный файл был связан с опцией компановщика "-z nodeflib", данный шаг будет пропущен.
/* определяем путь "./lib/demo/", как значение для поля DT_RPATH/DT_RUNPATH в исполняемом файле prog */
$ gcc -Wl,-rpath=./lib/demo/ -o prog prog.c -L=./lib/demo/ -ldemo
$ gcc -Wl,rpath=./demo2/ -shared -o libdemo.so mod1.o -L=./demo2 -ldemo2
Результатом компоновки будет запись метки RUNPATH в ELF файле:
$ readelf -d prog
Dynamic section at offset 0xd78 contains 29 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdemo.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000001d (RUNPATH) Library runpath: [./lib/demo/]
$ ldd prog
libdemo.so => ./lib/demo/libdemo.so (0x0000ffffb7790000)
Данный способ бывает полезен:
1. На этапе локальных сборок приложений. Вместо того, чтобы всегда указывать переменную окружения LD_LIBRARY_PATH, можно прописать 2 сценария сборки (debug/release) - в последнем сборка происходит без -rpath и все библиотеки устанавливаются в директории, о которых динамический компоновщик в курсе.
2. При установке приложений, версии зависимостей которых отличаются от тех, которые доступны в системе. В таком случае имеет место размещение библиотек в директориях, о известных известно только целевому исполняемому файлу.
Разница между DT_RPATH и DT_RUNPATH:
В былые времена спецификация формата ELF допускала исключительно встраивание DT_RPATH, однако в последующих версиях данного формата эта метка считается устаревшей - в качестве замены был представлен DT_RUNPATH.
Разница между двумя указанными записями заключается в их приоритете относительно переменной среды LD_LIBRARY_PATH: DT_RPATH переопределяет LD и обрабатывается с наибольшим приоритетом, DT_RUNPATH имеет меньший вес и рассматривается сразу после LD.
По умолчанию современные версии компоновщика записывают значение -rpath в метку DT_RUNPATH. Чтобы задействовать вместо этого DT_RPATH, следует дополнительно указать параметр --disable-new-dtags:
$ gcc -g -Wall -Wl,--disable-new-dtags,-rpath=./lib/demo \
-o prog main.c -L=./lib/demo/ -ldemo
$ objdump -p prog | grep PATH
RPATH ./lib/demo/
Использование переменной $ORIGIN в списке rpath:
Представьте, что вам нужно распространять приложение, которое применяет собственные разделяемые библиотеки, при этом вы не хотите заставлять пользователя устанавливать их в один из стандартных каталогов. Вместо этого должна быть возможность распаковать приложение в любом месте и сразу же его запустить.
Проблема в том, что приложение не может определить местоположение своих разделяемых библиотек самостоятельно - мы должны попросить пользователя задать переменную LD_LIBRARY_PATH или предоставить небольшой установочный сценарий, который будет определять соответствующие каталоги. Ни один из вариантов не подходит.
Для решения данной проблемы динамический компоновщик позволяет указать в параметре -rpath специальную строку, $ORIGIN, которую он умеет анализировать и интерпретировать как «каталог, содержащий приложение». Это значит, что поиск динамических зависимостей будет происходить по пути, относительно тому, в котором приложение установлено:
$ gcc -Wl,-rpath,'$ORIGIN'/lib/demo ...
$ objdump -p prog | grep PATH
RPATH $ORIGIN/lib/demo/
Теперь можно предоставить пользователю простой установочный пакет с программой и необходимыми библиотеками, который он сможет установить в любое место и успешно запустить приложение ("Дело сделано" - принимайте работу под ключ).