имя выходного бинарника что это
Один бинарник, любое окружение. Магия чистого C
Как мы представляем себе кроссплатформенность? Мы пишем программу на языке, который либо компилируется в исполняемый файл отдельно для каждой поддерживаемой платформы, либо использует разновидность виртуальной машины вместо бинарника (и тогда эта среда должна присутствовать в целевых системах). Есть также и низкоуровневые языки, на которых писать серьёзные программы менее удобно, чем на высокоуровневых монстрах со своими компиляторами или рантаймами, но зато такие программы менее требовательны к предустановленному софту или наличию ОС в принципе, как и менее разборчивы в архитектуре. И всё же, есть возможность писать один и тот же код и собирать один и тот же бинарь под все популярные архитектуры и ОС (и даже bare metal), и эта возможность появилась благодаря гениальной Justine Tunney. Она написала Cosmopolitan, библиотеку на C, позволяющую исполнять один и тот же код на любой машине, подобно Java… но без какого-либо предустановленного интерпретатора или виртуальной машины! Один и тот же скомпилированный файл может исполняться как минимум в любом дистрибутиве Linux, на Mac OS, Windows NT, FreeBSD, OpenBSD, и NetBSD и на bare-metal на x86 и ARM*. Это настоящая магия.
* — для ARM всё же потребуется своего рода эмулятор, но он также встраивается в единственный исполняемый файл. При этом страдает минимальный размер файла, но не его производительность, и разница всё равно будет заметна только для совсем крохотных программ вроде hello world.
αcτµαlly pδrταblε εxεcµταblε
Всё началось с переосмысления формата Windows Portable Executable. Оказывается, совместив в одном файле заголовки Windows и UNIX, можно выполнять WPE как скрипт для Thompson shell: пока его не сменил sh в седьмой версии UNIX, скрипты не использовали шебанг. А значит, такой формат позволяет бинарнику запускаться на Windows, Linux и Mac OS:
Этот формат теперь называется αcτµαlly pδrταblε εxεcµταblε (если вы против пост-мета-сарказмо-иронии, лучше не читайте исходники у этой разработчицы, а формат называйте APE. Исходники правда классные). Проект Cosmopolitan — пример реального использования APE, развивавшийся от PoC до первых релизов и включения в другие проекты.
Нужна лишь одна строка, чтобы прокачать gcc для компиляции в APE:
Помимо простоты, Cosmopolitan удивляет легковесностью: hello world весит примерно в сто раз меньше аналога на «оптимизированном и кроссплатформенном» Go и занимает всего 16 килобайт! Вышеупомянутый эмулятор для ARM уменьшит превосходство с 100 до 10 раз, но такая большая разница только для такого малого размера. Но и это не всё: библиотека ещё и показывает великолепную производительность: чуть медленнее glibc, но с меньшим размером кода, и значительно быстрее Musl и Newlib при сопоставимом размере.
Например, по быстродействию функции memcpy() Cosmopolitan вообще всех обгоняет из-за специфичной механики копирования памяти:
Работает это так: чтобы ускорить часто используемые функции libc, функция вызывается внутри макроса, в котором компилятор получает информацию об используемых регистрах CPU, что позволяет экономить на сохранении состояния CPU, работая только с изменёнными регистрами. На примере memcpy:
При этом ускоряется не только сам объект оптимизации, но и, в качестве побочного эффекта, вызывающие его функции. Таким образом, только при применении оптимизации к memcpy, количество генерируемого кода для многих других функций уменьшилось на треть.
Вот так выглядит код функции strlcpy, BSD-аналог strcpy:
А теперь сравним результаты её компиляции:
Заключение
«As far as I’m concerned, this is literal magic», «this is the best programming-related thing I’ve seen on the internet in a long time», «This is one of the most interesting projects I have seen this year» — комментарии на hackernews и в твиттере буквально ломятся от восторженных возгласов. Несмотря на некоторые ограничения, концепция APE действительно выглядит как большой и важный прорыв в подходе к кроссплатформенности. Уже есть несколько реальных примеров использования Cosmopolitan, из них самым мощным точно можно назвать сервер Redbean. Однофайловый, независимый от платформы сервер. Потенциал этой штуки сложно даже мысленно охватить, а тред на HN собрал больше двух тысяч комментов. Помимо этого в твиттере автора периодически появляются всякие интерпретаторы (Lua, JS) и примеры помельче.
Облачные серверы по низким ценам для любых задач. Используем новейшее железо, лучший дата-центр в Москве уровня надёжности TIER IV, бесплатно предоставляем защиту от DDoS-атак на любом тарифном плане, который можно создать самостоятельно в течение мгновения.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!
В чем отличие бинарного файла от исходного?
Во время работы с Linux у вас есть возможность на выбор использовать два совсем разных способа установки программ, а именно:
Выбрать нужный необходимо отталкиваясь от ваших потребностей и требований к системе, ну и конечно от наличия навыков и опыта в развертывании ПО. Рассмотрим отдельно каждый из методов, их плюсы, а также минусы и трудности, которые могут встретиться при установке.
Бинарный файл
Для установки требуется специальная программа для распаковки этих файлов и помещения их на компьютер. То есть менеджер пакетов вашего дистрибутива Linux (например, apt, yum и т. д.). Менеджер пакетов также выполняет и другие полезные функции кроме распаковки, такие как отслеживание установленных файлов и управление обновлениями программного обеспечения.
Преимущества и плюсы использования бинарных файлов
Недостатки и минусы использования
Исходные файлы
Чтобы получить tar-архив с исходным кодом для определенного программного обеспечения, вам нужно знать URL-адрес к tar-архиву. После чего нужно распаковать скачанный tar-архив специальной командой tar для определенного типа расширения архива, чтобы получить доступ к файлам и возможность работать с исходником. Следующим шагом выполняются нужные настройки среды для компиляции и установки программного обеспечения из исходного кода.
Исходные файлы, написанные на разных языках, и нуждаются в специальных компиляторах и командах для преобразования его в исполняемый бинарный файл, который будет читаемым для системы и затем сможет запустить ваш компьютер.
Специальный набор инструментов помогает автоматизировать этот процесс. На десктопах Linux это обычно происходит в форме программы командной строки под названием make. Выше перечислены стандартные этапы, при выполнении каких возможно могут появляться ошибки, и будет необходимо выполнять дополнительные манипуляции, в этом и есть сложность внедрения проектов через исходные файлы.
Касательно вопроса, где можно найти исходный код к продукту, вариантов много, в большинстве случаев Вы можете загрузить исходный код проекта с таких сервисов, как GitHub или BitBucket. Некоторые владельцы ПО могут даже разместить его на личном веб-сайте.
Преимущества и плюсы использования исходных файлов
Недостатки и минусы использования
К минусам этот пункт можно и не относить, но для установки ПО с исходника потребуется уже наличие теоретических знаний и необходимых навыков в понимании документации к продукту, работы с терминалом и т.д., тут обычному пользователю может быть сложно.
Оба метода хороши и несут в себе разные цели использования. В большинстве случаев достаточно выбрать стандартный метод с помощью бинарных файлов.
Пишем ОС на Rust. Настройка среды. Бинарник для «голого» железа
Первый шаг в написании своей ОСи — создание бинарника, не зависящего от стандартных библиотек, это делает возможным запуск кода без ОС — мы же пишем свою.
Вступление
Для того, чтобы написать свою ОС, нам нужен код, не зависящий от библиотек или функций другой операционной системы. Это значит, что мы не можем использовать потоки исполнения (threads), файлы, память в куче (heap), сеть, вывод в терминал и так далее. Но это можно преодолеть, написав свою ОС и драйвера.
Мы не можем использовать большую часть стандартной библиотеки Rust, но также есть много функций, которые мы можем использовать. Например итераторы, замыкания, сопоставление с образцом, Option и Result, форматирование строк, и конечно же, концепцию владения. Это позволит написать своё ядро в высокоуровневом стиле, не заботясь о неопределённом поведении или безопасности памяти.
Эта статья рассказывает, как создать автономный исполняемый файл и почему это необходимо. Если нужен только пример, можно прокрутить до части «Заключение».
Отключение стандартной библиотеки
Сначала создадим проект с помощью Cargo. Это делается с помощью коммандной строки:
Атрибут no_std
Сейчас код собирается с использованием стандартной библиотеки. Это поведение можно выключить с помощью атрибута no_std :
Если попробовать собрать проект, получим ошибку:
Причиной этому является тот факт, что макрос println — часть библиотеки Rust, которую мы больше не используем. Получается, мы не можем ничего выводить. Это имеет смысл, так как для вывода нужна консоль или файл, а для них нужна запущенная ОС. 🙁
Так что удалим вывод и снова попробуем запустить сборку:
Реализация panic!()
Атрибут panic_handler говорит языку, что эту функцию нужно вызвать, если произошла паника (вызов panic!() ). В стандартных библиотеках она есть, но в среде no_std надо её создать самим:
eh_personality
Вообще, можно попробовать создать свою реализацию элементов языка, это нужно использовать только в крайнем случае, потому, что они очень часто меняются и даже не имеют проверки типов! Хорошо, что есть еще один способ.
Элемент языка eh_personality помечает функцию, которая используется для реализации «разматывания» стека вызовов. По умолчанию Rust использует это для вызова деструкторов всех переменных на стеке в случае паники, чтобы освободить всю использованную память. Но это сложный процесс, которому требуются библиотеки, специфические для каждой ОС ( libunwind на Linux и структурированная обработка исключений на Windows), то мы не будем это использовать.
Выключение разматывания
Rust дает возможность просто прерывать исполнение программы в случае паники. Это также исключает необходимые для разматывания данные, уменьшая бинарник. Есть несколько мест, где можно это выключить. Самое простое — Cargo.toml :
Мы пофиксили обе ошибки. Но теперь есть новая:
Атрибут start
Можно подумать, что функция main вызывается первой при исполнении программы. Но это не так для большинства языков. Много языков программирования имеют свою среду исполнения, которая отвечает за сборку мусора (Java, C#, JavaScript. ) или программных потоков исполнения (корутины, горутины в Go). Эту среду нужно инициализировать перед вызовом main с очевидных причин.
Переопределение точки входа
Ошибки сборщика
Сборщик — это программа, которая собирает ваш код и библиотеки, которые ему нужны, в единое целое. А так как разные системы имеют свои форматы исполняемых файлов, они имеют свои сборщики, и разные ошибки.
Надо сделать так, чтобы они не появлялись, нужно сказать сборщику, что мы не используем стандартные библиотеки. Это можно сделать 2 способами: передавая сборщику параметры, или созданием конфигурации целевой платформы.
Сборка для «железа»
Теперь можно скомпилировать код:
Заключение
Код для минимального бинарника, что запустится на голом железе выглядит так:
Для сборки — эта команда:
Но есть одно но. Это только минимальный пример. Этот бинарник ожидает некоторые вещи, например, что стек будет инициализирован. Если захотите использовать это где-то, надо доработать.
Бинарные файлы
Бинарные файлы
Т екстовые файлы хранят данные в виде текста (sic!). Это значит, что если, например, мы записываем целое число 12345678 в файл, то записывается 8 символов, а это 8 байт данных, несмотря на то, что число помещается в целый тип. Кроме того, вывод и ввод данных является форматированным, то есть каждый раз, когда мы считываем число из файла или записываем в файл происходит трансформация числа в строку или обратно. Это затратные операции, которых можно избежать.
Текстовые файлы позволяют хранить информацию в виде, понятном для человека. Можно, однако, хранить данные непосредственно в бинарном виде. Для этих целей используются бинарные файлы.
Выполните программу и посмотрите содержимое файла output.bin. Число, которое ввёл пользователь записывается в файл непосредственно в бинарном виде. Можете открыть файл в любом редакторе, поддерживающем представление в шестнадцатеричном виде (Total Commander, Far) и убедиться в этом.
Запись в файл осуществляется с помощью функции
Функция возвращает число удачно записанных элементов. В качестве аргументов принимает указатель на массив, размер одного элемента, число элементов и указатель на файловый поток. Вместо массив, конечно, может быть передан любой объект.
Запись в бинарный файл объекта похожа на его отображение: берутся данные из оперативной памяти и пишутся как есть. Для считывания используется функция fread
Функция возвращает число удачно прочитанных элементов, которые помещаются по адресу ptr. Всего считывается count элементов по size байт. Давайте теперь считаем наше число обратно в переменную.
fseek
Одной из важных функций для работы с бинарными файлами является функция fseek
Эта функция устанавливает указатель позиции, ассоциированный с потоком, на новое положение. Индикатор позиции указывает, на каком месте в файле мы остановились. Когда мы открываем файл, позиция равна 0. Каждый раз, записывая байт данных, указатель позиции сдвигается на единицу вперёд.
fseek принимает в качестве аргументов указатель на поток и сдвиг в offset байт относительно origin. origin может принимать три значения
В случае удачной работы функция возвращает 0.
Дополним наш старый пример: запишем число, затем сдвинемся указатель на начало файла и прочитаем его.
Вместо этого можно также использовать функцию rewind, которая перемещает индикатор позиции в начало.
В си определён специальный тип fpos_t, который используется для хранения позиции индикатора позиции в файле.
Функция
используется для того, чтобы назначить переменной pos текущее положение. Функция
используется для перевода указателя в позицию, которая хранится в переменной pos. Обе функции в случае удачного завершения возвращают ноль.
Рассмотрим пример: пользователь вводит числа. Первые 4 байта файла: целое, которое обозначает, сколько чисел было введено. После того, как пользователь прекращает вводить числа, мы перемещаемся в начало файла и записываем туда число введённых элементов.
Вторая программа сначала считывает количество записанных чисел, а потом считывает и выводит числа по порядку.
Примеры
1. Имеется бинарный файл размером 10*sizeof(int) байт. Пользователь вводит номер ячейки, после чего в неё записывает число. После каждой операции выводятся все числа. Сначала пытаемся открыть файл в режиме чтения и записи. Если это не удаётся, то пробуем создать файл, если удаётся создать файл, то повторяем попытку открыть файл для чтения и записи.
4. Функция saveInt32Array позволяет сохранить массив типа int32_t в файл. Обратная ей loadInt32Array считывает массив обратно. Функция loadInt32Array сначала инициализирует переданный ей массив, поэтому мы должны передавать указатель на указатель; кроме того, она записывает считанный размер массива в переданный параметр size, из-за чего он передаётся как указатель.
5. Создание таблицы поиска. Для ускорения работы программы вместо вычисления функции можно произвести сначала вычисление значений функции на интервале с определённой точностью, после чего брать значения уже из таблицы. Программа сначала производит табулирование функции с заданными параметрами и сохраняет его в файл, затем подгружает предвычисленный массив, который уже используется для определения значений. В этой программе все функции возвращают переменную типа Result, которая хранит номер ошибки. Если функция отработала без проблем, то она возвращает Ok (0).
6. У нас имеются две структуры. Первая PersonKey хранит логин, пароль, id пользователя и поле offset. Вторая структура PersonInfo хранит имя и фамилию пользователя и его возраст. Первые структуры записываются в бинарный файл keys.bin, вторые структуры в бинарный файл values.bin. Поле offset определяет положение соответствующей информации о пользователе во втором файле. Таким образом, получив PersonKey из первого файла, по полю offset можно извлечь из второго файла связанную с данным ключом информацию.
Зачем так делать? Это выгодно в том случае, если структура PersonInfo имеет большой размер. Извлекать массив маленьких структур из файла не накладно, а когда нам понадобится большая структура, её можно извлечь по уже известному адресу в файле.
Оптимизация размера Go-бинарника
Если вы когда-нибудь писали на Go, то размер получающихся бинарников не мог пройти мимо вашего внимания. Конечно, в век гигабитных линков и терабайтных дисков это не должно быть большой проблемой. Но все-таки встречаются ситуации, когда хочется, чтобы размер бинарника был как можно меньше, и при этом вы не хотите расставаться с Go. О вариантах, как сделать так, чтобы Go-бинарник “похудел”, пойдет речь ниже.
Цель aka «жертва»
Для начала немного контекста. Есть демон (постоянно запущенный процесс), который выполняет некоторую весьма несложную работу. Близкими аналогиями по манере работы могут быть DigitalOcean Agent или Amazon CloudWatch Agent, которые собирают метрики с машин и засылают их в централизованное хранилище. Наш демон выполняет немного другую задачу, но это не принципиально.
Еще несколько фактов о демоне:
На момент начала исследования размер Go-бинарника составлял 11 Мб.
Let Me See You Stripped
Скомпилированный бинарник содержит отладочную информацию. В моей ситуации она не нужна — отлаживать на целевых машинах все равно возможности нет ввиду отсутствия доступа. Так что можно смело удалить ее, скомпилировав с указанием нужных флагов или воспользовавшись утилитой strip. Процесс называется стриппингом (stripping) и для любителей Linux должен быть весьма знаком (описание флагов можно поглядеть в выводе go tool link ):
После этой процедуры размер бинарника составил 8,5 Мб. То есть отладочная информация давала +30% к размеру в данном конкретном случае.
Сжатие
Следующий ход конем — это использование компрессии. Можно просто попробовать сделать tar.gz-архив и распространять его. В целом, это рабочее решение. Возможность распаковать архив на целевых системах присутствует.
Другой вариант — воспользоваться упаковщиком, который будет делать распаковку и запуск бинарника “на лету”. Пожалуй, самый известный в этой сфере — это UPX. Первое мое знакомство с ним случилось, наверное, больше 20 лет назад, в эпоху dialup-модемов и crack/keygen-поделок. Несмотря на столь солидный возраст, UPX до сих пор находит своих пользователей и продолжает развиваться. Я пропустил момент эволюции, когда upx заработал из коробки для Go, но сегодня никаких дополнительных приседаний не требуется. Судя по истории, случилось это года 4 назад, так что все работает весьма стабильно.
Пробуем упаковать наш бинарник с помощью UPX:
На упаковку потребовалось 1,5 секунды, и мы получили бинарник размером в 3,4 Мб! Отличный результат.
Размер полученного бинарника составил 2,6 Мб, что в 4 раза меньше нашего первоначального варианта. Правда, процедура упаковки значительно замедлилась и заняла аж 134 секунды.
Размер бинарника — все те же 2,6 Мб (на самом деле, размер уменьшился, но всего на 8 Кб). К времени на упаковку добавилось еще 11 секунд, и суммарное время составило составило 145 секунд.
Мысль о том, что по скорости хочется как в первом варианте, а по размеру — как во втором и третьем, не давала покоя и привела к следующей команде:
В результате мы имеем все те же 2,6 Мб по размеру, но времени требуется всего лишь 4 секунды.
Тяжелые зависимости
Легкость добавления внешних зависимостей может иметь свои негативные последствия. Например, довольно просто добавить какой-то “очень нужный” модуль, который увеличит размер бинарника несоизмеримо приносимой пользы.
Есть очень хорошая (но часто игнорируемая) практика — организовывать мониторинг размера дистрибутива. Когда есть график соответствия “ревизия — размер дистрибутива”, то очень легко выяснить, какое из изменений привело к “ожирению”.
В моем случае к “ожирению” привела интеграция с Sentry. Если никогда не сталкивались с Sentry, то в двух словах, это сервис, собирающий информацию об ошибках, которые происходят в приложении. Такие штуки прикручивают в первую очередь для повышения качества и поиска проблем, возникающих в промышленной эксплуатации сервиса или продукта. Возвращаясь к проблеме “ожирения”, смотрим, что нам дает вариант без интеграции с Sentry. Упражнения опять начинаем с 11 Мб бинарника. Без “стрипания” после убирания интеграции размер составил 7,8 Мб, а после “стрипания” размер стал и вовсе 6,2 Мб. Почти в 2 раза меньше изначального!
Конечно, трекинг ошибок может хотеться сохранить. Но, в данном случае, мне дешевле организовать промежуточный сервис, куда отправлять сообщения об ошибках хоть по HTTP, а оттуда уже перенаправлять их в Sentry.
Еще раз про сжатие
После того, как разобрались с зависимостями пробуем еще раз воспользоваться upx:
Результирующий размер бинарника: 1,9 Мб! Напомню, что путь начался с 11 Мб.
Платой за компактный размер будет время на запуск, так как предварительно требуется распаковка. Грубый замер с помощью утилиты time в моем случае показывал увеличение на 170-180 миллисекунд. В контексте демона, где время работы несоизмеримо больше накладных расходов на запуск, это совершенно не является проблемой. Но иметь ввиду этот аспект нужно.
Другие варианты
Куда идти, если хочется большего?
Один из вариантов по решению проблемы доставки обновлений минимального размера — это бинарные патчи. Например, распространение обновлений Google Chrome использует эту концепцию. Есть утилиты bsdiff/bspatch, которые позволяют легко организовать такой процесс. В моем случае, утилита bspatch на целевых машинах отсутствует, поэтому пока для себя посчитал эти упражнения нецелесообразными. Хотя предварительные эксперименты показали весьма хорошие результаты в плане небольшого размера патчей.
Другой вариант, вскользь упомянутый в самом начале, — переписать все на другом языке. Если поставить размер во главу угла, то закончится все на Си. Время на разработку мне дорого, да и хочется испытывать удовольствие от процесса, поэтому — тоже нет.
Еще один любопытный вариант — это gccgo. Если целевые машины более-менее однообразны, то можно воспользоваться этим способом и получить Go-бинарник с динамической линковкой. Размер бинарника сильно порадует.
Это не мой случай (ОСи самые разные), но эксперимент я тоже попробовал провести:
Условия не совсем равные (это другая виртуалка и другая ОСь), но уже на старте мы получаем бинарник размером 1,8 Мб. Правда, с динамической линковкой. Применяем upx и получаем… всего 284 Кб! Главное, при переносе в другое окружение не удивляться потом ошибкам следующего характера:
В копилку экзотических компиляторов можно добавить TinyGo. Данный проект у меня им собрать не получилось — рассыпается с рядом ошибок на этапе компиляции. Но, в целом, успешные попытки уже были в контексте другого проекта (небольшого, но все-таки не “Hello, World!”). Бинарник будет с динамической линковкой, но количество зависимостей меньше, чем в варианте с gccgo (а значит, чуть меньше и проблем с портируемостью).
При наличии достаточного объема кода, зависящего от платформы, могут пригодиться build tags. Правила могут быть более хитрыми, чем просто именование файлов с суффиксами _windows.go или _linux.go. Размер экономии сильно зависит от конкретной ситуации. В моем случае экономии практически нет, так как основная целевая платформа — это Linux x86_64, а поддержка Mac и ARM — лишь эксперименты.
Docker
Нередко можно встретить, что Go-бинарник распространяется в виде Docker-контейнера. Например, для полной изоляции демона от хостовой системы и проброса только нужных файлов или директорий. В моей ситуации есть соседний демон, который распространяется именно так и используется на двух машинах. В этом случае более важны усилия по оптимизации размера не самого бинарника, а размера Docker-образа. Оптимальный по размеру Docker-образ создается с помощью довольно стандартного трюка с двухфазной сборкой:
Выводы
Итак, мы прошли путь с 11 Мб до 1,9 Мб, то есть сократили размер Go-бинарника практически в 6 раз! Стрипание бинарника с последующей упаковкой его с помощью upx — очень эффективная мера по сокращению размера. Не стоит пренебрегать и убиранием лишних зависимостей. В моем случае, это приводило к очень заметному сокращению. Если нет особой вариативности в средах для запуска бинарника, то можно присмотреться к варианту с gccgo.