Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- \documentclass[12pt]{article}
- % Эта строка — комментарий, она не будет показана в выходном файле
- \usepackage{ucs}
- \usepackage[utf8]{inputenc} % Включаем поддержку UTF8
- \usepackage[russian]{babel} % Включаем пакет для поддержки русского языка
- \title{Databases Course Project}
- \date{}
- \author{Anthony Belyaev}
- \usepackage{geometry} % А4, примерно 28-31 строк(а) на странице
- \geometry{paper=a4paper}
- \geometry{includehead=false} % Нет верх. колонтитула
- \geometry{includefoot=true} % Есть номер страницы
- \geometry{bindingoffset=0mm} % Переплет : 0 мм
- \geometry{top=20mm} % Поле верхнее: 20 мм
- \geometry{bottom=25mm} % Поле нижнее : 25 мм
- \geometry{left=25mm} % Поле левое : 25 мм
- \geometry{right=25mm} % Поле правое : 25 мм
- \geometry{headsep=10mm} % От края до верх. колонтитула: 10 мм
- \geometry{footskip=20mm} % От края до нижн. колонтитула: 20 мм
- \usepackage{amsmath} % \bar (матрицы и проч. ...)
- \usepackage{amsfonts} % \mathbb (символ для множества действительных чисел и проч. ...)
- \usepackage{mathtools} % \abs, \norm
- \DeclarePairedDelimiter\abs{\lvert}{\rvert}
- \DeclarePairedDelimiter\norm{\lVert}{\rVert}
- \usepackage{textcomp} %arrows
- \usepackage{listings} %листинги
- \lstset{
- basicstyle=\ttfamily,
- columns=fullflexible,
- keepspaces=true,
- frame=top,frame=bottom,
- }
- \usepackage[table,xcdraw]{xcolor}
- \usepackage{graphicx} %package to manage images
- \graphicspath{ {img/} }
- \begin{document}
- \begin{titlepage}
- \newpage
- \begin{center}
- \begin{footnotesize}
- \textit{Федеральное государственное бюджетное образовательное учреждение \\ высшего профессионального образования} \\
- \vspace{0.7cm}
- \textbf{Московский государственный технический университет имени Н.Э. Баумана \\ (МГТУ им. Н.Э. Баумана)} \\
- \vspace{0.7cm}
- Факультет: <<Информатика и системы управления>> \\
- Кафедра: <<Теоретическая информатика и компьютерные технологии>> \\
- \end{footnotesize}
- \end{center}
- \hrulefill
- \vspace{\fill}
- \begin{figure}[htbp]
- \centering
- \includegraphics[width=0.15\textwidth]{bmstulogo}
- \label{fig:bmstu_logo}
- \end{figure}
- \vspace{\fill}
- \begin{center}
- \Large Расчетно-пояснительная записка \\ к курсовому проекту по дисциплине \\ «Конструирование компиляторов»
- \end{center}
- \vspace{1em}
- \begin{center}
- \textsc{\textbf{Портирование компилятора BeRo TinyPascal \\
- на платформу Linux x64}}
- \end{center}
- \vspace{2em}
- \begin{center}
- \begin{tabular}{ r c l }
- Руководитель курсового проекта: & \underline{\hspace{4cm}} & (А. В. Коновалов) \\ & (подпись, дата) & \\ \\
- Исполнитель курсового проекта,\\студент группы ИУ9-72: & \underline{\hspace{4cm}} & (А. В. Беляев) \\ & (подпись, дата) & \\ \\
- \end{tabular}
- \end{center}
- \vspace{3em}
- \begin{center}
- Москва, 2017
- \end{center}
- \end{titlepage}
- \newpage
- {
- \tableofcontents
- \clearpage
- }
- {
- \section{ВВЕДЕНИЕ}
- }
- Основная цель данной работы --- портирование компилятора языка Pascal, BeRo TinyPascal на ОС Linux, используемого на кафедре ИУ9 для проведения лабраторных работ по курсу конструирования компиляторов. Исходный компилятор работает на платформе Windows 32-х битной разрядности. В процессе портирования необходимо увеличить разрядность до 64 бит.
- В ходе работы будет исследовано устройство исполняемых файлов ОС Windows, исходной платформы компилятора, а также ОС Linux, целевой платформы и произведено их сравнение. Будет исследована архитектура существующего компилятора и особенности его реализации в ОС Windows. Затем будет произведена попытка обеспечить подобную архитектуру в ОС Linux.
- Далее будет произведен непосредственно портирование компилятора, изучены последствия переноса и будет произведено его тестирование на новой платформе.
- \clearpage
- {
- \section{ОБЗОР ФОРМАТОВ ИСПОЛНЯЕМЫХ ФАЙЛОВ}
- }
- Прежде всего стоит рассмотреть устройство исполняемых файлов. Исполняемый файл(Executable file) представляет из себя программу в таком виде, в котором она может быть загружена в память и после этого исполнена. Перед исполнением могут быть также выполнены некоторые подготовительные операции, например, загрузка динамических библиотек и настройка окружения. Программа несет в себе решение определенной задачи, что влияет на ее внутреннее устройство.
- Внутри файла данные хранятся в определенном формате, который разделяет их на несколько составляющих частей. Общими для большинства форматов частями являются {\it заголовки, инструкции} и {\it метаданные}.
- Заголовки представляют из себя структуры, определяющие, что именно находится в определенной части файла, как данные из этой части должны быть интерпретированы, как они должны быть исполнены и так далее. Более формально, в заголовках могут быть указаны параметры окружения, исполнители инструкций, настройки этих исполнителей, а также формат инструкций и данных. Исполнителями в данном случае могут являться конкретные процессоры конкретной архитектуры (например, Intel 80386, i486 от Intel или Am386, Am486 от AMD архитектуры x86), микроконтроллеры, интерпретаторы, как программные, так и аппаратные. Также известными исполнителями являются всевозможные виртуальные машины, начиная от JVM, CPython, .NET и вполть до VirtualBox и VMware. В рамках данной работы целевыми исполнителями будут процессоры архитектуры x86\_64.
- Часть исполняемого файла с инструкциями может содержать как машинные инструкции, так и исходный код, или же байт-код виртуальной машины.
- Существуют и другие части файла, разнящиеся от формата к формату. Так, кроме заголовков и инструкций в файле могут присутствовать данные для загрузчика файла в память, данные для отладки, таблицы симоволов, константы, описание окружения, на котором подразумевается исполнение файла, а также всяческие изображения, тексты, архивы, иконки ярлыков и любые другие данные.
- Так как описанные выше части файла и их наполнение рознятся от формата к формату, существует привязка файлов к конкретным исполнителям, например файлы только для вирутальной машины Java. Наличие вызовов библиотечных функций связывает файл с конкретной библиотекой, которая в свою очередь может связывать его с определенным ядром ОС, набором модулей ОС и т.д.
- Среди наиболее популярных форматов файлов, которые будучи загруженными соответствующим загрузчиком, могут быть непосредственно выполенены CPU, а не интерпретироваться специальным ПО, можно выделить --- PE (на Microsoft Windows), ELF (на Unix-подобных ОС), Mach-O (на OS X и iOS) и устаревший формат MZ (на DOS).
- Исходным форматом в рамках данной работы является PE формат (его 32-х битная версия, PE32). Целевым форматом является ELF. Далее они будут рассмотрены подробнее, так как значительная часть работы связана с обеспечением архитектуры Bero TinyPascal, аналогичной формату PE32 в рамках формата ELF.
- \bigskip
- {
- \subsection{Формат PE32}
- }
- После того, как под ОС Windows написан код, подключены библиотеки и загружены ресурсы, все компонуется в единственный исполняемый файл формата .exe (для исполняемых файлов типов DLL и SYS также используется формат PE). Он содержит в себе всю информацию, необходимую PE-загрузчику для отображения файла в память.
- PE (Portable Executable, также известный, как PE/COFF) представляет из себя модифицированную под ОС Windows версию COFF формата Unix (не только Windows, но и ReactOS использует PE). С появлением Windows NT 3.1 в 1993 году Microsoft перешла на этот новый формат. Однако, для обратной совместимости с DOS-системами, формат сохранил ограниченную поддержку популярного на тот момент MZ. Так, формат PE на данный момент содержит в себе <<заглушку>> размером в 256 байт, в которую может быть записана мини версия программы размером в 192 байта и заголовок в 64 байта. Формат MZ на данный момент устарел и большинство программа содержат заглушку в виде вывода сообщения <<\verb|This program cannot be run in DOS mode|>>. Однако, формат PE32 уже начинает устаревать и появляются его расширения, например PE32+ (PE+) для x64, PE.NET.
- Перейдем непосредственно к техническим деталям формата. \cite{tour}. Для этого рассмотрим приблизительную схему его устройства.
- \begin{figure}[h]
- \caption{Схематическое устройство PE}
- \centering
- \includegraphics[width=0.4\textwidth]{pe_small}
- \label{peSmall}
- \end{figure}
- Рассмторение начнем сверху-вниз. Начинается файл, согласно Рисунку \ref{peSmall}, с заголовков. Первым заголовком является упомянутый выше DOS заголовок (DOS header) длиной в 64 байта. Останавиваться на нем не имет смысла, так как интерес в нем представляют лишь первое и последнее поле - e\_magic и e\_lfanew. Первое представляет из себя 2 байта сигнатуры родительского формата -- 0x4D 0x5A -- <<MZ>>. Последнее поле -- по смещению 0x3C -- содержит адрес PE-заголовка (PE header), который начинается в свою очередь с сигнатуры 0x50 0x45 -- <<PE>>. Если данные сигнатуры не соответствуют описанным, файл не загрузится, либо не будет исполняемым.
- Далее следует сама <<DOS-заглушка>>.
- Затем начинается заголовок файла (File header, или же COFF header). Формально, file header расположен внутри PE header'а, вместе с сигнатурой и опциональным заголовком. File header представлен струтктурой на Листинге \ref{peFileHdr}.
- \begin{lstlisting}[caption={Структура File header'а}, label={peFileHdr}]
- typedef struct _IMAGE_FILE_HEADER {
- WORD Machine;
- WORD NumberOfSections;
- DWORD TimeDateStamp;
- DWORD PointerToSymbolTable;
- DWORD NumberOfSymbols;
- WORD SizeOfOptionalHeader;
- WORD Characteristics;
- } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
- \end{lstlisting}
- Здесь содержится набор полей, описывающий базовые характеристики файла, такие, как архитектура процессора (Machine), количество секций в файле (ограниченное числом 96), дата и время создания файла, указатель на таблицу символов и размер таблицы символов (NumberOfSymbols), размер опционального заголовка и характеристики файла.
- %Рассмотрим только основные его поля на Листинге \ref{peOptinalHdr}.
- %
- %\begin{lstlisting}[caption={Структура Optional header'а}, label={peOptinalHdr}]
- %typedef struct _IMAGE_OPTIONAL_HEADER {
- % ...
- % DWORD AddressOfEntryPoint;
- % DWORD BaseOfCode;
- % DWORD BaseOfData;
- % DWORD SectionAlignment;
- % DWORD FileAlignment;
- % DWORD SizeOfImage;
- % DWORD SizeOfHeaders;
- % DWORD NumberOfRvaAndSizes;
- % IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
- % ...
- %} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;
- %\end{lstlisting}
- Далее следует опциональный заголовок (Optional header). Он содержит стандартные COFF поля, Windows-специфичные поля и директории данных. Представленные здесь поля -- RVA (Relative Virtual Address) адрес точки входа в программу, RVA начала секции кода (BaseOfCode), RVA начала секции данных (BaseOfData), размер выравнивания секций (в байтах) при выгрузке в вирутальную память (SectionAlignment), размер выравнивания секций внутри файла (FileAlignment), размер файла, кратный SectionAlignment, размер всех заголовков, кратный FileAlignment, количество каталогов в таблице директорий, всегда равное 16 (NumberOfRvaAndSizes) и сама таблица директорий, каждый элемент которой представлен структурой на Листинге \ref{peDataDir}.
- \begin{lstlisting}[caption={Структура элемента таблицы директорий, Data Directory}, label={peDataDir}]
- typedef struct _IMAGE_DATA_DIRECTORY {
- DWORD VirtualAddress;
- DWORD Size;
- } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- \end{lstlisting}
- Поле VirtualAddress Листинга \ref{peDataDir} -- RVA на таблицу, которой соответствует элемент. Второе поле -- размер. Элемент №0 , например, ответчает за таблицу экспорта, элемент №1 -- за таблицу импорта, второй -- за ресурсы (изображения, иконки, тексты) (IMAGE\_DIRECTORY\_ENTRY\_RESOURCE), №4, например, -- за безопасность (IMAGE\_DIRECTORY\_ENTRY\_SECURITY).
- Сразу за рассмотренным выше массивом идут друг за другом заголовки секций (Section headers). Для дальнейшей работы важно остановиться на данной структуре. Заголовок секции представлен на Листинге \ref{peSectionHdr}.
- \begin{lstlisting}[caption={Структура заголовка секции}, label={peSectionHdr}]
- typedef struct _IMAGE_SECTION_HEADER {
- BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
- union {
- DWORD PhysicalAddress;
- DWORD VirtualSize;
- } Misc;
- DWORD VirtualAddress;
- DWORD SizeOfRawData;
- DWORD PointerToRawData;
- DWORD Characteristics;
- ...
- } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
- \end{lstlisting}
- Характеристики секции представлены большим числом полей -- название, размер в виртуальной памяти (Virtual Size), размер в файле (SizeOfRawData), RVA адрес секции (VirualAddress), RAW смещение (смещение относительно начала файла) до начала секции, кратное FileAlignment, атрибуты секции (Characteristics) -- атрибуты содержимого (код, данные, неинициализированные данные), атрибуты доступа (чтение, запись, выполнение). Как видно из описанной выше структуры, секции в PE формате имеют давольно много характеристик.
- За заголовками, как следует из Рисунка \ref{peSmall}, следует следующий большой раздел файла -- секции. При загрузке в вирутальную память их размер увеличивается до размера, кратного выравниванию. Более наглядно это представлено на Рисунке \ref{peSectionMem}. \cite{pirates}. <<Дополнительное>> пространство, образовавшееся при этом в секции -- обнуляется.
- \begin{figure}[h]
- \caption{Выгрузка секций в память}
- \centering
- \includegraphics[width=1\textwidth]{peSectionMem}
- \label{peSectionMem}
- \end{figure}
- Исполняемый образ состоит из нескольких секций, каждая из которых требует различных прав доступа к памяти, а значит, начало секции должно быть выровнено по границе страницы. Чтобы не тратить место на диске, секции на нем не выровнены, а значит это дополнительная работа для загрузчика.
- Примерами секций могут служить таблицы импорта и экспорта. Таблица импорта (IAT, Import Address Table) соотносит вызовы функций динамических библиотек по имени или ординалу (порядковому номеру) с соответствующими адресами. Таблица экспорта (EAT -- Export Address Table) в свою очередь предоставляет адреса функций другим модулям, обращающимся к EAT за импортом. Это характерно для DLL библиотек. Еще один пример -- таблица релокаций, используемая для вычисления адреса загрузки в случае, если предпочтительный адрес (IMAGE\_OPTIONAL\_HEADER.ImageBase) оказался недоступен. Каждая секция также представлена своей специфической структурой, которая в свою очередь представляет из себя набор структур (например, <<путь>> до имен функций в таблице импорта или экспорта).
- Представленной выше информации достаточно для общего понимания внутреннего устройства файлов PE формата.
- Процесс загрузки можно вкратце описать так:
- \begin{itemize}
- \item Считывание и валидация сигнатур и заголовков.
- \item Выделение виртуальной памяти под приложение и попытка выгрузить его по предпочтительному адресу.
- \item Вычисление адреса и размера секций в виртуальной памяти, в том числе вычисление смещений и установка атрибутов страниц согласно атрибутам секций, обнуление секций до нужной длины.
- \item Анализ таблицы импорта и подгрузка соответствующих DLL, связывание импортируемых симоволов.
- \end{itemize}
- На данный момент уже можно представить, что проиходит с исполняемым файлом в исходной системе. Получив базовые сведения об устройстве PE, необходимо изучить и ELF, так как он является целевым форматом.
- \bigskip
- {
- \subsection{Формат ELF}
- }
- ELF (Executable and Linkable Format) -- формат исполняемых файлов во многих Unix-подобных системах, разработанный USL (Unix System Laboratories) в качестве переносимого для ОС на базе Intel x86. Затем, будучи доработанным компанией HP до стандарта ELF-64, распространился и для 64-разрядных систем. Стандарт ELF различает следующие типы файлов -- {\itперемещаемые файлы}, хранящие инструкции и данные для связи с объектными файлами, {\it разделяемые объектные файлы}, схожие с перемещаемыми и {\it исполняемые файлы}, содержащие необходимые данные для создания образа процесса.
- На первый взгляд схема ELF'а напоминает немного перекомпанованный PE файл. Рассмотрим ее устройство на Рисунке \ref{elfSmall}, и проверим, насколько верно это утверждение.
- \begin{figure}[h]
- \caption{Устройство ELF}
- \centering
- \includegraphics[width=0.36\textwidth]{elfSmall}
- \label{elfSmall}
- \end{figure}
- ELF-файл состоит из заголовка файла, таблицы программных заголовков, секций и таблицы заголовков секций в конце файла.
- Заголовок файла (ELF header) расположен в самом начале файла, как это следует из Рисунка \ref{elfSmall} и содержит общее описание структуры файла и его основные характеристики, такие, как тип, версия, архитектура, точка входа. Опустим детальное рассмотрение этой структуры.
- Таблица заголовков программы (Program header table), также известная как таблица заголовков сегментов (Segment table) идет следом за ELF header'ом. Каждый элемент этой таблицы содержит в себе тип сегмента (p\_type), расположение сегмента, точку входа, размер(p\_filesz, p\_memsz) и флаги доступа(p\_flags), согласно Листингу \ref{elfProgramHdr}. Сегменты необходимы для загрузки файла в память и исполнения. Только та часть данных, которую они охватывают нужна для работы с файлом. Остальное, с позиции загрузчика -- отладочная информация, или же метаданные.
- \begin{lstlisting}[caption={Программный заголовок (заголовок сегмента)}, label={elfProgramHdr}]
- typedef struct _ELF64_PHDR{
- Elf64_Word p_type;
- Elf64_Word p_flags;
- Elf64_Xword p_filesz;
- Elf64_Xword p_memsz;
- ...
- } Elf64_Phdr;
- \end{lstlisting}
- Далее следуют непосредственно секции. Секции необходимы при линковке (компоновке) файла.
- За ними вновь идет таблица -- таблица заголовков секций (Section header table). Ее структура представлена полями типа (sh\_type), смещения секции (sh\_offset), виртуальным адресом секции во время выполнения(sh\_addr) и размером (sh\_size) на Листинге \ref{elfSHdr}.
- \begin{lstlisting}[caption={Программный заголовок (заголовок сегмента)}, label={elfSHdr}]
- typedef struct _ELF64_SHDR{
- Elf64_Word sh_name;
- Elf64_Word sh_type;
- Elf64_Addr sh_addr;
- Elf64_Off sh_offset;
- Elf64_Xword sh_size;
- ...
- } Elf64_Shdr;
- \end{lstlisting}
- Стоит заметить, что Section header может располагаться не только в конце файла, как показано на Рисунке \ref{elfSmall}, но и, например, между секций. Его положение задано стандартом, как <<плавающее>>. \cite{elf}
- %ссылка на стандарт
- На этом, формально, описание заголовков можно закончить. Процесс выгрузки ELF файла в память во многом схож с таковым для PE файлов. Следующим пунктом этого раздела будет краткое сравнение форматов.
- \bigskip
- {
- \subsection{Сравение форматов}
- }
- На основании сказанного выше, можно отметить следующее:
- \begin{itemize}
- \item PE формат представлен гораздо бОльшим числом структур и всевозможных подструктур.
- \item Поля PE формата предоставляют загрузчику исчерпывающую информацию для выгрузки в память, делая код позиционно-зависимым.
- \item Размещение структур в PE файле задано более жестко.
- \item В PE имеется лишняя неиспользуемая <<заглушка времен DOS-овского прошлого>> системы.
- \item ELF использует полностью позиционно-независимый код и глобальную таблицу смещений, которая жертвует временем выполнения в пользу расходования памяти.
- \item В случае отсутствия необходимости в перебазировании адресов, PE-файлы имеют преимущество очень эффективного кода, но при наличии перебазирования издержки в использовании памяти могут быть значительными.
- \end{itemize}
- Исходя из всего этого, можно предположить, что работа с ELF файлом проще, чем с PE. Однако опровергнуть или доказать это утверждение удасться только при непосредственной работе по устройству архитектуры. На данный момент имеется достаточно информации об устройстве исходного и целевого форматов. Можно приступить к изучению самого компилятора.
- \bigskip
- {
- \section{ОБЗОР КОМПИЛЯТОРА BeRo TinyPascal}
- }
- Рассматриваемым в данной работе компилятором является компилятор языка Pascal, {\it Bero TinyPascal}. Он был разработан в 2006 году, тогда двадцатилетним немецким программистом-музыкантом Бенджамином <<BeRo>> Россо (Benjamin Rosseaux) и на данный момент распространяется под лицензией zlib (совместимой с GNU GPU) из его репозитория на GitHub (github.com/BeRo1985/berotinypascal).
- Компилятор является самоприменимым и преобразует исходный код (из подмножества языка Pascal) в 32-битный ассемблерный код платформы Windows x86.
- Условно его можно разделить на 2 части -- <<верхнюю>>, btpc.dpr, сам компилятор BTPC реализующий фазы анализа и синтеза, и <<нижнюю>>, rtl.asm, библиотеку RTL (RunTime Library), частично реализующую фазы синтеза.
- \bigskip
- {
- \subsection{Библиотека рантайма, RTL}
- }
- Библиотека RTL частично релизует фазу синтеза, а именно -- распределение памяти и запись выходного ассемблерного кода в выходной поток (файл). Непосредственно генерация кода на целевом языке происходит на <<верхнем>> уровне, в BTPC.
- Библиотека состоит из 9 встроенных функций:
- \begin{enumerate}
- \item RTLHalt — остановка программы.
- \item RTLWriteChar — запись char’а на stdout.
- \item RTLWriteInteger — запись целого на stdout, принимает два параметра: число и ширину вывода.
- \item RTLWriteLn — выводит на stdout символ новой строки.
- \item RTLReadChar — считывает символ из stdin, результат кладёт в \verb|EAX|.
- \item RTLReadInteger — считывает целое из stdin, результат кладёт в \verb|EAX|.
- \item RTLReadLn — пропускает стандартный ввод до конца файла или ближайшего перевода строки.
- \item RTLEOF — возвращает в \verb|EAX| число больше 1, если достигнут конец файла (следующий символ прочитать невозможно) или 0 в противном случае.
- \item RTLEOLN — устанавливает в \verb|DL| 1, если следующий символ \textbackslash n, 0 — в противном случае.
- \end{enumerate}
- Также имеется импорт утилитарных функций из библиотеки kernel32.dll таких, как HeapAlloc для работы с кучей, выделяющая 4 Мбайта памяти при инициализации, устанавливающая на них указатель стека и функция, HeapFree, в конце работы программы освобождающая эту память и функции обработки ввода данных -- GetStdHandle, SetConsole, ReadConsoleInputA.
- Все взаимодействие с библиотекой осущестляется посредством таблицы встроенных функций (RTLFunctionTable). В таблице, в описанном выше порядке находятся указатели на соответствующие функции, при этом таблица в течение работы всегда доступна в регистре \verb|ESI|. Особенностью работы рантайма является неизменяемость этого регистра.
- Примечательной особенностью, сбивающей поначалу с толку является взаимодействие RTL и BTPC. В этих файлах нет ни одной ссылки друг на друга, а в RTL после перехода на метку \verb|StubEntryPoint| и подготовки самой библиотеки, на последней строке присутствует метка \verb|ProgramEntryPoint|, указывающая по сути в никуда. То есть стандартным поведением RTL, как самостоятельного процесса будет ошибка сегментирования (Segmentation fault), см Листинг \ref{originEntry} (оригинальная нотация соблюдена).
- \begin{lstlisting}[caption={Метки StubEntryPoint и ProgramEntryPoint, получающие управление}, label={originEntry}]
- StubEntryPoint:
- INVOKE GetStdHandle,BYTE -10
- MOV DWORD PTR StdHandleInput,EAX
- INVOKE SetConsoleMode,EAX,1+2
- INVOKE HeapAlloc,EAX,HEAP_GENERATE_EXCEPTIONS+HEAP_ZERO_MEMORY
- +HEAP_CREATE_ALIGN_16,4194332
- ...
- .LIBRARY "kernel32.dll"
- IMPORT WriteFile "WriteFile"
- IMPORT HeapFree "HeapFree"
- ProgramEntryPoint:
- \end{lstlisting}
- Это первая встретившаяся <<особенность>> реализации. Идея BeRo будет понятна при рассмотрении самого BTPC.
- \bigskip
- {
- \subsection{Компилятор BTPC}
- }
- Компилятор BTPC (BeRo TinyPascal Comiler), представленный исходным кодом btpc.dpr и является <<верхней>> и главной частью системы. В нем происходит как {\it фаза анализа} -- чтение входного потока, лексический анализ, синтаксический анализ, генерация промежуточного представления, так и {\it фаза синтеза} -- генерация кода на целевом языке. Оптимизация, как стадия компиляции отсутствует впринципе.
- Код, как в промежуточном представлении, так и целевой,
- генерируется по входному потоку <<as is>>, как есть, то есть без каких-либо оптимизаций и изменений. Входным потоком на первом этапе служит поданный на stdin код. Входным потоком на втором этапе служит сгенерированный на первом этапе промежуточный код. Он представляет из себя обычный массив (\verb|Code|), каждый $i$-й элемент которого является идентификатором операции (которых 45), а $(i+1)$-й --- данными, связанными с операцией (\verb|Value|).
- После порождения промежуточного кода запускается процедура \verb|AssembleAndLink|, в которой с обходом массива промежуточного кода генерируется ассемблерный код в виде отдельных кодов инструкций в hex формате. Это также просходит в 2 этапа:
- На первом этапе библиотека RTL из пункта выше вручную компилируется в исполняемый файл rtl.exe. Для этого, вероятно, используется Borland Turbo Assembler со специальной конфигурацией, настроенной на получение минималистичного выходного файла. Этот файл преобразуется специальной программой, написанной BeRo также на Pascal (rtl2pas.dpr) в pascal-строки вида \verb|OutputCodeString(#77#90 ... #$90#$90)|, где после символов \verb|#| идут десятичные представления байтов поданной на вход программы, а дополнение до длины в 255 элементов происходит путем записи недостающего количества байтов \verb|$90|, являющихся кодами (opcode) ассемблерной инструкции \verb|NOP|, в конец строки. В примере выше, элементы \verb|#77#90| при переводе обратно в hex представление становятся байтами \verb|4D 5A|, или <<MZ>>, и являются сигнатурой PE файла, описанной ранее в данной работе. Очевидно, что файл размером в 1000 байт будет записан 4 подобными строками, последняя из которых дополнена до длины 255 инструкциями \verb|NOP| (No operation). Размер файла в строковом представлении при этом будет искуственно увеличен на 20 байт (20 инструкций \verb|NOP|). Полученные таким образом строки записываются в <<итоговую>> последовательность кода (массив \verb|OutputCodeData|).
- На данном этапе внутри BTPC имеется полноценный работоспособный (инициализирующий библиотеку и намеренно <<сваливающийся>> в Seg fault) файл, записанный в массив.
- \begin{lstlisting}[caption={Генерация инструкции байтами кода операции}, label={originMovEax}]
- procedure OCMovEAXDWordPtrESP;
- begin
- EmitByte($8b); EmitByte($04); EmitByte($24);
- LastOutputCodeValue:=locMovEAXDWordPtrESP;
- end;
- \end{lstlisting}
- Далее процедурами \verb|EmitChar(char)|, \verb|EmitByte(integer)|, \verb|EmitInt16(integer)|, \verb|EmitInt32(integer)| и более частными их случаями (например, процедура на Листинге \ref{originMovEax}, отвечающая за инструкцию \verb|MOV EAX,DWORD PTR [ESP]|) в массив поверх инструкций \verb|NOP| записываются порожденные из промежуточного кода ассемблерные инструкции, также в виде кодов операций, но уже с префиксом \verb|$| -- в hex формате.
- Однако, не все инструкции получили свои процедуры и бОльшая часть кода генерируется прямо в \verb|AssembleAndLink| блоками \verb|switch|-оператора, пободными представленному на Листинге \ref{originSwitch} блоку, эквивалентному инструкциям на Листинге \ref{originSwitchAsm}. Таким образом, на месте <<падения>> из метки \verb|ProgramEntryPoint| появляется новый, <<скомпилированный>> код, являющийся программой, поданной на вход компилятору.
- \begin{lstlisting}[caption={Генерация инструкций (opcode) из промежуточной инструкции OPStl}, label={originSwitch}]
- OPStL:begin
- OCPopEAX;
- Value:=Value-4;
- if Value=0 then begin
- EmitByte($89); EmitByte($04); EmitByte($24);
- end else if (Value>=-128) and (Value<=127) then begin
- EmitByte($89); EmitByte($44); EmitByte($24); EmitByte(Value);
- end else begin
- EmitByte($89); EmitByte($84); EmitByte($24); EmitInt32(Value);
- end;
- LastOutputCodeValue:=locNone;
- PC:=PC+1;
- end;
- \end{lstlisting}
- \begin{lstlisting}[caption={Ассемблерные инструкции блока OPStl}, label={originSwitchAsm}]
- POP EAX
- MOV DWORD PTR [ESP],EAX
- MOV DWORD PTR [ESP+BYTE Value],EAX
- MOV EAX,DWORD PTR [ESP+DWORD Value]
- \end{lstlisting}
- Pascal, который обрабатывает BTPC, является подмножеством обычного языка Pascal, в нем запрещены дженерики, шаблоны, перегрузка операторов и прочие <<новые>> возможности, которых не было в Delphi 7 и в самом BTPC на момент создания (2006 год), но при этом присутствуют и <<незадекларированные>> возможности, связанные с недоработкой логики компилятора (об этом позднее).
- В самом конце процедуры \verb|AssembleAndLink| находится еще одна архитектурная особенность BTPC -- патчевание выходного файла. Фрагмент этой части представлен на листинге \ref{originPatch}.
- \begin{lstlisting}[caption={Патчевание выходного файла}, label={originPatch}]
- { Size Of Code }
- PEEXECodeSize:=OutputCodeGetInt32($29)+(OutputCodeDataSize-CodeStart);
- OutputCodePutInt32($29,PEEXECodeSize);
- { Get section alignment }
- PEEXESectionAlignment:=OutputCodeGetInt32($45);
- ...
- { Calculate and patch image size }
- OutputCodePutInt32($5d,PEEXESectionVirtualSize+OutputCodeGetInt32($39));
- { Patch section raw size }
- OutputCodePutInt32($115,OutputCodeGetInt32($115)+(OutputCodeDataSize-CodeStart));
- \end{lstlisting}
- На Литинге \ref{originDump} представлен дамп памяти, соответствующий началу файла BTPC в PE32 формате. Как и упомяналось ранее, выходной файл скомипилированной RTL библиотеки очень мнималистичен. В частности на месте DOS-<<заглушки>> находится авторская сигнатура, хотя сигнатуры, необходимые PE-загрузчику тоже присутствуют (<<MZ>>, <<PE>>).
- \begin{lstlisting}[caption={Дамп BTPC}, label={originDump}]
- 0000-0010: 4d 5a 52 c3-42 65 52 6f-5e 66 72 00-50 45 00 00 MZR.BeRo ^fr.PE..
- 0000-0020: 4c 01 01 00-00 00 00 00-00 00 00 00-00 00 00 00 L....... ........
- 0000-0030: e0 00 0f 03-0b 01 00 00-3f cf 00 00-00 00 00 00 ........ ?.......
- 0000-0040: 00 00 00 00-c4 10 00 00-00 10 00 00-0c 00 00 00 ........ ........
- 0000-0050: 00 00 40 00-00 10 00 00-00 02 00 00-04 00 00 00 ..@..... ........
- \end{lstlisting}
- Исследованного ранее устройства формата PE32 и комментариев BeRo достаточно для понимания патчевания. Происходит следующее:
- \begin{itemize}
- \item Корректировка ближних переходов (\verb|JMP xx, CALL xx|) --- это связано с особенностью генерации меток и переходов по ним при кодогенерации.
- \item Корректировка размера кода (рассмотренное ранее поле SizeOfCode Optional header'а).
- \item Получение имеющегося выравнивания и патчевание с помощью него вирутальных размеров секций (SectionHeader.VirtualSize). Это необходимо при загрузке файла на исполнение, см Рисунок \ref{peSectionMem}.
- \item Корректировка \verb|ImageSize|'а, размера образа.
- \item Патчевание RAW размера секции (размер в файле).
- \end{itemize}
- Почти ко всем полям добавляется размер инжектированного кода входной программы.
- Компилятор работает, а значит, патчевание происходит успешно и нескольких подкорректированных полей достаточно, чтобы PE загрузчик обработал файл корректно.
- Однако, становится заметна еще одна неприятная особенность кода --- наличие <<магических>> констант, не к каждой из которых имеется соответсвующий поясняющий комментарий. Оптимальность порожденного кода также остает желать лучшего, хотя данная проблема находится вне контекста работы.
- На этом изучения входных данных и теоретической подготовки достаточно для перехода к портированию.
- \bigskip
- {
- \section{ПОРТИРОВАНИЕ}
- }
- Первым делом необходимо разобраться с внутренним устройством библиотеки RTL и портировать ее. Затем изменить кодогенерацию на порождение инструкций набора x64. Далее -- пропатчевать выходной ELF аналогичным описанному ранее способом и убедиться в работоспособности полученного ELF файла.
- \bigskip
- {
- \subsection{Библиотека RTL}
- }
- Верхнеуровневое описание библиотеки приводилось ранее. Теперь необходимо портировать ее на платформу amd64. Для этого нужно заменить системные вызовы к WinAPI на аналоги в Linux'е. Исходные системные вызовы BeRo реализовал с помощью макросов своей среды разработки.
- На платформе же amd64 систеные вызовы осуществляются подготовкой аргументов вызываемой функции с помощью регистров и затем вызовом инструкции \verb|syscall|. Более детально распределение регистров на архитектурах i386 и x86\_64 представлено в Таблице \ref{syscall}.
- Так как BeRo работал с макросами, использующими стэк для обращения к WinAPI, а системные вызовы \verb|syscall| используют регистры, необходимо постараться обеспечить аналогичную <<чистоту>> функций RTL, чтобы конечный резльтат функционировал так же, как и оригинал.
- Портируем простую функцию -- RTLWriteChar. Назначние понятно из названия. На Листинге \ref{originWriteChar} представлена оригинальная функция, а на листинге \ref{myWriteChar} -- ее портированная версия.
- \begin{table}
- \caption {Интерфейс 32 и 64-битных системных вызовов}
- \label {syscall}
- \begin{tabular}{|c|c|c|c|c|c|c|c|c|c|}
- \hline
- архитектура & инструкция & syscall \# & ret value & arg1 & arg2 & arg3 & arg4 & arg5 & arg6 \\ \hline
- i386 & int \$0x80 & eax & eax & ebx & ecx & edx & esi & edi & ebp \\ \hline
- x86\_64 & syscall & rax & rax & rdi & rsi & rdx & r10 & r8 & r9 \\ \hline
- \end{tabular}
- \end{table}
- Вызовом \verb|syscall| обратимся к фунции \verb|Write()|. Ее подбробную спецификацию можно получить командой <<\verb|man 2 write|>> в терминале. Распределим ее аргументы по соответствующим регистрам.
- \begin{lstlisting}[caption={Оригинал RTLWriteChar}, label={originWriteChar}]
- RTLWriteCharBytesWritten: DB 0x90, 0x8D, 0x40, 0x00
- RTLWriteChar:
- push esi
- lea esi, [esp+8]
- pushad
- invoke WriteFile, dword ptr StdHandleOutput, esi, byte 1,
- offset RTLWriteCharBytesWritten, byte 0
- popad
- pop esi
- ret 4
- \end{lstlisting}
- С портированием даже такой простой функции выявляется еще одна <<особенность>> --- данные внутри кода. На Листинге \ref{originWriteChar} количество байт, отправленное на stdout, хранится по метке \verb|RTLWriteCharBytesWritten|, которая расположена не в секции данных, а в секции кода, в связи с чем секция кода является единственной (!) секцией в файле и отмечена как {\it исполняемая, перезаписываемая}.
- Это может привести к непредвиденным последствиям в работе, начиная от Seg fault'а и заканчивания внедрением стороннего кода. Если не учесть всех деталей (что спустя 10 лет может быть давольно затруднительно даже для автора), можно столкнуться с большими трудностями, особенно с учетом не просто портирования, но и увеличнения разрядности инструкций.
- Обезопасить себя можно, отрефакторив код параллельно с портированием. Для инициализированных данных в новой версии введена секция \verb|.data|, дле неинициализированных -- секция \verb|.bss|.
- В связи с введением дополнительных регистров в x64 и меньшей надобности сохранять регистры в стеке при вызовах, из набора существующих инструкций исчезли аналоги инструкций \verb|pusha, pushad| и \verb|popa, popad| для 64-битных регистров. Но, поскольку так или иначе, при \verb|syscall|'ах регистры меняются, стоит самостоятельно ввести аналогичные инструкции в виде макросов -- \verb|pushall'| и \verb|popall|, запушивающих регистры от \verb|rdi| до \verb|rax| и возвращающих их из стека в обратном порядке. Эти и другие отладочные инструкции добавлены в секцию \verb|.bss|.
- Для отладочных целей и для соблюдения best practices введем также и создание стекового фрейма. \cite{zubkov}. Стоит заметить, что в портированной версии используется более привычный для Unix-подобных систем AT\&T синтаксис ассемблерных иснтрукций (см. Листинг \ref{myWriteChar}).
- \begin{lstlisting}[caption={Портированная версия RTLWriteChar}, label={myWriteChar}]
- .section .data
- RTLWriteIntegerBuffer: .byte 0x3c, 0x57, 0x72, 0x69, 0x74, 0x69, ...
- .section .text
- RTLWriteChar:
- pushall
- movq %rsp, %rbp
- movq $1, %rax #syscall #1 = Write();
- movq $1, %rdi #param1 = write_to = 1 = stdout
- movq %rbp, %rsi #p2 = write_from = %rbp = stack
- addq $72, %rsi #reach arg on top of stack
- movq $1, %rdx #p3 = count = single_byte
- syscall
- popall
- ret $8
- \end{lstlisting}
- Портируем следующую функцию, RTLWriteInteger. Ее листинг может быть опущен. Стоит лишь заметить, что эта функция используется для вывода целых чисел на stdout и она обращается к RTLWriteChar. Отдельно скомпилируем и соберем с помощью ld имеющийся рантайм:
- \centerline{\texttt{gcc -c rtl64.s}}
- \centerline{\texttt{ld rtl64.o -g -o rtl64}}
- Протестируем так все имеющиеся функции. На этом этапе появлилась еще одна типичная для работы с legacy кодом проблема --функция RTLReadInteger оказалась нерабочей потому, что прочитанное значение сохранялось в \texttt{EAX}, а затем просто затиралось при восстановлении регистров из стека. Судя по состоянию репозитория и отсутсвию коммитов, исправляющих баг, функция оставалась нерабочей на протяжении 10(!) лет. После уведомления об этом автора, функция спустя какое-то время была исправлена (значение регистра в стеке теперь обновляется), однако это еще раз поднимает вопрос эффективности работы с legacy кодом.
- Портированная функция была также исправлена.
- На данном этапе новые функции работают корректно и ведут себя предсказуемо. Портируем аналогично все остальные функции рантайма.
- Теперь, когда имеется полный набор RTL функций, необходимо подготовить окружение -- зарезервировать стек так, как это делает BeRo. Чтобы инициализировать достаточный для работы программы стек можно пройти по нему, записывая какие-либо значения через некоторые промежутки, пока не встретится Guard Page -- см Рисунок \ref{guard}.
- \begin{figure}[h]
- \caption{Резервирование стека}
- \centering
- \includegraphics[width=0.36\textwidth]{guard}
- \label{guard}
- \end{figure}
- При более детальном изучении проблемы выяснилось, что для каждого запущенного процесса Linux держит список регионов вирутальной памяти и в случае ошибки при обращении к странице, проверяет, не произошел ли Seg fault. Если не произошел, значит было обращение к неинициализированной странице, а значит необходимо лишь выделить еще одну страницу виртуальной памяти и добавить к региону. Столкнуться с Guard Page можно при этом только при использовании некоторых функций дебаггинга, содержащих \texttt{malloc}. Значит, нет необходимости в аналогичных действиях в новой библиотеке.
- Осталось лишь скомпилировать библиотеку в <<заглушку>> для вставки ее в BTPC. Так как программа для перевода написана на Delphi Pascal'е, необходимо найти все необходимые инструменты для ее запуска, что в 2017 году -- не сама простая задача. Так как цель пободной программы предельно ясна, было принято решение переписать эту программу на более удобном для парсинга файлов языке -- C++. С помощью новой программы rtl64topas переводим скомпилированную библиотеку в строковый формат и вставляем ее на место заглушки BTPC -- в функцию \texttt{EmitStubCode}.
- На этом можно считать, что библиотека RTL портирована и на новой платформе имеется набор функций, аналогичных оригинальной RTL.
- \bigskip
- {
- \subsection{Генерация кода}
- }
- Следующим этапом является изменение генерации кода, порождаемого на целевой платформе. Необходим набор инструкций x64.
- Переведем инсутркуцию <<\verb|MOV R10, (R12 + R13)|>> (в AT\&T-синтаксисе соответственно: \verb|mov (%r12, %r13), %r10|) вручную. Для этого понадобятся справочные материалы курса <<Низкоуровневое программирование>>. \cite{makarov}.
- %ссылка на x64b макарова
- \begin{figure}[h!]
- \caption{Вычисление кода операции вручную}
- \centering
- \includegraphics[width=0.95\textwidth]{modrm2}
- \label{modrm2}
- \end{figure}
- \begin{enumerate}
- \item Переводим номера операндов в двоичный вид. Регистр-приемник -- \texttt{R10} подходит под формат <<R?>>.
- \item Находим инструкцию \texttt{MOV} в таблице кодов инструкций i8086+. Нужен формат <<r\slash m \textrightarrow R?>>. Итого, КОП инструкции = 0x8B.
- \item \texttt{R10} в двоичном представлении имеет длину 4 бита, а значит не входит в отведенные 3 бита Rn байта ModR/M.
- \item Для представления подобных <<больших>> регистров понадобится байт <<Префикс REX>>.
- \item Старший бит \texttt{R10} уходит в бит R префикса REX и расширяет Rn в байте ModR/M.
- \item Согласно таблице ModR/M для 32-разрядных инструкций, биты R/M байта ModR/M = 100, а биты Mod = 00. Это дает дополнительный байт SIB.
- \item Старший бит регистра \texttt{R12}, равный 1, взводит бит X в префиксе REX и расширяет номер индекса в SIB.
- \item Байт SIB выглядит, как \verb|[#Base + #Index*|$2^{Scale}$\verb|]|. Базу образует регистр \texttt{R12}. 3 младших его бита = 100 и устанавливаются на место 3х битов Base в SIB.
- \item Индекс(Index) в SIB составляют 3 младших бита регистра \texttt{R13} = 101.
- \item Так как в инструкции простое сложение (1 регистр + 1 регистр), биты Scale в байте SIB = 00 и дают $2^{Scale} = 2^0 = 1$.
- \item Старший бит регистра \texttt{R13} уходит в бит B префикса REX. Это расширяет набор базового регистра в байте SIB.
- \item Сконкатенируем байт префикса REX: <<0100>> + <<W:1>> + <<R:1>> + <<X:1>> + <<B:1>> = 01001111, что в hex записи дает 0x4F. Префикс REX найден.
- \item Сконкатенируем байт ModR/M: <<Mod:00>> + <<Rn:010>> + <<R/M:100>> = 00010100, что дает байт 0x14.
- \item Сконкатенируем наконец байт SIB: <<Scale:00>> + <<Index:101>> + <<Base:100>> = 00101100, что дает байт 0x2C.
- \end{enumerate}
- Путем конкатенации полученных байтов имеем инструкцию <<\verb|MOV R10, (R12 + R13)|>> в виде 0x4F 0x8B 0x14 0x2C. На Рисунке \ref{modrm2} описанный выше процесс представлен более наглядно.
- Подобных инструкций, требующих перевода в BTPC около 70, без повторений. Очевидно, что этот способ совсем не оптимален и очень затратен. Один упущенный бит меняет инстукцию или создает несуществующую. В связи с этим и после того, как процесс перевода стал понятен, можем воспользоваться более быстрым путем --- выписать необходимые инструкции в формате x64, скомпилировать файл а затем дизассемблировать его. На выходе получим все, что необходимо, чтобы заменить генерируемые байты в процедурах, пободных Листингам \ref{originMovEax} и \ref{originSwitch}.
- После этого кодогенерацию можно также считать портированной, так как теперь инструкции порождаются в новом, 64-битном формате.
- \bigskip
- {
- \subsection{Формирование ELF файла}
- }
- Теперь, когда имеется портированная библиотека в функции \texttt{EmitStubCode}, а новый код генерируется в формате x64, остается лишь пропатчевать ELF файл, собранный в заглушке, подобно патчеванию PE файла у BeRo.
- При компиляции и обычной линковке командами
- \centerline{\texttt{gcc -c rtl64.s}}
- \centerline{\texttt{ld rtl64.o -g -o rtl64}}
- в файл может попасть множество <<мусора>> -- функции библиотек, таблицы символов, лишние секции и прочие метаданные, в которых нет надобности при загрузке и выполнении файла. Мусором в данном случае считается все, чего нет в <<образцовом>> и очень минималистичном исходном PE файле. На Листинге \ref{elfSections} представлены секции полученного обычной линковкой файла, и найденные утилитой readelf командой <<\verb|readelf -S rtl64|>>.
- Особенностью ELF формата является наличие пустой секции. Она всегда имеет нулевой номер. Далее идет секция кода \texttt{.text}, затем данных \texttt{.data}. После чего следуют отладочные секции -- секция с именами секций \texttt{.shstrtab} (Section header string table), секция с таблицей символов \texttt{.symtab} и наконец секция строк- меток в ассемблерном коде \texttt{.strtab}.
- Сразу вырисовывается следующая проблема -- секция кода, к которой должен был конкатенироваться код находится в самом начале файла, так что дописывание кода после метки \texttt{ProgramEntryPoint} в RTL сделает файл неработоспособным. В эталонном файле присутствует лишь одна секция. Постараемся добиться максимально приближения к этому, но с учетом введенных при рефакторинге дополнительных секций (данных инициализированных и неинициализированных). То есть, необходимо оставить лишь 2 секции (кода и данных), при этом приблизив секцию кода к концу файла.
- \begin{lstlisting}[caption={Секции файла rtl64}, label={elfSections}]
- [#] Name Type Address Offset
- Size Size.Ent Flags Link Info Align
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .text PROGBITS 00000000004000b0 000000b0
- 0000000000000339 0000000000000000 AX 0 0 1
- [ 2] .data PROGBITS 00000000006003e9 000003e9
- 0000000000000067 0000000000000000 WA 0 0 1
- [ 3] .shstrtab STRTAB 0000000000000000 00000450
- 0000000000000027 0000000000000000 0 0 1
- [ 4] .symtab SYMTAB 0000000000000000 000005f8
- 0000000000000420 0000000000000018 5 40 8
- [ 5] .strtab STRTAB 0000000000000000 00000a18
- 000000000000024e 0000000000000000 0 0 1
- \end{lstlisting}
- Лишние секции никак не заданы в исходном коде RTL64, а значит попадают в файл на стадии линковки. Это заметно по размеру файла после этих действий. Зададим линкеру не генерировать мусор в виде функций библиотеки stdlib и секций с таблицами:
- \centerline{\texttt{ld rtl64.o -g -o rtl64 -nostdlib -s}}
- При повторном чтении ELF'а оказывается, что секция \texttt{shstrtab} осталась и все так же хранит имена других секций. Нулвая секция тоже на месте, но это не существенно.
- В комплекте binutils есть утилита для редактирования в том числе и исполнимых файлов. С помощью этой утилиты, strip, <<вырезаем>> оставшуюся секцию \texttt{shstrtab}:
- \centerline{\texttt{strip -R shstrtab rtl64}}
- Однако, вновь прочитав файл на предмет секций обнаруживаем там \texttt{shstrtab}. Так как секция хранит имена других секций, она всегда генерируется при линковке и всегда в конце файла. Однако, для выполнения, согласно спецификации, она не нужна. Так что можно будет очистить ее при генерации кода. Остается проблема с тем, что секция кода все еще внутри файла и прикрыта секцией данных.
- Для того, чтобы поменять местами секции необходимо использовать специальный линкер-скрипт для ld. Это файл, представляющий какие данные куда отобразятся при линковке файла, в том числе здесь задается и порядок вхождения секций. Выполним линковку согласно скрипту на Листинге \ref{linker}, заменив им стандартный скрипт (может быть получен командой <<\verb|ld --verbose|>>). Для этого запустим линкер с еще одной опцией:
- \centerline{\texttt{ld rtl64.o -g -o rtl64 -T linkerScript.ld -nostdlib -s}}
- В представленном скрипте указана точка входа --\texttt{\_start}, адреса размещения секций данных и кода, и выделяемая память на стэке и в куче.
- \begin{lstlisting}[caption={Скрипт линкера ld}, label={linker}]
- stack_size = 4194332;
- heap_size = 256;
- ENTRY(_start)
- SECTIONS
- {
- . = 0x4000b0;
- .data : { *(.data) }
- .bss : { *(.bss) *(COMMON) }
- . = 0x6000d3;
- .text : { *(.text) }
- }
- \end{lstlisting}
- После линковки по этому скрипту, секции кода и данных поменялись местами. Секция \texttt{shstrtab} все также замыкает файл. Но из-за минималистичности скрипта, линкер перестал делать какие-либо оптимизации и вычисления, полностью положившись на файл, в котором ничего кроме порядка секций не задано. После некоторых манипуляций приводим текущий скрипт к виду стандартного скрипта (verbose), но с описанными в Листинге \ref{linker} изменениями. Получаем оптимизированный скомпанованный файл. Обновляем заглушку \texttt{EmitStubCode} в BTPC.
- Пробуем собрать компилятор, отключив генерацию кода. На данном этапе компилятор лишь выводит заглушку. Выводит успешно. Файл, наполненый \texttt{NOP}'ами для выравнивания под размер в 255 может быть запущен и также предсказуемо и намеренно сваливается в Seg fault.
- Однако, при тестировании есть проблема -- файл собран слишком минималистично и в нем отсутствует какая-либо полезная при отладке информация. Несмотря на то, что код RTL64 был тщательно изучен, отлаживать и тестировать ассемблерный код без таблицы символов и без единственной именованной метки очень затруднительно. В связи с этим было приянто решение вернуть на место секции \texttt{symtab} и \texttt{strtab}, убрав соответствующие опции линкера.
- На данный момент RTL64 может быть протестирован и отлажен, так как внутри устроен абсолютно корректно. Это одно из преимуществ новой библиотеки перед оригиналом, где отстстуют какие-либо метки, а многие команды распознаются неправильно из-за наличия байтов данных в коде, которые дизассемблером путаются с реальными командами.
- Теперь необходимо отправить на выполнение весь сгенерированный код, содержащий логику компилируемой программы. Для этого необходимо пропатчевать поля файла согласно внесенным изменениям.
- Поскольку секция кода, хоть и была перенесена в конец, все равно осталась скрытой отладочными секциями, необходимо <<разрезать>> файл надвое, вставить туда код и <<склеить>> обратно. На данный момент представление файла полностью соответствует изображенному на Рисунке \ref{elf}.
- \begin{figure}[h!]
- \caption{Устройство конечного ELF файла}
- \centering
- \includegraphics[width=0.8\textwidth]{elf}
- \label{elf}
- \end{figure}
- Кав видно из Рисунка \ref{elf}, файл выстроен в соответсвии описанными ранее требованиями и допусками: секция текста идет после секции данных, за ней следуют отладочные секции, среди которых имеется и таблица заголовков секций. На этой схеме также показаны поля ELF, рассматривавшиеся в самом начале работы, а именно -- поля программных заголовков (в данном случае Data program header) и поля заголовков секций, такие, как {\it имя} (sh\_name), содержащее смещение внутри секции \texttt{shstrtab} по которому лежит название этой секции, {\it смещение} (sh\_offset), указывающее на начало секции кода в файле и {\it размер секции} (sh\_size).
- Код компилируемой программы будет порождаться в секции кода поверх операций \texttt{NOP}, положение которых отмечено серым цветом.
- Итого, чтобы добиться работоспособности подобного файла, необходимо пропатчевать как минимум поля, указанные в Таблице \ref{patch}.
- \begin{table}
- \caption {Редактируемые поля}
- \label {patch}
- \begin{tabular}{|c|c|c|c|}
- \hline
- ~ & Смещение до поля & Размер & Значение \\ \hline
- Elf\_hdr.e\_shoff & 0x28 & 8 & += injSize \\ \hline
- Text\_phdr.p\_filesz & s(Elf\_hdr)+s(p\_hdr)+0x20 & 4 & += injSize \\ \hline
- Text\_phdr.p\_memsz & s(Elf\_hdr)+s(p\_hdr)+0x28 & 4 & += injSize \\ \hline
- Text\_shdr.sh\_size & Elf\_hdr.e\_shoff+injSize+2*s(s\_hdr)+0x20 & 8 & += injSize \\ \hline
- Shstrtab\_shdr.sh\_offs & Elf\_hdr.e\_shoff+injSize+3*s(s\_hdr)+0x18 & 8 & += injSize \\ \hline
- Symtab\_shdr.sh\_offs & Elf\_hdr.e\_shoff+injSize+4*s(s\_hdr)+0x18 & 8 & += injSize \\ \hline
- Strtab\_shdr.sh\_offs & Elf\_hdr.e\_shoff+injSize+5*s(s\_hdr)+0x18 & 8 & += injSize \\ \hline
- \end{tabular}
- \end{table}
- Значения всех представленных полей должны быть увеличены на размер сгенерированного кода (injSize). В Таблице \ref{patch}, в столбце <<Смещение до поля>> функция s(x) обзначает размер соответствующей структуры, и далее прибавлено смещение конкретного поля внутри структуры. Elf\_hdr.e\_shoff вместе с тем -- значение этого поля после патчевания этого поля в первой строке таблицы. Поля указаны в том порядке, в котором они должны быть отредактированы, так как есть зависимость значений некоторых полей от другого поля.
- Все смещения и значения могут быть с легкостью вычислену вручную, однако это не самый эффективный подход, так высока вероятность ошибки в таком случае. С целью вычисления полей была доработана программа rtl64topas.cpp. Она не только разбивает файл на 2 набора строк (начало файла до секции кода включительно и конец файла), но и также вычисляет смещения и значения полей, после чего с помощью функций \verb|OutputCodePutInt32(offset, value)| значения записываются в итоговый набор опкодов инструкций.
- \begin{lstlisting}[caption={Редактирование полей ELF файла}, label={elfPatch}]
- const EndStubSize=$8f7;
- ElfHdrShoff_val0=$488;
- TextPhdrFilesz_val0=$349;
- OffsElfHdrShoff=$28;
- OffsTextPHdrFilesz=$98;
- ...
- InjSize:=OutputCodeDataSize-EndStubSize-PECodeStart;
- {ElfHdr.e_shoff, 8b}
- OutputCodePutInt32(OffsElfHdrShoff + $1, ElfHdrShoff_val0 + InjSize);
- OutputCodePutInt32(OffsElfHdrShoff + $1 + $4, 0);
- {TextPhdr.p_filesz, 4b}
- OutputCodePutInt32(OffsTextPHdrFilesz + $1, TextPhdrFilesz_val0 + InjSize);
- \end{lstlisting}
- Так как быйты представлены в формате Little Endian (по страшему адресу -- старшие байты) и с допущением, что генерируемый промежуточный код имеет 32-х разрядный формат, а значит не может превзойти или добраться по размеру до x64 кода, старшие 4 байта (расположенные по старшему адресу) 8-байтных полей могут быть обнулены. Пример такого редактирования поля -- в Листинге \ref{elfPatch}. Везде добавляется один байт из-за устройства процедуры \texttt{OutputCodePutInt32}, которая редактирует байт по текущему адресу, а не со следующего байта.
- Еще одной доработкой BTPC является двукратное увеличение значения \texttt{Value} (данных для операции, представленной предыдущим значнием) там, где Value является {\it смещением}. Например, в операциях \verb|subq Value, %rsp| (формально, Value в подобной инструкции не является смещением, однако вычитание у BeRo реализовано прибавлением отрицательного значения, а сами инструкции вычитания используются для аллокации переменных в стеке, в связи с чем значение должно быть увеличено в 2 раза) или же \verb|movq Value(%rsp), %rax|.
- На этом основную часть портирования можно считать завершенной.
- Данный проект в его текщем состоянии доступен из репоизтория на Github \\(github.com/avbelyaev/course-compilers).
- \bigskip
- {
- \section{ТЕСТИРОВАНИЕ И ОТЛАДКА}
- }
- Далее следует отладка в стиле TDD разработки (Test Driven Development), т.е. необходимо получить эталонное поведение, сравнивая поведение оригинала и новой релаизации на тестах. В случае <<падения>> необходимо дизассемблировать и изучить причину подробнее.
- Далее следует простой пример одной итерации такого TDD подхода к отладке. На Рисунке \ref{diff}, в левом столбце содержится код, сгенерированный новым компилятором по программе из Листинга \ref{pascal}, в правом столбце -- код, порожденный исходным компилятором.
- \begin{lstlisting}[caption={Тестирование работы функции, обращающейся к 2м функциям RTL}, label={pascal}]
- program RTLFucnTest;
- var x:integer;
- procedure simpleProc(s:integer);
- begin
- Write('tst');
- WriteLn(s+3);
- end;
- begin
- x:=5;
- simpleProc(x);
- end.
- \end{lstlisting}
- Как видно из результата сравнения дизассемблирования, ассемблерная функция (метка) порождается на том же месте, что и процедура в верхнеуровневом коде. Далее пушится значение аргумента и следует переход на метку функции. Можно также заметить, что печать строки это на самом деле печать одного символа $n=strlen$ раз (функцией RTLWriteChar по смещению 0x8), а вся арифметика происходит прямо в стеке. Далее следуют еще 2 RTL функции -- RTLWriteInteger и RTLWriteLn по смещениям 0x10 и 0x18 соответственно. Заканчивается программа переходом на функцию RTLHalt.
- \begin{figure}[h!]
- \caption{Сравнение порожденных частей}
- \centering
- \includegraphics[width=0.95\textwidth]{diff}
- \label{diff}
- \end{figure}
- В данном случае имеется проблема с аллокацией места в стеке под переменные -- инструкция \verb|sub #0x4, %rsp| сдвигает стек на 4 байта, что приводит к коллизии с остальными 8-байтовыми значениями в нем. Если теперь изменить функцию, порождающую эту инструкцию, то оба файла отработают корректно. Первый -- под Linux x64, второй -- под Windows 32.
- Если же проблема на уровне заглушки и требует пересборки библиотеки, то необходимо ортедактировать код, скомпилировать и слинковать его, затем преобразовать в набор строк, вставить выходной результат программы rtl64topas на соответствующее место в BTPC. Затем вновь идут итерации тестирования.
- Таким образом мы получаем полностью работоспособный компилятор, который порождает ELF файлы для использования под Linux. Следующим этапом является раскрутка компилятора. Необходимо подать на вход компилятору под Windows исходный код компилятора под Linux. На выходе получится компилятор, работающий под Windows, но генерирующий x64 инструкции под Linux. После подачи ему на вход себя же, полуим полностью работоспособный компилятор Pascal, работающий под Linux.
- В случае неудачи, необходимо возврщаться к итеративному подходу для локлизации ошибки и ее исправления. После чего вновь вернуться к раскрутке. Это может быть давольно долгим и трудоемким процессом. Отдельно стоит отметить препятствующие этому факторы.
- {
- \subsection{Особенности исходной реализации}
- }
- Чтобы избежать ошибок, которые значительно замедляли данную работу, стоит отметить имеющиеся проблемы, которые появлялись по мере работы над портированием:
- \begin{itemize}
- \item В RTL нет ни одного комментария на несколько сотен строк ассемблерного кода притом, что в некоторых функциях используются не самые очевидные решения.
- \item В RTL данные хранятся внутри кода, в нарушение практик по безопасному программированию, что еще и приводит к невозможности отладить работу (можно рассматривать это, как своеобразный прием антиотладки -- техники злоумышленников по скрытию реальных действий программы), дизассемблировав файл, так как данные принимаются за инструкции.
- \item Отсутствуют фреймы функций, что делает невозможным отладку в некоторых средах.
- \item Содержимое регистров, которое можно принять за <<мусор>> после некоторых операций, спустя некоторое количество инструкций будет переиспользовано и дальнейший код ожидает увидеть там определенное значение. В других местах мусор перезаписывается.
- \item Наличие ошибок в коде, одна из которых просущестововала 10 лет.
- \item В BTPC присутствует несколько десятков комментариев (многие из которых дублируют названия функций, в которых эти комментраии указаны) на почти 3000 строк кода собственного множества Pascal'а. Какая-либо иная документаия отсутствует.
- \item Для формирования <<заглушки>> используется устревшее ПО, найти которое становится все сложнее.
- \item Обилие никак не аннотированных <<магических констант>> в коде, как часть <<worst practice>>.
- \end{itemize}
- Описанные выше проблемы действительно способны вызвать значительные затруднения в работе с legacy кодом даже для автора кода спустя большое количество времени, так что в новом проекте была предпринята попытка избавиться хотя бы от части проблем.
- \clearpage
- {
- \section{ЗАКЛЮЧЕНИЕ}
- }
- В результате данной работы была произведена попытка портирования программы, написанной более 10 лет назад, которая {\it отчасти} увенчалась успехом. В ходе работы было изучено внутренне представление файлов разных форматов для разных операционных систем, был изучен процесс компиляции и сборки выходного файла, а также был установлен контроль над всем данным процессом, что привело к положительному результату и ожидаемым последствиям. Былы изучена генерация опокдов из инструкций, а затем и упаковка опкодов, а также разные подходы к компоновке выходных файлов.
- Во время работы было встречено и преодолено множество проблем, характерных для работ подобного рода.
- Данная работы может быть названа выполненной лишь отчасти, так как вероятно в коде еще множество ошибок и однажды кто-нибудь с ними столкнется, после чего придется вновь углубляться в изучение неочевидного кода, так как большая часть кода нового компилятора (вся фаза анализа) досталась ему по наследству от старого.
- \newpage
- \begin{thebibliography}{00} % 00 влияет на выравнивание номеров записей в списке литературы.
- \bibitem{tour} Peering Inside the PE: A Tour of the Win32 Portable Executable File Format [Электронный ресурс] \newblock --- Режим доступа: https://msdn.microsoft.com/en-us/library/ms809762.aspx
- \bibitem{elf} Executable and Linkable Format (ELF) Specification [Электронный ресурс] \newblock --- Режим доступа: http://www.skyfree.org/linux/references/ELF\_Format.pdf
- \bibitem{makarov} А. Макаров, Р. Насыров. Написание собственной операционной системы, ``x64b'', учебное пособие \newblock --- Издательство: самоиздат., 2011
- \bibitem{zubkov}С. Зубков, Assembler для DOS, Windows и Unix \newblock ---МСК: ДМК Пресс, 2015. \newblock --- 206 с.
- \bibitem{pirates} PE (Portable Executable): На странных берегах [Электронный ресурс] \newblock --- Режим доступа: https://habrahabr.ru/post/266831/
- \end{thebibliography}
- %\addcontentsline{toc}{section}{Список литературы}
- \end{document}
Advertisement
Add Comment
Please, Sign In to add comment