Утилиты командной строки: top и htop
Команды необходимы для мониторинга производительности системы (Linux). Их использование полезно для анализа процессов и определения того, сколько CPU тратится на их выполнение, какое потребление оперативной памяти и т.д.
До знакомства с более продвинутыми средствами профилирования, сам использовал htop для обнаружения утечек памяти и узких мест программы, где потребление CPU зашкаливало🔥
Их основные отличия:
Доступность
• top: Является предустановленным для болишинства UNIX систем.
• htop: Требует ручной установки (доступен в большинстве стандартных репозиториев).
Интерфейс
• top: Текстовый интерфейс с ограниченными возможностями навигации.
• htop: Интерактивный интерфейс с возможностью использования мыши, прокрутки и сортировки.
Информация о системе
• top: Отображает основную информацию о системе - использование ЦП, памяти, своп и процессов.
• htop: Предоставляет более подробную информацию, включая использование диска, сетевой трафик, температуру ЦП, использование его ядер и многое другое.
Управление процессами
• top: Позволяет убивать процессы, изменять их приоритет и просматривать подробную информацию.
• htop: Предлагает более расширенные возможности управления процессами, такие как древовидный вид, фильтрация и поиск.
Настраиваемость
• top: Имеет ограниченные возможности настройки.
• htop: Высоко настраиваемый, позволяет изменять цвета, столбцы и горячие клавиши.
Другие функции
• top: Может отображать графики использования ресурсов.
• htop: Имеет встроенный просмотрщик файлов, монитор сети и другие полезные функции.
Ресурсоемкость
• top: Более легкий и потребляет меньше ресурсов.
• htop: Более ресурсоемкий, но предлагает расширенный набор функций.
Бонус, если вы все еще используете top: для того, чтобы преобразить его внешний вид и вывести дополнительную информацию, можно воспользоваться последовательным вводом (1, z, c, m)👨💻
Команды необходимы для мониторинга производительности системы (Linux). Их использование полезно для анализа процессов и определения того, сколько CPU тратится на их выполнение, какое потребление оперативной памяти и т.д.
До знакомства с более продвинутыми средствами профилирования, сам использовал htop для обнаружения утечек памяти и узких мест программы, где потребление CPU зашкаливало
Их основные отличия:
Доступность
• top: Является предустановленным для болишинства UNIX систем.
• htop: Требует ручной установки (доступен в большинстве стандартных репозиториев).
Интерфейс
• top: Текстовый интерфейс с ограниченными возможностями навигации.
• htop: Интерактивный интерфейс с возможностью использования мыши, прокрутки и сортировки.
Информация о системе
• top: Отображает основную информацию о системе - использование ЦП, памяти, своп и процессов.
• htop: Предоставляет более подробную информацию, включая использование диска, сетевой трафик, температуру ЦП, использование его ядер и многое другое.
Управление процессами
• top: Позволяет убивать процессы, изменять их приоритет и просматривать подробную информацию.
• htop: Предлагает более расширенные возможности управления процессами, такие как древовидный вид, фильтрация и поиск.
Настраиваемость
• top: Имеет ограниченные возможности настройки.
• htop: Высоко настраиваемый, позволяет изменять цвета, столбцы и горячие клавиши.
Другие функции
• top: Может отображать графики использования ресурсов.
• htop: Имеет встроенный просмотрщик файлов, монитор сети и другие полезные функции.
Ресурсоемкость
• top: Более легкий и потребляет меньше ресурсов.
• htop: Более ресурсоемкий, но предлагает расширенный набор функций.
Бонус, если вы все еще используете top: для того, чтобы преобразить его внешний вид и вывести дополнительную информацию, можно воспользоваться последовательным вводом (1, z, c, m)
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7🔥3❤🔥2
Ядро Linux было создано под впечатлением от операционной системы MINIX, которая не устраивала Линуса своей ограниченной лицензией.
Ядро 0.0.1 имело размер 62 Кб в сжатом виде и содержало около 10 тысяч строк исходного кода. Кодовая база последних версий насчитывает более 30 млн строк.
По данным исследования, проведённого в 2010 году по заказу Евросоюза, приблизительная стоимость разработки с нуля проекта, аналогичного современному ядру Linux, составила бы более миллиарда долларов США.
Динамика роста кодовой базы ядра Linux:
0.0.1 - сентябрь 1991, 10 тыс. строк;
1.0.0 - март 1994, 176 тыс. строк;
1.2.0 - март 1995, 311 тыс. строк;
2.0.0 - июнь 1996, 778 тыс. строк;
2.2.0 - январь 1999, 1.8 млн. строк;
2.4.0 - январь 2001, 3.4 млн. строк;
2.6.0 - декабрь 2003, 5.9 млн. строк;
2.6.28 - декабрь 2008, 10.2 млн. строк;
2.6.35 - август 2010, 13.4 млн. строк;
3.0 - август 2011, 14.6 млн. строк.
3.5 - июль 2012, 15.5 млн. строк.
3.10 - июль 2013, 15.8 млн. строк;
3.16 - август 2014, 17.5 млн. строк;
4.1 - июнь 2015, 19.5 млн. строк;
4.7 - июль 2016, 21.7 млн. строк;
4.12 - июль 2017, 24.1 млн. строк;
4.18 - август 2018, 25.3 млн. строк;
5.2 - июль 2019, 26.55 млн. строк;
5.8 - август 2020, 28.4 млн. строк;
5.13 - июнь 2021, 29.2 млн. строк;
5.19 - август 2022, 30.5 млн. строк;
Ядро 0.0.1 имело размер 62 Кб в сжатом виде и содержало около 10 тысяч строк исходного кода. Кодовая база последних версий насчитывает более 30 млн строк.
По данным исследования, проведённого в 2010 году по заказу Евросоюза, приблизительная стоимость разработки с нуля проекта, аналогичного современному ядру Linux, составила бы более миллиарда долларов США.
Динамика роста кодовой базы ядра Linux:
0.0.1 - сентябрь 1991, 10 тыс. строк;
1.0.0 - март 1994, 176 тыс. строк;
1.2.0 - март 1995, 311 тыс. строк;
2.0.0 - июнь 1996, 778 тыс. строк;
2.2.0 - январь 1999, 1.8 млн. строк;
2.4.0 - январь 2001, 3.4 млн. строк;
2.6.0 - декабрь 2003, 5.9 млн. строк;
2.6.28 - декабрь 2008, 10.2 млн. строк;
2.6.35 - август 2010, 13.4 млн. строк;
3.0 - август 2011, 14.6 млн. строк.
3.5 - июль 2012, 15.5 млн. строк.
3.10 - июль 2013, 15.8 млн. строк;
3.16 - август 2014, 17.5 млн. строк;
4.1 - июнь 2015, 19.5 млн. строк;
4.7 - июль 2016, 21.7 млн. строк;
4.12 - июль 2017, 24.1 млн. строк;
4.18 - август 2018, 25.3 млн. строк;
5.2 - июль 2019, 26.55 млн. строк;
5.8 - август 2020, 28.4 млн. строк;
5.13 - июнь 2021, 29.2 млн. строк;
5.19 - август 2022, 30.5 млн. строк;
👍19❤🔥5🔥5
Версионирование разделяемых библиотек:
Как известно, одним из преимуществ разделяемых библиотек является отсутствие необходимости в повторной компоновке исполняемых файлов, которые библиотеку используют: программа во время выполнения подтягивает необходимые ее части и не содержит копий объектных файлов, как в случае со статическими аналогами.
В Linux у вас может быть несколько версий одной и той же библиотеки. Следование правилам версионирования и компановки позволяет сохранить работоспособность программ при обновлениях, которые нарушают совместимость (добавление дополнительного параметра в публичный метод: подробнее можно ознакомиться здесь).
Корректно спроектированная динамическая библиотека должна состоять из 3 имен:
1. Реальное имя - название самой библиотеки “lib<name>.so.<major>.<minor>.<patch>”. Напрямую при линковке не используется (разве что для дебага).
Major - меняется в случае, когда правки приводят к несовместимости с приложениями (изменение API).
Minor - меняется при расширении API (добавлении функционала).
Patch - багфиксы, оптимизации (изменение функционала внутри готового API).
2. SONAME - метка, которая записывается в файл библиотеки на этапе создания (DT_SONAME в терминологии формата ELF). Именно она встраивается статическим компоновщиком в программу и, как следствие, используется динамическим компоновщиком для поиска на этапе выполнения. Структура, обычно, следующая “lib<library name>.so.<major>”.
3. Компоновочное имя - используется на этапе статического связывания приложения и библиотеки (не содержит номеров версий).
Для того, чтобы корректно подключить зависимость, нужно создать 2 символьные ссылки:
1) связывает компоновочное имя и soname;
2) связывает soname и реальное имя библиотеки;
Компоновочное имя может указывать на файл библиотеки, но желательно, чтобы оно ссылалось на soname, т.к. пользователь может удалить старую версию после минорного обновления и ссылка станет невалидной.
После корректного обновления библиотеки, ссылка soname должна указывать на самую последнюю ее версию, в результате чего, ваше приложение будет работать с актуальным экземпляром.
Если обновление меняет мажорную версию, то, при использовании утилиты ldconfig, автоматически будет создана ссылка, связывающая soname и имя библиотеки. При этом, приложение будет работать с последней совместимой версией (если не была удалена).
Как известно, одним из преимуществ разделяемых библиотек является отсутствие необходимости в повторной компоновке исполняемых файлов, которые библиотеку используют: программа во время выполнения подтягивает необходимые ее части и не содержит копий объектных файлов, как в случае со статическими аналогами.
В Linux у вас может быть несколько версий одной и той же библиотеки. Следование правилам версионирования и компановки позволяет сохранить работоспособность программ при обновлениях, которые нарушают совместимость (добавление дополнительного параметра в публичный метод: подробнее можно ознакомиться здесь).
Корректно спроектированная динамическая библиотека должна состоять из 3 имен:
1. Реальное имя - название самой библиотеки “lib<name>.so.<major>.<minor>.<patch>”. Напрямую при линковке не используется (разве что для дебага).
Major - меняется в случае, когда правки приводят к несовместимости с приложениями (изменение API).
Minor - меняется при расширении API (добавлении функционала).
Patch - багфиксы, оптимизации (изменение функционала внутри готового API).
2. SONAME - метка, которая записывается в файл библиотеки на этапе создания (DT_SONAME в терминологии формата ELF). Именно она встраивается статическим компоновщиком в программу и, как следствие, используется динамическим компоновщиком для поиска на этапе выполнения. Структура, обычно, следующая “lib<library name>.so.<major>”.
/* Создаем libdemo.so.1.0.0 с меткой libdemo.so.1 */
$ gcc -shared -Wl,-soname=libdemo.so.1 -o libdemo.so.1.0.0 module1.o
3. Компоновочное имя - используется на этапе статического связывания приложения и библиотеки (не содержит номеров версий).
/* В исполняемый файл prog будет записан soname, к которому приведет нас libdemo.so */
$ gcc -g -Wall -o prog main.o libdemo.so
Для того, чтобы корректно подключить зависимость, нужно создать 2 символьные ссылки:
1) связывает компоновочное имя и soname;
2) связывает soname и реальное имя библиотеки;
Компоновочное имя может указывать на файл библиотеки, но желательно, чтобы оно ссылалось на soname, т.к. пользователь может удалить старую версию после минорного обновления и ссылка станет невалидной.
libdemo.so -> libdemo.so.1
libdemo.so.1 -> libdemo.so.1.0.0
libdemo.so.1.0.0
После корректного обновления библиотеки, ссылка soname должна указывать на самую последнюю ее версию, в результате чего, ваше приложение будет работать с актуальным экземпляром.
libdemo.so.1 -> libdemo.so.1.0.1
Если обновление меняет мажорную версию, то, при использовании утилиты ldconfig, автоматически будет создана ссылка, связывающая soname и имя библиотеки. При этом, приложение будет работать с последней совместимой версией (если не была удалена).
$ sudo ldconfig -v | grep libdemo
libdemo.so.2 -> libdemo.so.2.0.0 (changed)
/* Выводим диинамические зависимости программы */
$ ldd prog
libdemo.so.1 => ./libdemo.so.1 (0x0000ffff8f870000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff7ff50000)
🔥9👍7❤🔥2
Разбор пакетов: build-essential
Метапакет в Linux, который включает в себя все основные зависимости и инструменты, необходимые для создания debian сборок из исходного кода. Пакет является Debian-специфичным, поэтому отсутствует в репозиториях для Fedora/RedHat/CentOS и т.д.
Сам по себе не является программой, он всего лишь содержит список зависимостей, которые менеджер apt считывает и устанавливает в систему. Так как готовый пакет может включать бинарь программы, build-essential ставит соответствующие инструменты для компиляции и компоновки.
Главное - не считать его супер решением, которое магическим образом установит все необходимое рабочее окружение для Ruby, Go, Python и т.д. В первую очередь, пакет содержит список базовых инструментов для сборки (.deb) образов.
Некоторыми компонентами являются:
1. gcc/g++: стандартные C/C++ компиляторы в среде разработки Linux.
2. libc6-dev: заголовочные и объектные файлы, необходимые для сборки приложений, использующих стандартную библиотеку C.
3. util-linux: различные утилиты командной строки (chroot, fdisk, mount).
4. dpkg-dev: инструменты, необходимые для создания и управления пакетами в формате (.deb).
5. make: универсальный инструмент для автоматизации сборки программ.
Метапакет в Linux, который включает в себя все основные зависимости и инструменты, необходимые для создания debian сборок из исходного кода. Пакет является Debian-специфичным, поэтому отсутствует в репозиториях для Fedora/RedHat/CentOS и т.д.
Сам по себе не является программой, он всего лишь содержит список зависимостей, которые менеджер apt считывает и устанавливает в систему. Так как готовый пакет может включать бинарь программы, build-essential ставит соответствующие инструменты для компиляции и компоновки.
Главное - не считать его супер решением, которое магическим образом установит все необходимое рабочее окружение для Ruby, Go, Python и т.д. В первую очередь, пакет содержит список базовых инструментов для сборки (.deb) образов.
Некоторыми компонентами являются:
1. gcc/g++: стандартные C/C++ компиляторы в среде разработки Linux.
2. libc6-dev: заголовочные и объектные файлы, необходимые для сборки приложений, использующих стандартную библиотеку C.
3. util-linux: различные утилиты командной строки (chroot, fdisk, mount).
4. dpkg-dev: инструменты, необходимые для создания и управления пакетами в формате (.deb).
5. make: универсальный инструмент для автоматизации сборки программ.
sudo apt install build-essential
🔥6👍5❤🔥2
Динамические библиотеки
Для устранения недочетов статических библиотек были придуманы динамические (разделяемые).
Их ключевая идея состоит в том, что объектные модули не копируются в исполняемый файл: вместо этого единый экземпляр библиотеки загружается в память при запуске первой, от нее зависящей, программы. Если позже будут запущены другие приложения, которым нужна эта библиотека, они обратятся к уже загруженной копии. В результате такой концепции исполняемые файлы требуют меньше места на диске и в виртуальной памяти.
Разделяемые библиотеки обладают следующими преимуществами:
1. Общий размер программ уменьшается, в связи с чем они могут быстрее загружаться в память и, как следствие, запускаться. Это относится только к большим разделяемым библиотекам, которые уже используются другими процессами. На самом деле, программа, которая первой начинает использовать библиотеку, запускается дольше остальных, т.к. с ее инициализацией происходит поиск библиотеки и выгрузка ее в память.
2. Объектные модули не копируются в исполняемые файлы, поэтому можно изменять исходный код библиотеки без необходимости выполнения повторной компоновки. Полностью осознать данное преимущество поможет вымышленный пример, где стандартная библиотека C (libc) является статической. Если кто-нибудь найдёт и исправит баг в printf, то каждая программа должна будет заново пройти компоновку для того, чтобы заполучить исправленный код.
Использование динамических библиотек действительно полезно в случае, когда они являются необходимыми для большинства программ и имеют внушительный размер (QT/GTK).
Однако за эти дополнительные возможности приходится платить:
1. Динамические библиотеки более сложные по сравнению со статическими - как с точки зрения самой концепции, так и в плане создания и компоновки внутри программ.
2. Излишняя зависимость: если в библиотеку было внесено изменение, нарушающее ее совместимость с приложением, запуск будет невозможен. Со статически собранным вариантом такой проблемы не наблюдается, т.к. в нем все необходимые объектные файлы хранятся в виде копий.
3. Перемещение символов должно выполняться во время работы программы: этот процесс требует дополнительных работ, в том числе, выгрузки ядром разделяемой библиотеки динамического линковщика ld-linux.so для связывания на этапе выполнения. Все это приводит к затратам по времени, поэтому программа может работать немного медленнее статически скомпонованного аналога.
Для сборки динамической версии библиотеки необходимо выполнить:
Для устранения недочетов статических библиотек были придуманы динамические (разделяемые).
Их ключевая идея состоит в том, что объектные модули не копируются в исполняемый файл: вместо этого единый экземпляр библиотеки загружается в память при запуске первой, от нее зависящей, программы. Если позже будут запущены другие приложения, которым нужна эта библиотека, они обратятся к уже загруженной копии. В результате такой концепции исполняемые файлы требуют меньше места на диске и в виртуальной памяти.
Разделяемые библиотеки обладают следующими преимуществами:
1. Общий размер программ уменьшается, в связи с чем они могут быстрее загружаться в память и, как следствие, запускаться. Это относится только к большим разделяемым библиотекам, которые уже используются другими процессами. На самом деле, программа, которая первой начинает использовать библиотеку, запускается дольше остальных, т.к. с ее инициализацией происходит поиск библиотеки и выгрузка ее в память.
2. Объектные модули не копируются в исполняемые файлы, поэтому можно изменять исходный код библиотеки без необходимости выполнения повторной компоновки. Полностью осознать данное преимущество поможет вымышленный пример, где стандартная библиотека C (libc) является статической. Если кто-нибудь найдёт и исправит баг в printf, то каждая программа должна будет заново пройти компоновку для того, чтобы заполучить исправленный код.
Использование динамических библиотек действительно полезно в случае, когда они являются необходимыми для большинства программ и имеют внушительный размер (QT/GTK).
Однако за эти дополнительные возможности приходится платить:
1. Динамические библиотеки более сложные по сравнению со статическими - как с точки зрения самой концепции, так и в плане создания и компоновки внутри программ.
2. Излишняя зависимость: если в библиотеку было внесено изменение, нарушающее ее совместимость с приложением, запуск будет невозможен. Со статически собранным вариантом такой проблемы не наблюдается, т.к. в нем все необходимые объектные файлы хранятся в виде копий.
3. Перемещение символов должно выполняться во время работы программы: этот процесс требует дополнительных работ, в том числе, выгрузки ядром разделяемой библиотеки динамического линковщика ld-linux.so для связывания на этапе выполнения. Все это приводит к затратам по времени, поэтому программа может работать немного медленнее статически скомпонованного аналога.
Для сборки динамической версии библиотеки необходимо выполнить:
/* -Wall - выводит предупреждения, вызванные потенциальными ошибками в коде, не препятствующими компиляции программы, но способными привести, по мнению компилятора, к тем или иным проблемам на этапе выполнения. */
// -fPIC - завставляет компилятор сгенерировать адресно-независимый код.
$ gcc -c -fPIC -Wall module1.c module2.c module3.c
/* -shared - указывает gcc, что в результате должен быть собран не исполняемый файл, а разделяемый объект - динамическая библиотека. */
$ gcc -shared -o libdemo.so module1.o module2.o module3.o
🔥7👍6🤯2👾1
Компоновка с динамическими библиотеками [1]
Перед применением библиотеки необходимо выполнить 2 дополнительных шага, которые не требуются для работы со статическими аналогами:
1. Поскольку исполняемый файл не содержит копии необходимых объектных модулей, он должен иметь возможность определять, какая разделяемая библиотека требуется для работы. Для этого, на этапе статической компоновки, в ELF файл программы внедряется метка DT_NEEDED с именем библиотеки.
Если говорить чуть более точно, то в поле DT_NEEDED записывается полный путь до библиотеки. Если зависимость находится в неизвестном компоновщику каталоге, то лучше указать дорогу через флаг -L, иначе потом может возникнуть вопрос "Почему зависимость находится корректно при ее локальном размещении?" - потому что ее полый путь записан в поле NEEDED.
2. Должен существовать механизм, который, во время выполнения программы, находит библиотеку по имени и выгружает ее в память, если она не была загружена ранее. Данная процедура выполняется динамическим компоновщиком и необходима для разрешения имени библиотеки на этапе выполнения.
Компоновщик, сам по себе, является разделяемой библиотекой “/lib/ld-linux.so.2” и используется всеми исполняемыми файлами формата ELF, которые содержат динамические зависимости.
На самом деле, Id-linux.so.2 представляет собой обычную символическую ссылку на библиотеку динамического компоновщика “Id-<version>.so” (например, ld-2.11. so), где version - это версия glibc, которая установлена в системе.
Если на данном этапе попробовать запустить программу, выведется следующее сообщение об ошибке:
Это возвращает нас ко второму пункту. Дело в том, что динамический компоновщик анализирует список рантайм зависимостей программы и находит соответствующие библиотечные файлы, используя набор заранее заданных правил.
Часть этих правил основывается на списке стандартных каталогов, в которых обычно хранятся разделяемые библиотеки (lib и /usr/lib). Причина ошибки выше заключается в том, что библиотека находится в текущем каталоге, который не учитывается при поиске.
Для оповещения динамического компоновщика о том, что разделяемая библиотека находится в нестандартном месте, можно воспользоваться переменной среды LD_LIBRARY_PATH, указав соответствующий каталог в качестве значения.
Если переменная определена, компоновщик начинает поиск разделяемой библиотеки с тех каталогов, которые в ней перечислены, и только потом переходит к стандартным библиотечным путям.
Следовательно, можно запустить программу с помощью следующей команды:
Перед применением библиотеки необходимо выполнить 2 дополнительных шага, которые не требуются для работы со статическими аналогами:
1. Поскольку исполняемый файл не содержит копии необходимых объектных модулей, он должен иметь возможность определять, какая разделяемая библиотека требуется для работы. Для этого, на этапе статической компоновки, в ELF файл программы внедряется метка DT_NEEDED с именем библиотеки.
Если говорить чуть более точно, то в поле DT_NEEDED записывается полный путь до библиотеки. Если зависимость находится в неизвестном компоновщику каталоге, то лучше указать дорогу через флаг -L, иначе потом может возникнуть вопрос "Почему зависимость находится корректно при ее локальном размещении?" - потому что ее полый путь записан в поле NEEDED.
$ gcc -Wall -o prog main.o -L=./libs/ -ldemo
// Выводим содержимое динамической секции ELF файла
$ readelf -d ./prog
Dynamic section at offset 0xd88 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdemo.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
2. Должен существовать механизм, который, во время выполнения программы, находит библиотеку по имени и выгружает ее в память, если она не была загружена ранее. Данная процедура выполняется динамическим компоновщиком и необходима для разрешения имени библиотеки на этапе выполнения.
Компоновщик, сам по себе, является разделяемой библиотекой “/lib/ld-linux.so.2” и используется всеми исполняемыми файлами формата ELF, которые содержат динамические зависимости.
На самом деле, Id-linux.so.2 представляет собой обычную символическую ссылку на библиотеку динамического компоновщика “Id-<version>.so” (например, ld-2.11. so), где version - это версия glibc, которая установлена в системе.
Если на данном этапе попробовать запустить программу, выведется следующее сообщение об ошибке:
$ ./prog
./prog: error in loading shared libraries: libdemo.so:
cannot open shared object file: No such file or directory
Это возвращает нас ко второму пункту. Дело в том, что динамический компоновщик анализирует список рантайм зависимостей программы и находит соответствующие библиотечные файлы, используя набор заранее заданных правил.
Часть этих правил основывается на списке стандартных каталогов, в которых обычно хранятся разделяемые библиотеки (lib и /usr/lib). Причина ошибки выше заключается в том, что библиотека находится в текущем каталоге, который не учитывается при поиске.
Для оповещения динамического компоновщика о том, что разделяемая библиотека находится в нестандартном месте, можно воспользоваться переменной среды LD_LIBRARY_PATH, указав соответствующий каталог в качестве значения.
Если переменная определена, компоновщик начинает поиск разделяемой библиотеки с тех каталогов, которые в ней перечислены, и только потом переходит к стандартным библиотечным путям.
Следовательно, можно запустить программу с помощью следующей команды:
$ LD_LIBRARY_PATH=./libs ./prog
🔥8👍7❤🔥3🥰1
Компоновка с динамическими библиотеками [2]
Мы уже познакомились с двумя способами оповещения динамического компоновщика о местоположении разделяемых библиотек:
1. Определение переменной среды LD_LIBRARY_РАТН.
2. Установка библиотеки в один из стандартных каталогов (lib, /usr/lib).
Но есть и третий путь: на этапе статической сборки в исполняемый файл (приложение либо разделяемую библиотеку) можно встроить список каталогов, по которым требуется производить поиск. Для этого можно воспользоваться параметром компоновщика -rpath:
Результатом компоновки будет запись метки RUNPATH в ELF файле:
Данный способ бывает полезен:
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:
Использование переменной $ORIGIN в списке rpath:
Представьте, что вам нужно распространять приложение, которое применяет собственные разделяемые библиотеки, при этом вы не хотите заставлять пользователя устанавливать их в один из стандартных каталогов. Вместо этого должна быть возможность распаковать приложение в любом месте и сразу же его запустить.
Проблема в том, что приложение не может определить местоположение своих разделяемых библиотек самостоятельно - мы должны попросить пользователя задать переменную LD_LIBRARY_PATH или предоставить небольшой установочный сценарий, который будет определять соответствующие каталоги. Ни один из вариантов не подходит.
Для решения данной проблемы динамический компоновщик позволяет указать в параметре -rpath специальную строку, $ORIGIN, которую он умеет анализировать и интерпретировать как «каталог, содержащий приложение». Это значит, что поиск динамических зависимостей будет происходить по пути, относительно тому, в котором приложение установлено:
Теперь можно предоставить пользователю простой установочный пакет с программой и необходимыми библиотеками, который он сможет установить в любое место и успешно запуститься. Как говориться, "Дело сделано" - принимайте работу под ключ.
Мы уже познакомились с двумя способами оповещения динамического компоновщика о местоположении разделяемых библиотек:
1. Определение переменной среды LD_LIBRARY_РАТН.
2. Установка библиотеки в один из стандартных каталогов (lib, /usr/lib).
Но есть и третий путь: на этапе статической сборки в исполняемый файл (приложение либо разделяемую библиотеку) можно встроить список каталогов, по которым требуется производить поиск. Для этого можно воспользоваться параметром компоновщика -rpath:
/* определяем путь "./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/
Теперь можно предоставить пользователю простой установочный пакет с программой и необходимыми библиотеками, который он сможет установить в любое место и успешно запуститься. Как говориться, "Дело сделано" - принимайте работу под ключ.
🔥11👍7❤🔥3❤2🕊2
Компоновка с динамическими библиотеками [3]
Последним из основных способов оповестить динамический компоновщик о местонахождении библиотек является набор соответствующих записей в файле "/etc/ld.so.conf". Как мы знаем, из коробки компоновщику известно несколько путей, по которым могут лежать библиотеки (lib, /usr/lib).
Для того, чтобы расширить этот список, можно внести набор соответствующих записей в конфиг (ld.so.conf), который используется программой ldconfig для того, чтобы создать необходимый компоновщику бинарный кэш "/etc/ld.so.cache" с информацией о всех зависимостях, найденных по заданным путям. Анализируя данный кэш, загрузчику станет понятно, какая библиотека требуется приложению на этапе выполнения.
Почему компоновщик не читает ld.so.conf напрямую? Чтение кэша - более быстрая процедура, относительно анализа файла конфигурации: нужно проверить инклуды, пройтись по файловой системе, открыть каждый файл, считать его содержимое, найти зависимости. С таким подходом потребовалось бы значительное количество дополнительных операций на этапе выполнения для того, чтобы связаться с библиотекой - лишние тормоза и задержки нам ни к чему.
Если посмотреть внутрь ld.so.conf, можно увидеть следующее:
Это значит, что содержимое всех файлов с сигнатурой <filename>.conf в каталоге "/etc/ld.so.conf.d/" будет рассмотрено при формировании итогового кэша:
Соответственно, для того, чтобы компоновщик смог найти библиотеку, с которой ваше приложение связано, необходимо записать полный путь до директории либо в ld.so.conf, либо в отдельный файл, который следует включить в список поиска через ключевое слово include.
После внесения изменений в конфиг, необходимо обязательно обновить кэш, иначе пути, которые вы прописали, останутся без внимания компоновщика:
Подводя черту, можно сказать, что динамические библиотеки, необходимые программе, ищутся в нескольких местах со следующим приоритетом:
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", данный шаг будет пропущен.
Последним из основных способов оповестить динамический компоновщик о местонахождении библиотек является набор соответствующих записей в файле "/etc/ld.so.conf". Как мы знаем, из коробки компоновщику известно несколько путей, по которым могут лежать библиотеки (lib, /usr/lib).
Для того, чтобы расширить этот список, можно внести набор соответствующих записей в конфиг (ld.so.conf), который используется программой ldconfig для того, чтобы создать необходимый компоновщику бинарный кэш "/etc/ld.so.cache" с информацией о всех зависимостях, найденных по заданным путям. Анализируя данный кэш, загрузчику станет понятно, какая библиотека требуется приложению на этапе выполнения.
Почему компоновщик не читает ld.so.conf напрямую? Чтение кэша - более быстрая процедура, относительно анализа файла конфигурации: нужно проверить инклуды, пройтись по файловой системе, открыть каждый файл, считать его содержимое, найти зависимости. С таким подходом потребовалось бы значительное количество дополнительных операций на этапе выполнения для того, чтобы связаться с библиотекой - лишние тормоза и задержки нам ни к чему.
Если посмотреть внутрь ld.so.conf, можно увидеть следующее:
$ 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", данный шаг будет пропущен.
👍9🔥7❤🔥3
Подошла к концу конференция C++ Russia 2024. Что могу сказать: супер организация, супер доклады, спикеры просто гении не из мира сего (люди разрабатывают статичесие анализаторы, профилировщики и, что самое страшное, игры на Metal API 😮 ).
Зрителей тоже хочется отдельно похвалить: благодаря их каверзным вопросам, которые иногда ставили рассказчиков в тупик, получалось более детально разбирать темы.
Много было интересных технических докладов: начиная с обзора интрузивных контейнеров библиотеки Boost и заканчивая программной оптимизацией исключений на уровне компилятора. В общем, на таких мероприятиях можно услышать то, на что в интернете так просто не наткнешься.
Не обошли стороной и тему разделяемых библиотек: подробно рассказали про различные способы оптимизации их использования. Сейчас структурирую информацию, будет интересный пост :)
Лично для меня было довольно значимым событием наконец-то увидеть вживую Илью Мещерина. Если кто не в курсе Илья - это один из самых толковых "медийных" преподавателей по C++. На самом деле, кроме него, из русских в голову приходит только Виндертон, но тут речь больше про Computer Science. Чем удивил Илья - своей простотой, энергичностью, умением грамотно и структурированно преподносить материал. Хоть я и не сильно горю алгоритмикой, просмотрел все выступление на одном дыхании.
Наш путь в продвижении качественного обучения только начинается, работаем вместе, меняем ход IT!
Зрителей тоже хочется отдельно похвалить: благодаря их каверзным вопросам, которые иногда ставили рассказчиков в тупик, получалось более детально разбирать темы.
Много было интересных технических докладов: начиная с обзора интрузивных контейнеров библиотеки Boost и заканчивая программной оптимизацией исключений на уровне компилятора. В общем, на таких мероприятиях можно услышать то, на что в интернете так просто не наткнешься.
Не обошли стороной и тему разделяемых библиотек: подробно рассказали про различные способы оптимизации их использования. Сейчас структурирую информацию, будет интересный пост :)
Лично для меня было довольно значимым событием наконец-то увидеть вживую Илью Мещерина. Если кто не в курсе Илья - это один из самых толковых "медийных" преподавателей по C++. На самом деле, кроме него, из русских в голову приходит только Виндертон, но тут речь больше про Computer Science. Чем удивил Илья - своей простотой, энергичностью, умением грамотно и структурированно преподносить материал. Хоть я и не сильно горю алгоритмикой, просмотрел все выступление на одном дыхании.
Наш путь в продвижении качественного обучения только начинается, работаем вместе, меняем ход IT!
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥15👍10😱3
Контент план по разделяемым библиотекам:
1. Общая информация про разделяемые библиотеки
2. Компоновка с динамическими библиотеками [1]
3. Компоновка с динамическими библиотеками [2]
4. Компоновка с динамическими библиотеками [3]
5. Версионирование разделяемых библиотек
6. Отложенное связывание символов (Lazy Binding)
7. Ускорение работы библиотек: отключение ленивого связывания
8. Использование флага компоновщика -Bsymbolic
9. Ускорение работы библиотек: отключение перехвата функций
10. Ускорение загрузки библиотек: отключение неиспользуемых библиотек
11. Команды ldconfig, ldd, objdump, readelf, nm
12. Отложенная загрузка библиотек (dlopen, dlclose, dlerror, dlsym, dladdr)
13. Управление видимостью символов (attribute: visibility)
14. Ускорение работы библиотек: сокращение интерфейса библиотеки
15. Версионирование символов
16. Инициализация и финализация функций (attribute: constructor, destructor)
17. Мониторинг работы загрузчика (LD_DEBUG)
1. Общая информация про разделяемые библиотеки
2. Компоновка с динамическими библиотеками [1]
3. Компоновка с динамическими библиотеками [2]
4. Компоновка с динамическими библиотеками [3]
5. Версионирование разделяемых библиотек
6. Отложенное связывание символов (Lazy Binding)
7. Ускорение работы библиотек: отключение ленивого связывания
8. Использование флага компоновщика -Bsymbolic
9. Ускорение работы библиотек: отключение перехвата функций
10. Ускорение загрузки библиотек: отключение неиспользуемых библиотек
11. Команды ldconfig, ldd, objdump, readelf, nm
12. Отложенная загрузка библиотек (dlopen, dlclose, dlerror, dlsym, dladdr)
13. Управление видимостью символов (attribute: visibility)
14. Ускорение работы библиотек: сокращение интерфейса библиотеки
15. Версионирование символов
16. Инициализация и финализация функций (attribute: constructor, destructor)
17. Мониторинг работы загрузчика (LD_DEBUG)
🔥12👍8🤩3
Отложенное связывание символов (Lazy Binding)
С этого поста мы начинаем глубже уходить в процесс разрешения динамических зависимостей. Разобравшись с тем, как исполняемый файл получает доступ к внешним символам, мы сможем точнее понять проблему разделяемых библиотек и аккуратно подойти к возможным оптимизациям, которые к ним применимы.
Как мы знаем, в динамической секции ELF файла содержатся названия всех разделяемых библиотек, от которых наше приложение зависит:
При старте программы, загрузчик проходится по всем известным ему путям, проверяет кэш ld.so.cache и определяет, доступна ли указанная библиотека для дальнейшей работы. Если зависимость не найдена, приложение упадет и выведется следующий лог:
На данном этапе никакого связывания не происходит, сейчас загрузчику нужно просто определить библиотеку для текущего процесса: выгрузить ее в виртуальное адресное пространство программы и, при первом использовании, в физическую память (RAM).
Для связывания в ELF существует специальная секция .dynsym, в которой ключевым словом UND (undefined) помечены названия всех символов, которые необходимо определить:
Как и когда происходит разрешение имен?
Для того, чтобы ответить на данный вопрос, рассмотрим две дополнительные сущности ELF файла: PLT и GOT таблицы. Эти таблицы расположены в адресном пространстве процесса и отвечают за определение адресов динамически подгружаемых символов. На каждый символ в этих таблицах существует точка входа:
При статическом связывании программы, все вызовы динамически подгружаемых символов записываются компоновщиком следующим образом "<name>@plt". Это говорит о том, что в рантайме будет выполнена не сама функция, а определенная заглушка, которая, по таблице GOT выяснит, найден ли соответствующий адрес. Вот так выглядит команда на уровне ассемблера:
Если адрес неизвестен, код внутри заглушки попросит загрузчик его определить и записать в соответствующее поле таблицы GOT, после чего будет выполнена команда jump на адрес и начнется выполнению кода. При последующих вызовах функции заглушка проверит адрес в таблице GOT и так как он будет определен, пропустит вызов компоновщика и сразу перейдет на jump до функции:
Так вот, что же такое Lazy Binding?
Это процесс определения адресов на этапе выполнения. Такой подход призван ускорить старт программы, так как не требуется при загрузке ее в память полностью заполнять все точки входа таблицы GOT: проходиться по всем библиотекам из секции .dynamic и определять адреса для каждого символа секции .dynsym.
Это не значит, что библиотеки, от которых зависит исполняемый файл, не будут загружены в память. Отложенная загрузка библиотек - это чуть другая история (Lazy Loading). В Linux, к сожалению, данный функционал отсутствует и реализован только сторонними утилитами, допустим, imlib.so.
Lazy binding обладает как плюсами, так и минусами. С одной стороны, ускоряется старт программы, с другой, приложение может неожиданно упасть, если на этапе выполнения не будет найден какой-то символ (как мы помним, это может произойти из-за того, что таблица GOT инициализируется не сразу, а по мере необходимости).
С этого поста мы начинаем глубже уходить в процесс разрешения динамических зависимостей. Разобравшись с тем, как исполняемый файл получает доступ к внешним символам, мы сможем точнее понять проблему разделяемых библиотек и аккуратно подойти к возможным оптимизациям, которые к ним применимы.
Как мы знаем, в динамической секции 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]
При старте программы, загрузчик проходится по всем известным ему путям, проверяет кэш 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 инициализируется не сразу, а по мере необходимости).
👍13🔥10❤3
LinuxCamp | DevOps pinned «Контент план по разделяемым библиотекам: 1. Общая информация про разделяемые библиотеки 2. Компоновка с динамическими библиотеками [1] 3. Компоновка с динамическими библиотеками [2] 4. Компоновка с динамическими библиотеками [3] 5. Версионирование разделяемых…»
Ускорение работы библиотек: отключение ленивого связывания
Как мы выяснили в прошлой публикации, ленивое связывание (Lazy Binding) - это процесс определения адресов символов на этапе выполнения. Данный механизм ускоряет загрузку приложения ценой накладных расходов в процессе работы: вместо прямого вызова функции вызывается PLT заглушка, которая задействует набор дополнительных машинных инструкций и дергает динамический компоновщик для определения адресов.
Система ленивого связывания не везде используется по дефолту: в Windows таблица адресов (Import Address Table - IAT) полностью заполняется на старте программы, в Linux ситуация обратная, однако никто не говорить, что скорректировать такое поведение невозможно - все возможно (особенно в Linux)!
Современные компиляторы поддерживают флаг "-fno-plt", который, как видно из названия, исключает из кода plt заглушки и необходим для связывания символов на старте программы:
Использование данного флага:
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 заглушки:
Результат использования флага и переменной окружения аналогичен:
1) plt заглушки все еще являются частью кода
2) все адреса символов определяются на этапе запуска программы
3) не вызывается динамический компоновщик для связывания функций при первом использовании
4) при вызове функции, переход происходит сразу на конкретный адрес в таблице GOT
Таким образом, однозначно добиться дополнительной производительности от приложения можно через использование флага компилятора "-fno-plt", который исключит из бинарного файла все plt заглушки и приведет к вызову функций через прямое обращение к GOT.
#linux
Как мы выяснили в прошлой публикации, ленивое связывание (Lazy Binding) - это процесс определения адресов символов на этапе выполнения. Данный механизм ускоряет загрузку приложения ценой накладных расходов в процессе работы: вместо прямого вызова функции вызывается PLT заглушка, которая задействует набор дополнительных машинных инструкций и дергает динамический компоновщик для определения адресов.
Система ленивого связывания не везде используется по дефолту: в Windows таблица адресов (Import Address Table - IAT) полностью заполняется на старте программы, в Linux ситуация обратная, однако никто не говорить, что скорректировать такое поведение невозможно - все возможно (особенно в Linux)!
Современные компиляторы поддерживают флаг "-fno-plt", который, как видно из названия, исключает из кода plt заглушки и необходим для связывания символов на старте программы:
$ 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.
#linux
👍16🔥6👏2
Использование флага компоновщика -Bsymbolic
Представьте, что глобальный символ (функция или переменная) определен сразу в нескольких местах - например, в исполняемом файле и разделяемой библиотеке или в нескольких разных библиотеках. Как будет разрешена ссылка на этот символ?
Допустим, у нас есть главная программа и разделяемая библиотека, и в обеих определена глобальная функция хуz(), которая вызывается из другой библиотечной функции:
Собрав разделяемую библиотеку и исполняемый файл и затем запустив полученную программу, мы увидим следующее:
В последней строчке мы видим, что определение хуz() из главной программы переопределяет (перекрывает) одноименную функцию в разделяемой библиотеке.
Главная проблема такого механизма - несовместимость с принципом, согласно которому разделяемая библиотека должна быть реализована в качестве самодостаточной подсистемы. В результате такого подхода, разделяемая библиотека не гарантирует, что ссылка на один из ее собственных глобальных символов будет привязана к ее же определению этого символа.
Следовательно, свойства библиотеки могут измениться при включении ее в более крупный модуль. Это может привести к непредвиденным сбоям в приложении и усложнить раздельную отладку (например, когда вы пытаетесь воспроизвести проблему, используя другие разделяемые библиотеки или уменьшая их количество).
Для гарантии того, что в вышеописанном сценарии вызов хуz() в разделяемой библиотеке приведет к запуску именно той функции, которая в ней определена, на этапе сборки компоновщику можно передать параметр -Bsymbolic:
Параметр компоновщика -Bsymbolic делает так, что ссылки на глобальный символ внутри разделяемой библиотеки в первую очередь должны привязываться к определению из этой библиотеки (если таковое существует).
Стоит отметить: вне зависимости от данного параметра, вызов хуz() главной программы всегда приводит к запуску той версии функции, которая в ней определена.
Представьте, что глобальный символ (функция или переменная) определен сразу в нескольких местах - например, в исполняемом файле и разделяемой библиотеке или в нескольких разных библиотеках. Как будет разрешена ссылка на этот символ?
Допустим, у нас есть главная программа и разделяемая библиотека, и в обеих определена глобальная функция хуz(), которая вызывается из другой библиотечной функции:
// Выводим содержимое 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() главной программы всегда приводит к запуску той версии функции, которая в ней определена.
👍9🔥7❤🔥2
Ускорение работы библиотек: отключение перехвата функций
В прошлый раз мы рассмотрели флаг компоновщика -Bsymbolic и узнали, как будет разрешена ссылка на глобальный символ, определенный сразу в нескольких местах: исполняемом файле и разделяемой библиотеке.
В этой публикации продолжим разбор данного флага и узнаем, как его использование в связке с параметром компилятора -fno-semantic-interposition может уменьшить размер исполняемого файла и ускорить процесс выполнения кода.
В обычной ситуации, если вы компилируете разделяемую библиотеку с помощью GCC, каждый вызов функции проходит через таблицу PLT для определения адреса и поддержки возможности, в случае чего, использовать стороннюю реализацию при перехвате (LD_PRELOAD). Это говорит о том, что если в библиотеке определены функции foo и bar и при этом foo вызывает bar, то вызов bar будет не прямым, а косвенным (через PLT заглушку):
Такая система вызовов:
1. Исключает возможные оптимизации на этапе генерации кода компилятором. Как пример, то же встраивание функций (inline) не будет отработано.
2. Приводит к большему размеру файла библиотеки за счет дополнительных машинных инструкций, необходимых для заполнения таблиц PLT и GOT.
3. Нагружает CPU instruction cache и приводит к более длительному исполнению кода.
4. Приводит к неоднозначности по отношению к используемой реализации: будет вызвана функция, определенная компоновщиком первой. Не факт, что это будет именно тот экземпляр, который реализован внутри библиотеки. Функции, которые определены в исполняемом файле, имеют больший приоритет.
Хммм, для чего тогда нужна такая система, раз уж столько в ней недостатков? По другому не получится, если нужен функционал перехвата функций через переменную LD_PRELOAD. В таком случае можно подменять реализации символов на те, которые указаны в передаваемой библиотеке:
Если мы соберем зависимость с флагом -Bsymbolic, все вызовы внутренних функций будут происходить напрямую, без PLT заглушек, что с одной стороны значительно уменьшит программный overhead, с другой исключит возможность перехвата символов в рантайме:
Таким образом, данный флаг не только приводит к однозначному использованию библиотечных реализаций вложенных функций, но и способствует лучшей производительности программ на этапе выполнения.
Как добиться лучшей производительности?
Все бы хорошо, но хочется полностью развязать компилятору руки и позволить эффективно оптимизировать код. Для данной цели предусмотрен флаг -fno-semantic-interposition, который исключает возможность перехвата символов на этапе компиляции и позволяет применять к функциям ряд различных оптимизаций. В GCC этот флаг по дефолту отключен и активируется под параметром -Ofast.
Для лучшей производительности, рассмотренные флаги лучше использовать вместе, тогда получится не только избавиться от PLT вызовов, но и сгенерировать более оптимальный код:
В прошлый раз мы рассмотрели флаг компоновщика -Bsymbolic и узнали, как будет разрешена ссылка на глобальный символ, определенный сразу в нескольких местах: исполняемом файле и разделяемой библиотеке.
В этой публикации продолжим разбор данного флага и узнаем, как его использование в связке с параметром компилятора -fno-semantic-interposition может уменьшить размер исполняемого файла и ускорить процесс выполнения кода.
В обычной ситуации, если вы компилируете разделяемую библиотеку с помощью GCC, каждый вызов функции проходит через таблицу PLT для определения адреса и поддержки возможности, в случае чего, использовать стороннюю реализацию при перехвате (LD_PRELOAD). Это говорит о том, что если в библиотеке определены функции foo и bar и при этом foo вызывает bar, то вызов bar будет не прямым, а косвенным (через PLT заглушку):
$ 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
👍8❤🔥4🔥4👾1
Друзья, далеко мы уже зашли с библиотеками, как самочувствие?)
Хотите разбавить материал? Предлагаю на выбор следующие варианты🥸
Хотите разбавить материал? Предлагаю на выбор следующие варианты
Anonymous Poll
13%
Продолжаем бомбить либы и собираем статью на Хабр
23%
Делаем немного постов по C и C++ (структурное выравнивание, extern C и т.д)
60%
Рассматриваем базис Linux (работа с процессами, команды оболочки и т.д)
3%
Есть другие идеи, напишу в комменты 💬
Процессы и программы
Процессом является экземпляр выполняемой программы. В данном посте мы подробно разберем это определение и вы узнаете разницу между программой и процессом.
Программа представляет собой файл, содержащий различную информацию о том, как сконструировать процесс в ходе выполнения. В эту информацию входит:
1. Идентификационный признак двоичного формата.
Каждый программный файл включает в себя метаинформацию с описанием формата исполняемого файла. Это позволяет ядру корректно интерпретировать все остальные данные внутри файла.
Изначально для исполняемых файлов UNIX было предусмотрено два широко используемых формата: исходный формат a.out (assembler output — вывод на языке ассемблера) и появившийся позже более сложный общий формат объектных файлов COFF (Common Object File Format).
В настоящее время в большинстве реализаций UNIX (включая Linux) применяется формат исполняемых и компонуемых файлов ELF (Executable and Linking Format), предоставляющий множество преимуществ по сравнению с предшественниками.
2. Машинный код.
В нем закодирован алгоритм программы.
3. Адрес входа в программу.
В нем указывается место той инструкции, с которой должно начаться выполнение программы.
4. Данные.
В программном файле содержатся значения, используемые для инициализации переменных, а также применяемые программой символьные константы (например, строки).
5. Таблицы имен и переадресации.
В них дается описание расположений имен функций и переменных внутри программы. Эти таблицы предназначены для различных целей, включая отладку и разрешение имен в ходе выполнения программы (динамическое связывание).
6. Информация о совместно используемых библиотеках и динамической компоновке.
В программный файл включаются поля, где перечисляются совместно используемые библиотеки, которые программе потребуются в ходе выполнения, а также путевое имя динамического компоновщика, который должен применяться для загрузки этих библиотек.
Одна программа может использоваться для построения множества процессов, или же, если наоборот, во множестве процессов может быть запущена одна и та же программа.
Определение процесса, которое было дано в начале, можно переформулировать следующим образом: процесс является абстрактной сущностью, которая установлена ядром и которой, для выполнения программы, выделяются системные ресурсы.
С позиции ядра процесс состоит из памяти пользовательского пространства, внутри которой содержится код программы, а также из ряда структур данных, хранящих информацию о состоянии процесса.
Некоторая информация, записанная в структурах данных ядра, включает в себя: различные идентификаторы, связанные с процессом (PID, PPID ...), таблицы виртуальной памяти и дескрипторов открытых файлов, сведения, относящиеся к текущему рабочему каталогу, доставке и обработке сигналов, использованию и ограничениям ресурсов процесса.
#linuxcore #linux
Процессом является экземпляр выполняемой программы. В данном посте мы подробно разберем это определение и вы узнаете разницу между программой и процессом.
Программа представляет собой файл, содержащий различную информацию о том, как сконструировать процесс в ходе выполнения. В эту информацию входит:
1. Идентификационный признак двоичного формата.
Каждый программный файл включает в себя метаинформацию с описанием формата исполняемого файла. Это позволяет ядру корректно интерпретировать все остальные данные внутри файла.
Изначально для исполняемых файлов UNIX было предусмотрено два широко используемых формата: исходный формат a.out (assembler output — вывод на языке ассемблера) и появившийся позже более сложный общий формат объектных файлов COFF (Common Object File Format).
В настоящее время в большинстве реализаций UNIX (включая Linux) применяется формат исполняемых и компонуемых файлов ELF (Executable and Linking Format), предоставляющий множество преимуществ по сравнению с предшественниками.
2. Машинный код.
В нем закодирован алгоритм программы.
3. Адрес входа в программу.
В нем указывается место той инструкции, с которой должно начаться выполнение программы.
4. Данные.
В программном файле содержатся значения, используемые для инициализации переменных, а также применяемые программой символьные константы (например, строки).
5. Таблицы имен и переадресации.
В них дается описание расположений имен функций и переменных внутри программы. Эти таблицы предназначены для различных целей, включая отладку и разрешение имен в ходе выполнения программы (динамическое связывание).
6. Информация о совместно используемых библиотеках и динамической компоновке.
В программный файл включаются поля, где перечисляются совместно используемые библиотеки, которые программе потребуются в ходе выполнения, а также путевое имя динамического компоновщика, который должен применяться для загрузки этих библиотек.
Одна программа может использоваться для построения множества процессов, или же, если наоборот, во множестве процессов может быть запущена одна и та же программа.
Определение процесса, которое было дано в начале, можно переформулировать следующим образом: процесс является абстрактной сущностью, которая установлена ядром и которой, для выполнения программы, выделяются системные ресурсы.
С позиции ядра процесс состоит из памяти пользовательского пространства, внутри которой содержится код программы, а также из ряда структур данных, хранящих информацию о состоянии процесса.
Некоторая информация, записанная в структурах данных ядра, включает в себя: различные идентификаторы, связанные с процессом (PID, PPID ...), таблицы виртуальной памяти и дескрипторов открытых файлов, сведения, относящиеся к текущему рабочему каталогу, доставке и обработке сигналов, использованию и ограничениям ресурсов процесса.
#linuxcore #linux
👍12🔥6⚡3❤2🥰1👾1
Структура памяти процесса
Память, выделяемая каждому процессу, состоит из нескольких частей, которые обычно называют сегментами. К числу таких сегментов относятся следующие:
1. Текстовый сегмент (.text)
Cодержит машинный код, который принадлежит программе, запущенной процессом. Текстовый сегмент создается только для чтения, чтобы процесс не мог случайно изменить свои собственные инструкции (из-за read-only статуса, некоторые компиляторы могут записывать туда константы).
Поскольку многие процессы могут выполнять одну и ту же программу, текстовый сегмент создается с возможностью совместного использования - единственная копия кода программы может быть отображена на виртуальное адресное пространство всех процессов.
2. Сегмент инициализированных данных (.data)
Хранит глобальные и статические переменные, инициализированные явным образом. Значения этих переменных считываются из исполняемого файла при загрузке программы в память.
3. Сегмент неинициализированных данных (.bss - block started by symbol)
Содержит глобальные и статические переменные, не инициализированные явным образом. Перед запуском программы система определяет всю память в этом сегменте значением 0.
Основная причина помещения прошедших инициализацию переменных в отдельный от неинициализированных переменных сегмент заключается в том, что, когда программа сохраняется на диске, нет никакого смысла выделять пространство под неинициализированные данные.
Вместо этого исполняемой программе просто нужно записать местоположение и размер, требуемый для сегмента неинициализированных данных, и это пространство выделяется загрузчиком программы в ходе ее выполнения.
4. Динамически увеличивающийся и уменьшающийся сегмент стека (stack)
Содержит стековые фреймы: для каждой отдельной функции выделяется один стековый фрейм, в котором хранятся ее локальные переменные, аргументы и возвращаемое значение. По мере вызова функций и возврата из них стек расширяется и сжимается.
Текущая вершина стека отслеживается в специально предназначенном для этого регистре - указателе стека. Процесс сжатия стека называется расруткой.
5. Динамическая память (heap)
Область, которая предназначена для динамического выделения памяти в ходе выполнения программы (malloc, calloc, free, realloc - под капотом используют системные вызовы brk и sbrk).
6. Сегмент аргументов командной строки
Сегмент для хранения переменных, которые переданы программе в качестве аргументов (argc и argv[]), где argc хранит количество переданных аргументов, а argv хранит значение фактических значений вместе с именем файла:
Далее продемонстрированы различные типы переменных в коде и комментарии, указывающие на сегменты их размещения (в случае оптимизаций, часто используемые переменные могут быть помещены в регистры или вообще исключены):
В комменты я отправил несколько изображений для визуализации материала.
#linuxcore #linux #cpp #cppcore
Память, выделяемая каждому процессу, состоит из нескольких частей, которые обычно называют сегментами. К числу таких сегментов относятся следующие:
1. Текстовый сегмент (.text)
Cодержит машинный код, который принадлежит программе, запущенной процессом. Текстовый сегмент создается только для чтения, чтобы процесс не мог случайно изменить свои собственные инструкции (из-за read-only статуса, некоторые компиляторы могут записывать туда константы).
Поскольку многие процессы могут выполнять одну и ту же программу, текстовый сегмент создается с возможностью совместного использования - единственная копия кода программы может быть отображена на виртуальное адресное пространство всех процессов.
2. Сегмент инициализированных данных (.data)
Хранит глобальные и статические переменные, инициализированные явным образом. Значения этих переменных считываются из исполняемого файла при загрузке программы в память.
3. Сегмент неинициализированных данных (.bss - block started by symbol)
Содержит глобальные и статические переменные, не инициализированные явным образом. Перед запуском программы система определяет всю память в этом сегменте значением 0.
Основная причина помещения прошедших инициализацию переменных в отдельный от неинициализированных переменных сегмент заключается в том, что, когда программа сохраняется на диске, нет никакого смысла выделять пространство под неинициализированные данные.
Вместо этого исполняемой программе просто нужно записать местоположение и размер, требуемый для сегмента неинициализированных данных, и это пространство выделяется загрузчиком программы в ходе ее выполнения.
4. Динамически увеличивающийся и уменьшающийся сегмент стека (stack)
Содержит стековые фреймы: для каждой отдельной функции выделяется один стековый фрейм, в котором хранятся ее локальные переменные, аргументы и возвращаемое значение. По мере вызова функций и возврата из них стек расширяется и сжимается.
Текущая вершина стека отслеживается в специально предназначенном для этого регистре - указателе стека. Процесс сжатия стека называется расруткой.
5. Динамическая память (heap)
Область, которая предназначена для динамического выделения памяти в ходе выполнения программы (malloc, calloc, free, realloc - под капотом используют системные вызовы brk и sbrk).
6. Сегмент аргументов командной строки
Сегмент для хранения переменных, которые переданы программе в качестве аргументов (argc и argv[]), где argc хранит количество переданных аргументов, а argv хранит значение фактических значений вместе с именем файла:
~$ ./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
👍19🔥7❤3✍2🥰1👾1
Системные и библиотечные вызовы [1]
Ну чтож, начинаем погружение в интересную тему - рассмотрим то, без чего не обходится ни одно системное приложение - системные и библиотечные вызовы.
Тут, на самом деле, можно довольно глубоко копнуть. Если проявится ваш интерес, сделаем ряд постов, где вдоль и поперек разберем принцип работы вызовов, практики их использования и т.д.
Что такое системный вызов?
Можно считать, что все пользовательские программы работают внутри некоторой песочницы, из которой нельзя так просто выполнять ряд действий: получать доступ к файлам, передавать данные по сети, выводить текст на экран, создавать процессы и т.д. Для этого нужно обратиться к внутренним сервисам ядра и попросить его сделать что требуется через системные вызовы.
Системный вызов изменяет состояние процессора, переводя его из пользовательского режима в режим ядра, позволяя ему таким образом получить доступ к защищенной памяти ядра.
(Системные вызовы Linux перечисляются на странице руководства syscalls(2))
Это может вас немного напугать, но, обычно, системные вызовы недоступны напрямую. Все, с чем вы работаете - это тонкие функции-обертки библиотки glibc. Да-да, не удивляйтесь: read, write, fork являются всего лишь понятным пользователю интерфейсом, через который отрабатывают системные вызовы.
И да, важно помнить то, что системные вызовы - это платформозависимая история. Чтение файлов, взаимодействие с процессами, работа с сокетами, управление памятью и прочие системные операции реализованы по разному на каждой системе и могут отличаться друг от друга по API.
Как сделать прямой системный вызов?
Иногда бывает полезно явно обраться к системному вызову в случае, если для него не реализована сишная функция-обертка. Сделать это можно через библиотечный вызов syscall() из unistd.h, который первым параметром принимает номер системного вызова из "sys/syscall.h", а далее - аргументы самого вызова:
Таким образом, системный вызов write() можно инициировать по разному:
Что такое библиотечный вызов?
Библиотечным вызовом можно считать вызов функции стандартной библиотеки C, в которой не содержится прямой реализации системного вызова: системного прерывания, переноса аргументов в определенные, необходимые ядру, регистры, копирования номера системного вызова в регистр (%eax) и т.д.
Эти функции предназначены для решения широкого круга разнообразных задач: открытия файлов, преобразования времени в формат, понятный человеку, сравнения двух символьных строк и т.д.
Многие библиотечные функции вообще не используют системные вызовы (например, функции для работы со сроками). С другой стороны, некоторые библиотечные функции являются надстройками над системными вызовами с платформозависимой реализацией.
Например, библиотечная функция fopen() использует для открытия файла системный вызов open().
Зачастую библиотечные функции разработаны для предоставления более удобного интерфейса вызова по сравнению с тем, что имеется у исходного системного вызова.
Например, функция printf() предоставляет форматирование вывода и буферизацию данных, а внутренний системный вызов write() просто выводит блок байтов.
Аналогично этому функции malloc() и free() выполняют различные вспомогательные задачи, существенно облегчающие выделение и высвобождение оперативной памяти по сравнению с использованием исходного системного вызова brk().
#linuxcore #linux #cpp #cppcore
Ну чтож, начинаем погружение в интересную тему - рассмотрим то, без чего не обходится ни одно системное приложение - системные и библиотечные вызовы.
Тут, на самом деле, можно довольно глубоко копнуть. Если проявится ваш интерес, сделаем ряд постов, где вдоль и поперек разберем принцип работы вызовов, практики их использования и т.д.
Что такое системный вызов?
Можно считать, что все пользовательские программы работают внутри некоторой песочницы, из которой нельзя так просто выполнять ряд действий: получать доступ к файлам, передавать данные по сети, выводить текст на экран, создавать процессы и т.д. Для этого нужно обратиться к внутренним сервисам ядра и попросить его сделать что требуется через системные вызовы.
Системный вызов изменяет состояние процессора, переводя его из пользовательского режима в режим ядра, позволяя ему таким образом получить доступ к защищенной памяти ядра.
(Системные вызовы Linux перечисляются на странице руководства syscalls(2))
Это может вас немного напугать, но, обычно, системные вызовы недоступны напрямую. Все, с чем вы работаете - это тонкие функции-обертки библиотки glibc. Да-да, не удивляйтесь: read, write, fork являются всего лишь понятным пользователю интерфейсом, через который отрабатывают системные вызовы.
И да, важно помнить то, что системные вызовы - это платформозависимая история. Чтение файлов, взаимодействие с процессами, работа с сокетами, управление памятью и прочие системные операции реализованы по разному на каждой системе и могут отличаться друг от друга по API.
Как сделать прямой системный вызов?
Иногда бывает полезно явно обраться к системному вызову в случае, если для него не реализована сишная функция-обертка. Сделать это можно через библиотечный вызов syscall() из unistd.h, который первым параметром принимает номер системного вызова из "sys/syscall.h", а далее - аргументы самого вызова:
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
👍32🔥6❤🔥5❤2🖕1👾1
Разбор команд: strace
Продолжаем погружаться во вселенную системных вызовов - сегодня поговорим про команду strace и получим знания о том, как залезть в недра процесса и посмотреть на его общение с ядром через набор системных вызовов.
Команда strace преимущественно используется для того, чтобы отследить системные вызовы со стороны процесса. По сравнению с gdb, это довольно легкая в использовании утилита, которая позволяет вам приоткрыть ширму высокоуровневого API и посмотреть на то, что происходит "under the hood".
Давайте разберем принцип ее работы на простом примере:
Теперь давайте прогоним это через strace и посмотрим на результат:
Воу, это лишь малая часть того, что вывелось в консоль... Как говорилось ранее, strace дает нам список всех системных вызовов, сделанных нашей программой.
Сейчас давайте обратим внимание на один конкретный вызов, который произошел входе выполнения и подробно разберем структуру, по которой strace формирует вывод информации:
Во-первых, можно увидеть то, что наша программа где-то, вероятно, через библиотечную функцию printf(), делает обращение к вызову write() для того, чтобы записать строку "Hello, world" в файл по дескриптору 1, также известному, как "standard output" (13 - это размер буфера на вывод):
Во-вторых, так как вывод команды strace и вывод нашей программы произошел в один и тот же терминал, строка "Hello, world" прилипла к информации про сам вызов:
В-третьих, было напечатано возвращаемое значение 13, которое передается нашей программе и говорит о том, что вызов успешно записал 13 символов по указанному дескриптору:
Несколько сценариев использования:
1. Выяснить, какие конфиги читает программа на старте. Бывало же у вас такое: запускаете софт, ожидаете получить один результат, а тут Оппа, либо ничего не работает либо происходит то, чего вы вообще не ожидали...
strace может показать вам перечень интересующих вызовов, по которым вы сможете отследить код возврата и понять, в чем дело. В данном случае, нас интересует вызов open(), на который мы указываем через флаг '-e':
В результате видно, что процессу не получилось открыть "/usr/local/bin/php.ini" и он пошел к "/usr/local/lib/php.ini", с которым проблем не возникло. Вызов успешно отработал и вернул файловый дескриптор 4.
2. Узнать вызов, на котором процесс завис. Если программа по какой-то причине перестала отвечать на запросы, возможно, она в блокирующем режиме ожидает получения данных или что-то в этом духе. Найти причину можно попробовать через запуск с флагом "-p <pid>":
Да, мало информации, известно только то, что завис на вызове futex(). Если вы не обладаете дополнительной информацией, то, скорее всего, придется лезть в исходники. Однако, базовое представление о проблеме получить можно.
3. Понять, на что уходит процессорное время. Иногда бывает полезно быстро пристроить strace к программе и посмотреть, на полезную ли работу тратится наше CPU или мы просто чего-то ждем... Запуститься в режиме профилирования можно через флаг '-c':
Продолжаем погружаться во вселенную системных вызовов - сегодня поговорим про команду strace и получим знания о том, как залезть в недра процесса и посмотреть на его общение с ядром через набор системных вызовов.
Команда strace преимущественно используется для того, чтобы отследить системные вызовы со стороны процесса. По сравнению с gdb, это довольно легкая в использовании утилита, которая позволяет вам приоткрыть ширму высокоуровневого API и посмотреть на то, что происходит "under the hood".
Давайте разберем принцип ее работы на простом примере:
#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
....
👍25🔥17❤🔥3👾2❤1
