Работа с файлами формата ELF -- популярная тема на Хабре.  ("Введение в ELF-файлы в Linux: понимание и анализ", "Минимизация файла ELF – попробуем в 2021?" и т. д.) Существуют библиотеки для Хаскела для работы с этими файлами: elf (Hackage) и data-elf (Hackage).  Эти библиотеки работают только с заголовками и элементами таблиц и не дают возможности сгенерировать объектный файл. Библиотека melf (GitHub, Hackage) даёт возможность полностью разобрать файл ELF и сгенерировать такой файл по несложной структуре данных.  Ниже даются примеры её использования. В файле формата ELF последовательно размещены заголовок файла, секции, сегменты, таблица секций, таблица сегментов.  Сегменты в свою очередь скомпанованы из таких же элементов.  Порядок этих элементов произвольный, за исключением того, что заголовок файла всегда размещается в начале файла, а таблиц секций и таблиц сегментов может быть не более одной. Каждый такой участок выровнен в файле, например, сегменты обычно выравниваются на размер страницы, а секции с данными -- на размер слова. В заголовке описано, где располагаются таблица секций и таблица сегментов, которые в свою очередь описывают, где располагаются секции и сегменты. Сегменты указывают, что нужно поместить в память при загрузки программы, а секции я бы определил как неделимые результаты работы компилятора.  В секциях размещены исполняемый код, таблицы символов, инициализированные данные.  Линковщик объединяет секции из различных единиц трансляции в сегменты. Вполне валидным может быть файл, где сегмент содержит данные, не размеченные как какая-либо секция. В модуле Data.Elf.Headers 🔗 реализованы разбор и сериализация заголовка файла ELF и элементов таблиц секций и сегментов.  Для различения 64- и 32-битных структур определён тип  ElfClass🔗 Некоторые поля заголовка и элементов таблиц секций и сегментов имеют разную ширину в битах, зависящую от  ElfClass, поэтому нужен тип WordXX a🔗, который был позаимствован из пакета data-elf: Заголовок файла ELF представлен с помощью типа HeaderXX a🔗: Для однообразной работы с форматами с разной шириной слова определён тип Header🔗: Header это пара, первый элемент которой -- объект типа ElfClass, определяющий ширину слова, второй -- HeaderXX, параметризованный первым элементом (Σ-тип из языков с зависимыми типами).  Для симуляции Σ-типов использована библиотека singletons (Hackage,  "Introduction to singletons"). Header является экземпляром класса Binary🔗.  Таким образом, имея ленивую строку байт, содержащую достаточно длинный начальный отрезок файла ELF, можно получить заголовок этого файла, например, следующей функцией: Здесь decodeOrFail🔗 определена в пакете binary🔗, а withElfClass🔗делает явный аргумент, определяющий размер слова, неявным (constraint).  Функция похожа на withSingI🔗: В Data.Elf.Headers определены также типы SectionXX🔗, SegmentXX🔗 и SymbolXX🔗 для элементов таблиц секций, сегментов и символов. В модуле Data.Elf🔗 реализованы полные разбор и сериализация файлов формата ELF.  Чтобы разобрать такой файл читаются заголовок ELF, таблицa секций и таблица сегментов и на основании этой информации создаётся список элементов типа ElfXX🔗,  отображающий рекурсивную структуру файла ELF.  Кроме восстановления структуры в процессе разбора, по номерам секций восстанавливаются их имена.  В результате получается объект типа Elf🔗: Не каждый объект такого типа может быть сериализован. В конструкторе ElfSection остался номер секции.  Он нужен, так как таблица символов и некоторые другие структуры ссылаются на секци по их номерам.  Поэтому при построении объекта такого типа нужно убедиться, что секции пронумерованы корректно, т. е. последовательными целыми числами от 1 до количества секций. Секция с номером 0 всегда пустая, она добавляется автоматически. В структуре должен быть единственный ElfHeader, он должен быть самым первым непустым узлом в дереве. Если есть хотя бы один узел ElfSection, то должен присутсвовать в точности один узел ElfSectionTable и в точности одна секция, поле esData которой равно ElfSectionDataStringTable (таблица строк для имён секций). Если есть хотя бы один узел ElfSegment, то должен присутсвовать в точности один узел ElfSegmentTable.  Корректно сформированный объект можно сериализовать с помощью функции serializeElf🔗 и разобрать с помощью функции parseElf🔗: Экземпляр класса Binary для ELF не определён, так как PutM🔗 не является экземпляром класса MonadFail. Для использования в демонстрационных приложениях написан модуль, генерирующий машинный код для AArch64 (файл AsmAArch64.hs🔗). Сгенерированный код использует системные вызовы чтобы вывести на стандартный вывод "Hello World!" и завершить приложение.  Идея позаимствована из вдохновляющей статьи Стивена Дила "От монад к машинному коду" (Stephen Diehl "Monads to Machine Code").  Так же как в статье, используется монада состояния, в нашем случае CodeState. CodeState содержит размер массива литералов, сам массив литералов, массив машинных кодов и массив символов. Массив литералов (literal pools) это участок секции в которой расположен исполняемый код, используемый для хранения константных данных.  К таким данным легко обращаться с помощью команд, вычисляющих адрес данных с использованием счётчика команд. Для создания меток и ссылок на данные в массиве литералов введён тип Label Конструктор CodeRef используется для ссылки на код (для создания меток): Конструктор PoolRef хранит смещение данных от начала массива литералов.  Он используется для создания CodeOffset в функции emitPool (см. ниже). В массиве машинных кодов хранятся функции для генерации машинного кода из смещения команды от начала секции и смещения массива литералов (которое будет известно только после обработки всех ассемблерных команд, так как массив литералов располагается после кода): Для добавления функций в массив машинных кодов используется функция emit': Каждая встретившаяся ассемблерная команда добавляет в этот массив очередной машинный код, например: Многие команды архитектуры AArch64 могут работать с регистрами как с 64-битными или как с 32-битными значениями.  Для указания разрядности регистров для них используются разные имена: x0, x1... -- для 64-битных, w0, w1... -- для 32-битных. Регистры определены с помощью фантомного типа: В системе команд AArch64 есть несколько вариантов команды mov. Реализована только команда с непосредственным широким аргументом (wide immediate). Команда adr работает с регистрами только как с 64-битными значениями: Для добавления данных в массив литералов используется функция emitPool: Здесь первый аргумент -- необходимое выравнивание, второй -- данные, которые нужно разместить в массиве.  Функция вычисляет, сколько нужно добавить байт чтобы выравнять данные, заносит соответствующую нулевую последовательность байт в массив poolReversed, добавляет в этот же массив данные и корректирует offsetInPool. С помощью этой функции можно, например, реализовать аналог ассемблерной директивы .ascii: Символы создаются из меток: Используя таким образом определённые примитивы можно написать код для вывода "Hello World!" на встроенном в Хаскел DSL (файл HelloWorld.hs🔗): Если нужно сослаться на метку, сформированную ниже по коду, нужно работать в монаде MonadFix и использовать ключевое слово mdo вместо do (см. файл ForwardLabel.hs🔗). Функция assemble (см. AsmAArch64.hs🔗) транслирует код на встроенном в Хаскел ассемблере в машинные коды и возвращает объект типа Elf: Она запускает переданную в качестве аргумента монаду State, представляющую ассемблерный код.  Конечное состояние этой монады содержит всю информацию о содержимом секции, которая будет содержать код (секция с именем .text), и таблицы символов, a этого достаточно чтобы сгенерировать объектный файл.  На содержимое секции .text ссылается имя txt, на содержимое таблицы символов -- имя symbolTableData, на содержимое таблицы строк, связанной с таблицей символов -- имя stringTableData: Здесь имена с суффиксом SecN (textSecN, shstrtabSecN, symtabSecN, strtabSecN) -- предопределённые номера секций, удовлетворяющие сформулированным выше условиям. Для простоты не реализовано обращение ко внешним символам и размещение данных в отдельных секциях.  Всё это требует реализации таблиц перемещений, с другой стороны, сгенерированный код получается позиционно-независимым. Сгенерируем с помощью этого модуля объектный файл и попробуем его слинковать: Как видим, линковщик благополучно принял сгенерированный объектный файл.  Попробуем запустить результат: Работает. Код из модуля DummyLd🔗 использует секцию .text объектного файла для того чтобы создать исполняемый файл. Перемещение кода и разрешение символов не реализовано, поэтому такая процедура сработает только с позиционно-независимым кодом, не ссылающимся на посторонние единицы трансляции, например, с кодом, который описан в предыдущем разделе. Функция dummyLd принимает объект типа Elf, ищет в нём секцию .text (функцией elfFindSectionByName🔗) и заголовок ELF (функцией elfFindHeader🔗). Тип заголовка меняется на ET_EXEC, прописывается адрес, по которому будет располагаться первая инструкция кода и формируется сегмент, в который помещается заголовок и содежимое .text: Попробуем использовать этот код для получения исполняемого файла без участия линковщика GNU: Работает. В статье даны примеры использования библиотеки melf и показано, как может быть определён встроенный в Хаскел DSL для генерации машинного кода.