Работа с файлами формата ELF -- популярная тема на Хабре. ("Введение в ELF-файлы в Linux: понимание и анализ", "Минимизация файла ELF – попробуем в 2021?" и т. д.)
Существуют библиотеки для Хаскела для работы с этими файлами: elf
(Hackage) и data-elf
(Hackage). Эти библиотеки работают только с заголовками и элементами таблиц и не дают возможности сгенерировать объектный файл.
Библиотека melf
(GitHub, Hackage) даёт возможность полностью разобрать файл ELF и сгенерировать такой файл по несложной структуре данных. Ниже даются примеры её использования.
Внутреннее устройство ELF
В файле формата ELF последовательно размещены заголовок файла, секции, сегменты, таблица секций, таблица сегментов. Сегменты в свою очередь скомпанованы из таких же элементов. Порядок этих элементов произвольный, за исключением того, что заголовок файла всегда размещается в начале файла, а таблиц секций и таблиц сегментов может быть не более одной. Каждый такой участок выровнен в файле, например, сегменты обычно выравниваются на размер страницы, а секции с данными -- на размер слова.
В заголовке описано, где располагаются таблица секций и таблица сегментов, которые в свою очередь описывают, где располагаются секции и сегменты.
Сегменты указывают, что нужно поместить в память при загрузки программы, а секции я бы определил как неделимые результаты работы компилятора. В секциях размещены исполняемый код, таблицы символов, инициализированные данные. Линковщик объединяет секции из различных единиц трансляции в сегменты.
Вполне валидным может быть файл, где сегмент содержит данные, не размеченные как какая-либо секция.
Базовый уровень
В модуле Data.Elf.Headers
🔗 реализованы разбор и сериализация заголовка файла ELF и элементов таблиц секций и сегментов. Для различения 64- и 32-битных структур определён тип ElfClass
🔗
data ElfClass
= ELFCLASS32 -- ^ 32-bit ELF format
| ELFCLASS64 -- ^ 64-bit ELF format
deriving (Eq, Show)
Некоторые поля заголовка и элементов таблиц секций и сегментов имеют разную ширину в битах, зависящую от ElfClass
, поэтому нужен тип WordXX a
🔗, который был позаимствован из пакета data-elf
:
-- | @IsElfClass a@ is defined for each constructor of `ElfClass`.
-- It defines @WordXX a@, which is `Word32` for `ELFCLASS32`
-- and `Word64` for `ELFCLASS64`.
class ( SingI c
, Typeable c
, Typeable (WordXX c)
, Data (WordXX c)
, Show (WordXX c)
, Read (WordXX c)
, Eq (WordXX c)
, Ord (WordXX c)
, Bounded (WordXX c)
, Enum (WordXX c)
, Num (WordXX c)
, Integral (WordXX c)
, Real (WordXX c)
, Bits (WordXX c)
, FiniteBits (WordXX c)
, Binary (Be (WordXX c))
, Binary (Le (WordXX c))
) => IsElfClass c where
type WordXX c = r | r -> c
instance IsElfClass 'ELFCLASS32 where
type WordXX 'ELFCLASS32 = Word32
instance IsElfClass 'ELFCLASS64 where
type WordXX 'ELFCLASS64 = Word64
Заголовок файла ELF представлен с помощью типа HeaderXX a
🔗:
-- | Parsed ELF header
data HeaderXX c =
HeaderXX
{ hData :: ElfData -- ^ Data encoding (big- or little-endian)
, hOSABI :: ElfOSABI -- ^ OS/ABI identification
, hABIVersion :: Word8 -- ^ ABI version
, hType :: ElfType -- ^ Object file type
, hMachine :: ElfMachine -- ^ Machine type
, hEntry :: WordXX c -- ^ Entry point address
, hPhOff :: WordXX c -- ^ Program header offset
, hShOff :: WordXX c -- ^ Section header offset
, hFlags :: Word32 -- ^ Processor-specific flags
, hPhEntSize :: Word16 -- ^ Size of program header entry
, hPhNum :: Word16 -- ^ Number of program header entries
, hShEntSize :: Word16 -- ^ Size of section header entry
, hShNum :: Word16 -- ^ Number of section header entries
, hShStrNdx :: ElfSectionIndex -- ^ Section name string table index
}
Для однообразной работы с форматами с разной шириной слова определён тип Header
🔗:
-- | Sigma type where `ElfClass` defines the type of `HeaderXX`
type Header = Sigma ElfClass (TyCon1 HeaderXX)
Header
это пара, первый элемент которой -- объект типа ElfClass
, определяющий ширину слова, второй -- HeaderXX
, параметризованный первым элементом (Σ-тип из языков с зависимыми типами). Для симуляции Σ-типов использована библиотека singletons
(Hackage, "Introduction to singletons").
Header
является экземпляром класса Binary
🔗. Таким образом, имея ленивую строку байт, содержащую достаточно длинный начальный отрезок файла ELF, можно получить заголовок этого файла, например, следующей функцией:
withHeader :: BSL.ByteString ->
(forall a . IsElfClass a => HeaderXX a -> b) -> Either String b
withHeader bs f =
case decodeOrFail bs of
Left (_, _, err) -> Left err
Right (_, _, (classS :&: hxx) :: Header) ->
Right $ withElfClass classS f hxx
Здесь decodeOrFail
🔗 определена в пакете binary
🔗, а withElfClass
🔗
делает явный аргумент, определяющий размер слова, неявным (constraint). Функция похожа на withSingI
🔗:
-- | Convenience function for creating a
-- context with an implicit ElfClass available.
withElfClass :: Sing c -> (IsElfClass c => a) -> a
withElfClass SELFCLASS64 x = x
withElfClass SELFCLASS32 x = x
В Data.Elf.Headers
определены также типы SectionXX
🔗, SegmentXX
🔗 и SymbolXX
🔗 для элементов таблиц секций, сегментов и символов.
Верхний уровень
В модуле Data.Elf
🔗 реализованы полные разбор и сериализация файлов формата ELF. Чтобы разобрать такой файл читаются заголовок ELF, таблицa секций и таблица сегментов и на основании этой информации создаётся список элементов типа ElfXX
🔗, отображающий рекурсивную структуру файла ELF. Кроме восстановления структуры в процессе разбора, по номерам секций восстанавливаются их имена. В результате получается объект типа Elf
🔗:
-- | `Elf` is a forrest of trees of type `ElfXX`.
-- Trees are composed of `ElfXX` nodes, `ElfSegment` can contain subtrees
newtype ElfList c = ElfList [ElfXX c]
-- | Elf is a sigma type where `ElfClass` defines the type of `ElfList`
type Elf = Sigma ElfClass (TyCon1 ElfList)
-- | Section data may contain a string table.
-- If a section contains a string table with section names, the data
-- for such a section is generated and `esData` should contain `ElfSectionDataStringTable`
data ElfSectionData
= ElfSectionData BSL.ByteString -- ^ Regular section data
| ElfSectionDataStringTable -- ^ Section data will be generated from section names
-- | The type of node that defines Elf structure.
data ElfXX (c :: ElfClass)
= ElfHeader
{ ehData :: ElfData -- ^ Data encoding (big- or little-endian)
, ehOSABI :: ElfOSABI -- ^ OS/ABI identification
, ehABIVersion :: Word8 -- ^ ABI version
, ehType :: ElfType -- ^ Object file type
, ehMachine :: ElfMachine -- ^ Machine type
, ehEntry :: WordXX c -- ^ Entry point address
, ehFlags :: Word32 -- ^ Processor-specific flags
}
| ElfSectionTable
| ElfSegmentTable
| ElfSection
{ esName :: String -- ^ Section name (NB: string, not offset in the string table)
, esType :: ElfSectionType -- ^ Section type
, esFlags :: ElfSectionFlag -- ^ Section attributes
, esAddr :: WordXX c -- ^ Virtual address in memory
, esAddrAlign :: WordXX c -- ^ Address alignment boundary
, esEntSize :: WordXX c -- ^ Size of entries, if section has table
, esN :: ElfSectionIndex -- ^ Section number
, esInfo :: Word32 -- ^ Miscellaneous information
, esLink :: Word32 -- ^ Link to other section
, esData :: ElfSectionData -- ^ The content of the section
}
| ElfSegment
{ epType :: ElfSegmentType -- ^ Type of segment
, epFlags :: ElfSegmentFlag -- ^ Segment attributes
, epVirtAddr :: WordXX c -- ^ Virtual address in memory
, epPhysAddr :: WordXX c -- ^ Physical address
, epAddMemSize :: WordXX c -- ^ Add this amount of memory after the section when the section is loaded to memory by execution system.
-- Or, in other words this is how much `pMemSize` is bigger than `pFileSize`
, epAlign :: WordXX c -- ^ Alignment of segment
, epData :: [ElfXX c] -- ^ Content of the segment
}
| ElfRawData -- ^ Some ELF files (some executables) don't bother to define
-- sections for linking and have just raw data in segments.
{ edData :: BSL.ByteString -- ^ Raw data in ELF file
}
| ElfRawAlign -- ^ Align the next data in the ELF file.
-- The offset of the next data in the ELF file
-- will be the minimal @x@ such that
-- @x mod eaAlign == eaOffset mod eaAlign @
{ eaOffset :: WordXX c -- ^ Align value
, eaAlign :: WordXX c -- ^ Align module
}
Не каждый объект такого типа может быть сериализован.
В конструкторе
ElfSection
остался номер секции. Он нужен, так как таблица символов и некоторые другие структуры ссылаются на секци по их номерам. Поэтому при построении объекта такого типа нужно убедиться, что секции пронумерованы корректно, т. е. последовательными целыми числами от 1 до количества секций. Секция с номером 0 всегда пустая, она добавляется автоматически.В структуре должен быть единственный
ElfHeader
, он должен быть самым первым непустым узлом в дереве.Если есть хотя бы один узел
ElfSection
, то должен присутсвовать в точности один узелElfSectionTable
и в точности одна секция, полеesData
которой равноElfSectionDataStringTable
(таблица строк для имён секций).Если есть хотя бы один узел
ElfSegment
, то должен присутсвовать в точности один узелElfSegmentTable
.
Корректно сформированный объект можно сериализовать с помощью функции serializeElf
🔗 и разобрать с помощью функции parseElf
🔗:
serializeElf :: MonadThrow m => Elf -> m ByteString
parseElf :: MonadCatch m => ByteString -> m Elf
Экземпляр класса Binary
для ELF
не определён, так как PutM
🔗 не является экземпляром класса MonadFail
.
Ассемблер как EDSL для Хаскела
Для использования в демонстрационных приложениях написан модуль, генерирующий машинный код для AArch64 (файл AsmAArch64.hs
🔗). Сгенерированный код использует системные вызовы чтобы вывести на стандартный вывод "Hello World!" и завершить приложение. Идея позаимствована из вдохновляющей статьи Стивена Дила "От монад к машинному коду" (Stephen Diehl "Monads to Machine Code"). Так же как в статье, используется монада состояния, в нашем случае CodeState
.
data CodeState = CodeState
{ offsetInPool :: CodeOffset
, poolReversed :: [Builder]
, codeReversed :: [InstructionGen]
, symbolsRefersed :: [(String, Label)]
}
CodeState
содержит размер массива литералов, сам массив литералов, массив машинных кодов и массив символов.
Массив литералов (literal pools) это участок секции в которой расположен исполняемый код, используемый для хранения константных данных. К таким данным легко обращаться с помощью команд, вычисляющих адрес данных с использованием счётчика команд.
Для создания меток и ссылок на данные в массиве литералов введён тип Label
newtype CodeOffset = CodeOffset { getCodeOffset :: Int64 }
deriving (Eq, Show, Ord, Num, Enum, Real, Integral,
Bits, FiniteBits)
data Label = CodeRef CodeOffset
| PoolRef CodeOffset
Конструктор CodeRef
используется для ссылки на код (для создания меток):
label :: MonadState CodeState m => m Label
label = gets (CodeRef . (* instructionSize)
. fromIntegral
. P.length
. codeReversed)
Конструктор PoolRef
хранит смещение данных от начала массива литералов. Он используется для создания CodeOffset
в функции emitPool
(см. ниже).
В массиве машинных кодов хранятся функции для генерации машинного кода из смещения команды от начала секции и смещения массива литералов (которое будет известно только после обработки всех ассемблерных команд, так как массив литералов располагается после кода):
type InstructionGen = CodeOffset ->
CodeOffset -> Either String Instruction
Для добавления функций в массив машинных кодов используется функция emit'
:
emit' :: MonadState CodeState m => InstructionGen -> m ()
emit' g = modify f where
f CodeState {..} = CodeState { codeReversed = g : codeReversed
, ..
}
emit :: MonadState CodeState m => Instruction -> m ()
emit i = emit' $ \ _ _ -> Right i
Каждая встретившаяся ассемблерная команда добавляет в этот массив очередной машинный код, например:
-- | C6.2.317 SVC
svc :: MonadState CodeState m => Word16 -> m ()
svc imm = emit $ 0xd4000001 .|. (fromIntegral imm `shift` 5)
Многие команды архитектуры AArch64 могут работать с регистрами как с 64-битными или как с 32-битными значениями. Для указания разрядности регистров для них используются разные имена: x0
, x1
... -- для 64-битных, w0
, w1
... -- для 32-битных. Регистры определены с помощью фантомного типа:
data RegisterWidth = X | W
type Register :: RegisterWidth -> Type
newtype Register c = R Word32
x0, x1 :: Register 'X
x0 = R 0
x1 = R 1
w0, w1 :: Register 'W
w0 = R 0
w1 = R 1
-- | C6.2.187 MOV (wide immediate)
mov :: (MonadState CodeState m, SingI w) =>
Register w ->
Word16 -> m ()
В системе команд AArch64 есть несколько вариантов команды mov
.
Реализована только команда с непосредственным широким аргументом (wide immediate).
Команда adr
работает с регистрами только как с 64-битными значениями:
-- | C6.2.10 ADR
adr :: MonadState CodeState m =>
Register 'X ->
Label -> m ()
Для добавления данных в массив литералов используется функция emitPool
:
emitPool :: MonadState CodeState m =>
Word ->
ByteString -> m Label
Здесь первый аргумент -- необходимое выравнивание, второй -- данные, которые нужно разместить в массиве. Функция вычисляет, сколько нужно добавить байт чтобы выравнять данные, заносит соответствующую нулевую последовательность байт в массив poolReversed
, добавляет в этот же массив данные и корректирует offsetInPool
.
С помощью этой функции можно, например, реализовать аналог ассемблерной директивы .ascii
:
ascii :: MonadState CodeState m => String -> m Label
ascii s = emitPool 1 $ BSLC.pack s
Символы создаются из меток:
exportSymbol :: MonadState CodeState m => String -> Label -> m ()
exportSymbol s r = modify f where
f (CodeState {..}) = CodeState { symbolsRefersed = (s, r) : symbolsRefersed
, ..
}
Используя таким образом определённые примитивы можно написать код для вывода "Hello World!" на встроенном в Хаскел DSL (файл HelloWorld.hs
🔗):
msg :: String
msg = "Hello World!\n"
-- | syscalls
sysExit, sysWrite :: Word16
sysWrite = 64
sysExit = 93
helloWorld :: MonadCatch m => StateT CodeState m ()
helloWorld = do
start <- label
exportSymbol "_start" start
mov x0 1
helloString <- ascii msg
adr x1 helloString
mov x2 $ fromIntegral $ P.length msg
mov x8 sysWrite
svc 0
mov x0 0
mov x8 sysExit
svc 0
Если нужно сослаться на метку, сформированную ниже по коду, нужно работать в монаде MonadFix
и использовать ключевое слово mdo
вместо do
(см. файл ForwardLabel.hs
🔗).
Генерация объектных файлов
Функция assemble
(см. AsmAArch64.hs
🔗) транслирует код на встроенном в Хаскел ассемблере в машинные коды и возвращает объект типа Elf
:
assemble :: MonadCatch m => StateT CodeState m () -> m Elf
Она запускает переданную в качестве аргумента монаду State
, представляющую ассемблерный код. Конечное состояние этой монады содержит всю информацию о содержимом секции, которая будет содержать код (секция с именем .text
), и таблицы символов, a этого достаточно чтобы сгенерировать объектный файл. На содержимое секции .text
ссылается имя txt
, на содержимое таблицы символов -- имя symbolTableData
, на содержимое таблицы строк, связанной с таблицей символов -- имя stringTableData
:
return $ SELFCLASS64 :&: ElfList
[ ElfHeader
{ ehData = ELFDATA2LSB
, ehOSABI = ELFOSABI_SYSV
, ehABIVersion = 0
, ehType = ET_REL
, ehMachine = EM_AARCH64
, ehEntry = 0
, ehFlags = 0
}
, ElfSection
{ esName = ".text"
, esType = SHT_PROGBITS
, esFlags = SHF_EXECINSTR .|. SHF_ALLOC
, esAddr = 0
, esAddrAlign = 8
, esEntSize = 0
, esN = textSecN
, esLink = 0
, esInfo = 0
, esData = ElfSectionData txt
}
, ElfSection
{ esName = ".shstrtab"
, esType = SHT_STRTAB
, esFlags = 0
, esAddr = 0
, esAddrAlign = 1
, esEntSize = 0
, esN = shstrtabSecN
, esLink = 0
, esInfo = 0
, esData = ElfSectionDataStringTable
}
, ElfSection
{ esName = ".symtab"
, esType = SHT_SYMTAB
, esFlags = 0
, esAddr = 0
, esAddrAlign = 8
, esEntSize = symbolTableEntrySize ELFCLASS64
, esN = symtabSecN
, esLink = fromIntegral strtabSecN
, esInfo = 1
, esData = ElfSectionData symbolTableData
}
, ElfSection
{ esName = ".strtab"
, esType = SHT_STRTAB
, esFlags = 0
, esAddr = 0
, esAddrAlign = 1
, esEntSize = 0
, esN = strtabSecN
, esLink = 0
, esInfo = 0
, esData = ElfSectionData stringTableData
}
, ElfSectionTable
]
Здесь имена с суффиксом SecN
(textSecN
, shstrtabSecN
, symtabSecN
, strtabSecN
) -- предопределённые номера секций, удовлетворяющие сформулированным выше условиям.
Для простоты не реализовано обращение ко внешним символам и размещение данных в
отдельных секциях. Всё это требует реализации таблиц перемещений, с другой стороны, сгенерированный код получается позиционно-независимым.
Сгенерируем с помощью этого модуля объектный файл и попробуем его слинковать:
[nix-shell:examples]$ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> :l AsmAArch64.hs HelloWorld.hs
[1 of 2] Compiling AsmAArch64 ( AsmAArch64.hs, interpreted )
[2 of 2] Compiling HelloWorld ( HelloWorld.hs, interpreted )
Ok, two modules loaded.
*AsmAArch64> import HelloWorld
*AsmAArch64 HelloWorld> elf <- assemble helloWorld
*AsmAArch64 HelloWorld> bs <- serializeElf elf
*AsmAArch64 HelloWorld> BSL.writeFile "helloWorld.o" bs
*AsmAArch64 HelloWorld>
Leaving GHCi.
[nix-shell:examples]$ aarch64-unknown-linux-gnu-gcc -nostdlib helloWorld.o -o helloWorld
[nix-shell:examples]$
Как видим, линковщик благополучно принял сгенерированный объектный файл. Попробуем запустить результат:
[nix-shell:examples]$ qemu-aarch64 helloWorld
Hello World!
[nix-shell:examples]$
Работает.
Генерация исполняемых файлов
Код из модуля DummyLd
🔗 использует секцию .text
объектного файла для того чтобы создать исполняемый файл. Перемещение кода и разрешение символов не реализовано, поэтому такая процедура сработает только с позиционно-независимым кодом, не ссылающимся на посторонние единицы трансляции, например, с кодом, который описан в предыдущем разделе.
Функция dummyLd
принимает объект типа Elf
, ищет в нём секцию .text
(функцией elfFindSectionByName
🔗) и заголовок ELF (функцией elfFindHeader
🔗). Тип заголовка меняется на ET_EXEC
, прописывается адрес, по которому будет располагаться первая инструкция кода и формируется сегмент, в который помещается заголовок и содежимое .text
:
data MachineConfig (a :: ElfClass)
= MachineConfig
{ mcAddress :: WordXX a -- ^ Virtual address of the executable segment
, mcAlign :: WordXX a -- ^ Required alignment of the executable segment
-- in physical memory (depends on max page size)
}
getMachineConfig :: (IsElfClass a, MonadThrow m) => ElfMachine -> m (MachineConfig a)
getMachineConfig EM_AARCH64 = return $ MachineConfig 0x400000 0x10000
getMachineConfig EM_X86_64 = return $ MachineConfig 0x400000 0x1000
getMachineConfig _ = $chainedError "could not find machine config for this arch"
dummyLd' :: forall a m . (MonadThrow m, IsElfClass a) => ElfList a -> m (ElfList a)
dummyLd' (ElfList es) = do
txtSection <- elfFindSectionByName es ".text"
txtSectionData <- case txtSection of
ElfSection { esData = ElfSectionData textData } -> return textData
_ -> $chainedError "could not find correct \".text\" section"
header <- elfFindHeader es
case header of
ElfHeader { .. } -> do
MachineConfig { .. } <- getMachineConfig ehMachine
return $ ElfList
[ ElfSegment
{ epType = PT_LOAD
, epFlags = PF_X .|. PF_R
, epVirtAddr = mcAddress
, epPhysAddr = mcAddress
, epAddMemSize = 0
, epAlign = mcAlign
, epData =
[ ElfHeader
{ ehType = ET_EXEC
, ehEntry = mcAddress + headerSize (fromSing $ sing @a)
, ..
}
, ElfRawData
{ edData = txtSectionData
}
]
}
, ElfSegmentTable
]
_ -> $chainedError "could not find ELF header"
-- | @dummyLd@ places the content of ".text" section of the input ELF
-- into the loadable segment of the resulting ELF.
-- This could work if there are no relocations or references to external symbols.
dummyLd :: MonadThrow m => Elf -> m Elf
dummyLd (c :&: l) = (c :&:) <$> withElfClass c dummyLd' l
Попробуем использовать этот код для получения исполняемого файла без участия линковщика GNU:
[nix-shell:examples]$ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/ :? for help
Prelude> :l DummyLd.hs
[1 of 1] Compiling DummyLd ( DummyLd.hs, interpreted )
Ok, one module loaded.
*DummyLd> import Data.ByteString.Lazy as BSL
*DummyLd BSL> i <- BSL.readFile "helloWorld.o"
*DummyLd BSL> elf <- parseElf i
*DummyLd BSL> elf' <- dummyLd elf
*DummyLd BSL> o <- serializeElf elf'
*DummyLd BSL> BSL.writeFile "helloWorld2" o
*DummyLd BSL>
Leaving GHCi.
[nix-shell:examples]$ chmod +x helloWorld2
[nix-shell:examples]$ qemu-aarch64 helloWorld2
Hello World!
[nix-shell:examples]$
Работает.
Заключение
В статье даны примеры использования библиотеки melf
и показано, как может быть определён встроенный в Хаскел DSL для генерации машинного кода.