Работа с файлами формата 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 для генерации машинного кода.
Комментарии (5)
stalker_by
28.12.2021 14:46-5"Все любят Ruby, Haskel не любит никого" (с) не помню кто.
А зачем?
csl
28.12.2021 14:58+3DSL писать удобнее.
встроенном в Хаскел DSL
Lazy by default
ленивую строку
-
Зависимые типы
хоть и прикрученные сбокуДля симуляции Σ-типов использована библиотека
singletons
leshabirukov
28.12.2021 15:32... А DSL-у (правда, как раз не встроенному) может понадобиться интересный кодеген или линковка. Продолжения (continuations), самомодифицирующийся код и прочая медвежуть.
stalker_by
28.12.2021 23:07+1Для тех кто обиделся на Ruby: я не сравниваю языки, я просто помню времена когда Haskel на Хабре был такой хайповый, что про него шутили, что его все любят а он нелюбит никого.
Злые вы)
csl
"Как опытный тролль не могу не согласиться: эльфы — это важно."
https://habr.com/ru/post/193722/comments/#comment_6751396