Перейти к содержимому

Как создать файл exe для запуска программы

  • автор:

Как создать единый exe-файл без app.dll.config?

Несмотря на выбор «Создать отдельный файл» в параметрах профиля публикации, в результате всё равно VS рядом с appName.exe создаёт файлы appName.dll.config и appName.pdb . И если pdb-файл, как я понимаю, нужен только для отладки и может быть спокойно удалён, то без config-файла приложение работать не будет. appName.dll.config был создан автоматически и хранит информацию с первоначальными настройками, установленными мной в Properties.Settings . Так как всё-таки перенести config в exe? Или изменить способ хранения настроек, чтобы не требовались дополнительные файлы вовсе? Параметры профиля Результат

Отслеживать
задан 3 мая 2023 в 5:21
23 5 5 бронзовых знаков

Никак и незачем: ru.stackoverflow.com/questions/823677 Перепишите программу, чтобы она не использовала XML-конфигурацию, тогда файл можно будет удалить. Кстати вы проверяли, что она работает для self-contained публикации (т.е. если поменять значения в .config, они подхватываются в программе)? Этот тип конфигурации устаревший, вполне возможно, что при такой схеме программа его не увидит.

3 мая 2023 в 5:27

@MSDN.WhiteKnight Не совсем вас понимаю. Как не использовать XML-конфигурацию, если она создаётся автоматически? Вовсе не использовать Properties.Settings ? Я вообще плохо понимаю зачем нужен этот файл, ведь в дополнение к нему в папке AppData создаётся файл user.config с текущими настройками, изменяющимися во время работы приложения. А изменение appName.dll.config ни на что не влияет. Если XML-конфигурация устарела, то чем её можно заменить?

4 мая 2023 в 5:14

appName.dll.config нужен для свойств, у которых задана область приложения, а не пользователя. Если у вас таких свойств нет, этот файл создается впустую, и его можно удалить (ну, точнее, не передавать его пользователю вместе с приложением). Он создается автоматически только из-за того, что этого требует шаблон.

4 мая 2023 в 7:39

Заменить можно на Microsoft.Extensions.Configuration, но он главным образом для параметров, которые не изменяются пользователем. Если вы используете параметры области пользователя, скорее всего, лучше оставить XML-конфигурацию (без appName.dll.config, но с использованием конфига из AppData, он будет работать).

4 мая 2023 в 7:43

Это нормально, но должен быть способ избавиться от appName.dll.config . Без него программа вообще не запускается, или падает с ошибкой? В журнале событий не появляется запись о необработанном исключении? «Microsoft.Extensions.Configuration, как я понимаю, он работает только на чтение» — Да, только на чтение.

Превращаем код на Python в исполняемый EXE-файл

Логотип компании Tproger

Как скомпилировать Python-код в файл EXE, чтобы защитить свой код или наглядно показать работу вашей программы.

Обычно код на Питон запускают двумя способами: через терминал или через текстовый редактор / IDE.

При этом стоит отметить, что файлик Python — это всего лишь скрипт, который сохранён в расширении .py. Да, это очевидно, но в этом и кроется проблема: иногда нужно скрыть исходный код, а поделиться программкой (в виде альфа-версии, например) очень хочется.

Одно из решений, которое закрывают боль с исходным кодом — это конвертация файла Python в исполняемый exe-файлик.

В этой статье мы научимся создавать «экзешники» при помощи двух библиотек: Pyinstaller and auto-py-to-exe.

Перед тем, как продолжить чтение статьи, скачайте этот репозиторий. Он здорово поможет вам понять процесс, если вы будете повторять все шаги вместе с нами.

Почему возникает потребность конвертировать Python-код в EXE

  1. Код могут украсть. Об этом мы уже говорили выше. Куда проще достать исходный код непосредственно из py-файла, чем из скомпилированного приложение. Py-файл достаточно просто открыть!
  2. Хочется поделиться кодом с другими юзерами. Дело не только в том, что ваш код могут украсть. Зачастую код содержит зависимости, которыми должен владеть другой юзер перед запуском вашего Python-кода. Но что, если он вообще ничего не смыслит в программировании? Не заставишь ведь его сперва выучить программирование, чтобы оценить ваше новое суперприложение. Проще создать исполняемый файл.
  3. Исполняемый файл можно запускать автоматически, по расписанию. Этот пункт актуален, если вы решили написать утилиту для вашего ПК, и эта утилита должна запускаться либо в определенное время, либо вместе с запуском ОС.

Конвертация кода Python в файл .exe

Для начала создадим скрипт на Python. В качестве примера мы используем открытый код бронирования жилья AitBnb в Нью-Йорке. Если у вас уже есть готовый код, можете сразу использовать его.

import pandas as pd from pandas.api.types import CategoricalDtype def perform_validation(filename:str): """ A function to validate inputs for NYC Airbnb Open data. """ path_to_data = "../.." data = pd.read_csv(f"") # Convert neighbourhood_group to type category neighbourhood_group_to_cat = CategoricalDtype( categories=["Manhattan", "Brooklyn", "Queens", "Bronx", "Staten Island"], ordered=False ) data["neighbourhood_group"] = data["neighbourhood_group"].astype(neighbourhood_group_to_cat) # Convert room_type to type category room_type_to_cat = CategoricalDtype( categories=["Entire home/apt", "Private room", "Shared room"], ordered=False ) data["room_type"] = data["room_type"].astype(room_type_to_cat) # Convert last_review to datetime data["last_review"] = pd.to_datetime(data["last_review"]) # Minimum nights a person can stay is one night assert data["minimum_nights"].min() >= 1 # Minimum number of reviews is 0 assert data["number_of_reviews"].min() >= 0 # Minimum number of reviews per month assert data["reviews_per_month"].min() >= 0.00 # Minimum amount of listings per host assert data["calculated_host_listings_count"].min() >= 1 # Number of days when listing is available for books # Could be 0 if tennant has long term booking assert data["availability_365"].min() >= 0 # Save validated data data.to_csv("validated_ab_nyc_2019.csv", index=False) if __name__ == "__main__": # User inputs filename filename = input("Enter filename: ") # Ensure it's a string if not filename.isalpha(): filename = str(filename) # Automated validation perform_validation(filename) 

Вы не забыли скачать библиотеки по ссылке по введении? Сделайте это сейчас.

Вот структура файлов на GitHub:

# Directory structure | AB_NYC_2019.csv | +---auto_py_to_exe | | requirements.txt | | validation.py +---pyinstaller | requirements.txt | validation.py 

Каждая из директорий должна обладать своей виртуальной средой. Убедитесь, что среды развёрнуты и в директории pyinstaller, и в auto-py-to-exe.

Сохраните ваш код и скопируйте его в обе директории.

Что делать в Pyinstaller

Ещё раз убедитесь, что виртуальная среда развёрнута. Да, мы настаиваем. Проверьте, установлены ли все зависимости между библиотеками и кодом.

Всё в порядке? Запускайте команду:

pyinstaller --onefile validation.py 

Процесс должен запуститься, и в конце консоль сообщит: «Completed successfully». Значит, всё прошло успешно. ?

Превращаем код на Python в исполняемый EXE-файл 1

В папке с библиотекой и кодом должны появиться две новые директории под названиями build и dist. В папке dist вы найдёте заветный exe-файл. Запустите его и проверьте работу приложения.

Что делать в Auto-py-to-exe

Также скиньте файлик с кодом в папку с библиотекой и проверьте зависимости между кодом и библиотекой.

auto-py-to-exe 

Команда запустит графический интерфейс.

Превращаем код на Python в исполняемый EXE-файл 2

Запускаем компилятор Auto-py-to-exe // DataCamp

  1. В строчке «Script Location» нажмите «Browse» и выберите py-скрипт.
  2. В строчке «Onefile» выберите, скомпилируется код в единый файл или в папку. В папке будет содержаться exe-файл и все зависимости кода.
  3. В строчке «Console Window» выберите настройки отображения exe-приложения, когда оно будет запущено. Если выбрать «Console Based», запустится и программа, и консоль, в которой будут показаны процессы. Опция «Window Based» скрывает консоль.
  4. Нажмите кнопку «Convert .py to .exe». Готово!

Запускаем exe-файл

В обоих случаях, после компиляции и запуска exe-файла, тот запустит консоль и попросит вас ввести имя файла. Придумайте его и укажите расширение, в котором всё должно работать. К примеру, Tproger-is-the-best-it-media-in-the-world.csv . Тогда консоль закроется, а в папке появится заветный файлик с расширением .csv.

И всё — вы великолепны!

Как создать файл exe для запуска программы

HomeLisp (по крайней мере в предлагаемой версии) не способен компилировать Лисп-код в команды микропроцессора. Поэтому для создания автономных исполняемых файлов применяется технология, которую можно условно назвать псевдо-EXE.

Суть этой технологии состоит в следующем.

В поставку HomeLisp входит исполняемый файл-заглушка, в составе которого содержится ядро HomeLisp. Объем этого файла составляет около двухсот килобайт. При создании EXE-файла сначала делается копия файла-заглушки с именем, которое задает разработчик. Все дальнейшие манипуляции выполняются с созданной копией, которую далее будем называть целевым файлом.

Предположим, что разработчик написал и загрузил ряд функций, которые в совокупности составляют приложение. При создании EXE-файла, все выбранные разработчиком функции приписываются в «хвост» целевого файла. Кроме этих функций, в «хвост» целевого файла записывается т.н. стартовое S-выражение, а также задаваемые пользователем настройки среды HomeLisp (размеры внутренних стеков, количество динамических объектов и т.д.)

При запуске целевого файла управление получает сначала программа инициализации ядра HomeLisp, которая затем считывает из файла все сохраненные пользователем функции и загружает их в среду Лиспа. После этого управление получает блок стартового S-выражения, и предусмотренная разработчиком программа начинает исполняться.

Рассмотрим процесс создания EXE-файла на простом практическом примере: создадим диалоговую программу, которая будет вычислять наибольший общий делитель двух целых чисел с помощью алгоритма Евклида. Пример этот не так уж бесполезен: поскольку HomeLisp работает с целыми неограниченной разрядности, написание аналогичного примера в любой расхожей среде программирования (VB,C++,Delphi) потребовала бы заметных усилий.

Функция *GCD имеет следующий вид:

(defun *gcd (x y) (cond ((eq x y) x) ((greaterp y x) (*gcd y x)) ((eq (remainder x y) 0) y) (T (*gcd y (remainder x y))))) ==> *gcd (*gcd 655 72) ==> 1 (*gcd 655 75) ==> 5 (*gcd 65536 4096) ==> 4096

Примеры вызова показывают работоспособность функции. Далее необходимо создать диалоговую оболочку для вызова этой функции. В простейшем случае эта оболочка может быть организована следующим образом:

Запрос (посредством функции ASK) первого операнда;

Запрос второго операнда;

Отображение результата с помощью функции SAY;

Напишем определяющее выражение для функции GCD-1, в котором будет реализован приведенный выше алгоритм:

(defun GCD-1 nil (prog (a1 a2) (setq a1 (str2fix (Ask "Введите первый операнд"))) (setq a2 (str2fix (Ask "Введите второй операнд"))) (TRY (say (fix2str (*gcd a1 a2))) EXCEPT (say "Ошибка!") ) ) )

Следует обратить внимание на то, что результат вызова ASK имеет тип STRING, а функция *GCD требует операндов типа FIXED. Поэтому, прежде, чем присваивать введенные пользователем значения локальным переменным, a1 и a2, эти значения следует преобразовать в тип FIXED (для чего служат вызовы функции STR2FIX).

Далее, поскольку готовится диалоговая программа, предназначенная для конечного пользователя, следует предусмотреть возможные ошибки. Именно поэтому вычисление наибольшего общего делителя сделано критическим участком кода с помощью вызова функции TRY Если в процессе вычисления произойдет сбой, программа не «упадет», а будет выдано сообщение «Ошибка!».

Если выполнить функцию GCD-1 из среды разработки, то сначала будет выведено окно запроса первого операнда:

Пользователь вводит первый операнд и нажимает кнопку OK; появляется окно запроса второго операнда:

Пользователь вводит второй операнд, нажимает кнопку OK и получает результат:

Теперь построим на основе отлаженной функции GCD-1 исполняемый файл. Для этого, находясь в среде разработки выберем пункт главного меню EXE-файл или нажмем кнопку на панели инструментов со следующей иконкой:

В обоих случаях будет открыто окно построения EXE-файла:

Чтобы создать EXE-файл, необходимо выполнить следующие действия:

Задать имя EXE-файла. Имя (и путь) будущего EXE-файла задаются в комбинированном поле ввода в левом верхнем углу формы создания EXE-файла. Имя и путь можно задать вручную, а можно нажать кнопку «. » и выбрать директорию и имя с помощью стандартного диалога Сохранить как. По умолчанию создается файл noname.exe в текущей директории.

В области ввода «Стартовое S-выражение» необходимо задать код, с которого начнется выполнение будущего приложения. Стартовое S-выражение может быть вызовом функции. Если требуется задать стартовое S-выражение, состоящее из нескольких вызовов, то эти вызовы следует «заключить» в PROG-конструкцию.

Перечисленные действия являются минимально-необходимыми. Кроме того, дополнительно пользователь может выполнить следующее:

В списке, озаглавленном «Функции, константы, переменные», сбросить флажки, стоящие против объектов, наличие которых для выполнения приложения не является необходимым. Это мероприятие несколько повысит скорость выполнения приложения;

Изменить значения настроечных параметров в списке, расположенном в правом верхнем углу. Последовательность действий при замене значений параметров будет описана ниже;

Сбросить флажок «Отображать диспетчерскую форму при старте». Если этот флажок оставить поднятым, то при запуске приложения будет отображаться специальная диспетчерская форма. Этой форме можно задать заголовок (в соответствующем поле ввода). Использование диспетчерской формы удобно при отладке.

Установить или сбросить флажок «Сообщать о завершении». Если этот флажок установлен, то перед завершением приложения будет выдано сообщение.

Окно создания EXE-файла с минимально-необходимым набором параметров может иметь вид:

Далее необходимо нажать кнопку с изображением зеленой галочки. Если заказанный EXE-файл уже существует, HomeLisp предолжит подтвердить перезапись файла или задать новое имя. После чего, при успешном завершении, выдается сообщение:

Можно убедиться, что в текущей директории пявился EXE-файл gcd-1.exe, при запуске которого происходит описанный выше диалог с пользователем.

Если же при создании EXE-файла поднять флажок «Отображать диспетчерскую форму при старте» и задать заголовок формы:

то при исполнении полученного EXE-файла, в правом верхнем углу экрана отображается диспетчерская форма следующего вида:

При выборе пункта меню «Пуск» (или нажатии соответствующей кнопки, расположенной ниже), начинает выполняться приложение. После завершения приложения, диспетчерская форма остается на экране, что позволяет запускать приложение многократно. Для окончательного завершения приложения нужно выбрать пункт меню «Выход» (или нажать кнопку, расположенную ниже).

Если в процессе выполнения приложения с диспетчерской формой возникает ошибка, то информация об ошибке выводится в область вывода, которая в нормальном состоянии скрыта. Например, если сделать стартовым S-выражением вызов несуществующей функции (unknown) , то при выполнении возникнет картинка, приведенная ниже:

Использование диспетчерской формы позволяет принудительно остановить выполнение приложения. Для этого служит пункт меню «Стоп» и кнопка, расположенная ниже (и пункт меню, и кнопка активны только в процессе выполнения приложения). Если в качестве стартового выражения задать, например, вычисление факториала достаточно большого числа, то можно успеть увидеть пункт меню и кнопку останова:

Если в процессе выполнения нажать «Стоп», то выполнение программы прерывается:

Пункт главного меню «Окна» диспетчерской формы (и соответствующая командная кнопка) будут активны в случае, когда в процессе выполнения приложения создано хотя бы одно графическое окно. При выборе пункта «Окна» (или нажатии на кнопку, расположенную ниже), вызывается менеджер графических окон.

Режим построения EXE-файлов с диспетчерской формой предназначен для облегчения отладки приложений. Если не использовать диспетчерскую форму, то управлять графическими окнами должен будет сам разработчик, а в случае «зависания» приложения для его снятия придется использовать диспетчер задач ОС.

При необходимости изменить значение какого-либо настроечного параметра (размер стека, число лексем и т.д.) необходимо щелкнуть мышью по нужно строке списка «Параметры». Числовое значение соответствующего параметра будет перенесено в область ввода, расположенную под списком:

Далее следует исправить значение параметра и нажать кнопку с изображением стрелки, направленной вверх. Значение параметра будет исправлено:

В качестве второго примера создания EXE-файла, рассмотрим прежнюю задачу (вычисление НОД), но с оконным графическим интерфесом. Для этого создадим диалог (как описано здесь), на форме которого разместим три метки, три поля ввода и две кнопки. Назначим одной из кнопок процедуру-обработчик, в котором вычислим НОД чисел, введенных в первое и второе поля ввода и занесение вычисленного НОД в поле результата. Обработчик нажатия второй кнопки будет закрывать и уничтожать диалог. Форма диалога с элементами управления может иметь вид, приведенный ниже:

Программный код, сгенерированный дизайнером, может иметь такой вид:

(prog nil (dlgCreate '_Dlg_ 419 233 "Наибольший общий делитель:") (dlgAddControl '_Dlg_ '_LBL_1 _LABEL 14 15 100 21 '("Tahoma" 14,25 1 0 0) "Первое:" 0 &H80000012 &H8000000F) (dlgAddControl '_Dlg_ '_TXT_1 _TEXT 155 13 248 31 '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005) (dlgAddControl '_Dlg_ '_LBL_2 _LABEL 12 59 133 26 '("Tahoma" 14,25 1 0 0) "Второе:" 0 &H80000012 &H8000000F) (dlgAddControl '_Dlg_ '_TXT_2 _TEXT 156 56 247 31 '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005) (dlgAddControl '_Dlg_ '_LBL_3 _LABEL 13 101 127 27 '("Tahoma" 14,25 1 0 0) "Н.О.Д." 0 &HFF &H8000000F) (dlgAddControl '_Dlg_ '_TXT_3 _TEXT 157 98 247 31 '("Tahoma" 14 1 0 0) "" 0 &HFF &H80000005) (dlgAddControl '_Dlg_ '_BUT_1 _BUTTON 24 148 180 50 '("Tahoma" 8,25 1 0 0) "Вычислить") (dlgPutPicture '_BUT_1 7) (dlgAddControl '_Dlg_ '_BUT_2 _BUTTON 213 149 180 50 '("Tahoma" 8,25 1 0 0) "Закрыть") (dlgPutPicture '_BUT_2 36) // // Обработчик события CLICK для кнопки _BUT_2 // (defun _BUT_2_Click Nil (prog Nil (dlgHide '_DLG_) (dlgDestroy '_DLG_) (gc) ) ) // // Назначение процедуры-события _BUT_2_Click контролу _BUT_2 // (dlgSetEvent '_BUT_2 '_BUT_2_Click ) // // Обработчик события CLICK для кнопки _BUT_1 // (defun _BUT_1_Click Nil (prog (a1 a2) (setq a1 (str2fix (dlgGetText '_TXT_1))) (setq a2 (str2fix (dlgGetText '_TXT_2))) (TRY (dlgPutText '_TXT_3 (fix2str (*gcd a1 a2))) EXCEPT (dlgPutText '_TXT_3 "Ошибка. ") ) ) ) // // Назначение процедуры-события _BUT_1_Click контролу _BUT_1 // (dlgSetEvent '_BUT_1 '_BUT_1_Click ) // // Отображение диалога _Dlg_ // (dlgShow '_Dlg_) )

Для того, чтобы построить EXE-файл, нужно загрузть в среду Лиспа функцию *GCD и написать стартовое выражение для отображения диалога. Проще всего «погрузить» приведенную выше PROG-конструкцию в функцию MAIN без параметров. Тогда стартовым S-выражением для запуска EXE-файла будет вызов (main). Итак, если загрузить в среду Лиспа следующие функции:

// // Поиск наибольшего общего делителя // (defun *gcd (x y) (cond ((eq x y) x) ((greaterp y x) (*gcd y x)) ((eq (remainder x y) 0) y) (T (*gcd y (remainder x y))))) // // Программа отображения диалога _Dlg_ // (defun main nil (prog nil (dlgCreate '_Dlg_ 419 233 "Наибольший общий делитель:") (dlgAddControl '_Dlg_ '_LBL_1 _LABEL 14 15 100 21 '("Tahoma" 14,25 1 0 0) "Первое:" 0 &H80000012 &H8000000F) (dlgAddControl '_Dlg_ '_TXT_1 _TEXT 155 13 248 31 '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005) (dlgAddControl '_Dlg_ '_LBL_2 _LABEL 12 59 133 26 '("Tahoma" 14,25 1 0 0) "Второе:" 0 &H80000012 &H8000000F) (dlgAddControl '_Dlg_ '_TXT_2 _TEXT 156 56 247 31 '("Tahoma" 14 1 0 0) "" 0 &H80000008 &H80000005) (dlgAddControl '_Dlg_ '_LBL_3 _LABEL 13 101 127 27 '("Tahoma" 14,25 1 0 0) "Н.О.Д." 0 &HFF &H8000000F) (dlgAddControl '_Dlg_ '_TXT_3 _TEXT 157 98 247 31 '("Tahoma" 14 1 0 0) "" 0 &HFF &H80000005) (dlgAddControl '_Dlg_ '_BUT_1 _BUTTON 24 148 180 50 '("Tahoma" 8,25 1 0 0) "Вычислить") (dlgPutPicture '_BUT_1 7) (dlgAddControl '_Dlg_ '_BUT_2 _BUTTON 213 149 180 50 '("Tahoma" 8,25 1 0 0) "Закрыть") (dlgPutPicture '_BUT_2 36) // // Обработчик события CLICK для кнопки _BUT_2 // (defun _BUT_2_Click Nil (prog Nil (dlgHide '_DLG_) (dlgDestroy '_DLG_) (gc) ) ) // // Назначение процедуры-события _BUT_2_Click контролу _BUT_2 // (dlgSetEvent '_BUT_2 '_BUT_2_Click ) // // Обработчик события CLICK для кнопки _BUT_1 // (defun _BUT_1_Click Nil (prog (a1 a2) (setq a1 (str2fix (dlgGetText '_TXT_1))) (setq a2 (str2fix (dlgGetText '_TXT_2))) (TRY (dlgPutText '_TXT_3 (fix2str (*gcd a1 a2))) EXCEPT (dlgPutText '_TXT_3 "Ошибка. ") ) ) ) // // Назначение процедуры-события _BUT_1_Click контролу _BUT_1 // (dlgSetEvent '_BUT_1 '_BUT_1_Click ) // // Отображение диалога _Dlg_ // (dlgShow '_Dlg_) ) )

а затем построить EXE-файл gcd-2.exe, задав следующие параметры:

то получится вполне работоспособный EXE-файл. Его можно запустить, и убедиться, что НОД считается верно:

Создаем EXE

Самоизоляция это отличное время приступить к тому, что требует много времени и сил. Поэтому я решил заняться тем, чем всегда хотел — написать свой компилятор.

Сейчас он способен собрать Hello World, но в этой статье я хочу рассказать не про парсинг и внутреннее устройство компилятора, а про такую важную часть как побайтовая сборка exe файла.

Начало

Хотите спойлер? Наша программа будет занимать 2048 байт.

Обычно работа с exe файлами заключается в изучении или модификации их структуры. Сами же исполняемые файлы при этом формируют компиляторы, и этот процесс кажется немного магическим для разработчиков.

Но сейчас мы с вами попробуем это исправить!

Для сборки нашей программы нам потребуется любой HEX редактор (лично я использовал HxD).

Для старта возьмем псевдокод:

Исходный код

func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll'] func ExitProcess(u32 code) ['kernel32.dll'] func main()

Первые две строки указывают на функции импортируемые из библиотек WinAPI. Функция MessageBoxA выводит диалоговое окно с нашим текстом, а ExitProcess сообщает системе о завершении программы.
Рассматривать отдельно функцию main нет смысла, так как в ней используются функции, описанные выше.

DOS Header

Для начала нам нужно сформировать корректный DOS Header, это заголовок для DOS программ и влиять на запуск exe под Windows не должен.

Более-менее важные поля я отметил, остальные заполнены нулями.

Стуктура IMAGE_DOS_HEADER

Struct IMAGE_DOS_HEADER < u16 e_magic // 0x5A4D "MZ" u16 e_cblp // 0x0080 128 u16 e_cp // 0x0001 1 u16 e_crlc u16 e_cparhdr // 0x0004 4 u16 e_minalloc // 0x0010 16 u16 e_maxalloc // 0xFFFF 65535 u16 e_ss u16 e_sp // 0x0140 320 u16 e_csum u16 e_ip u16 e_cs u16 e_lfarlc // 0x0040 64 u16 e_ovno u16[4] e_res u16 e_oemid u16 e_oeminfo u16[10] e_res2 u32 e_lfanew // 0x0080 128 >

Самое главное, что этот заголовок содержит поле e_magic означающее, что это исполняемый файл, и e_lfanew — указывающее на смещение PE-заголовка от начала файла (в нашем файле это смещение равно 0x80 = 128 байт).

Отлично, теперь, когда нам известна структура заголовка DOS Header запишем ее в наш файл.

(1) RAW DOS Header (Offset 0x00000000)

4D 5A 80 00 01 00 00 00 04 00 10 00 FF FF 00 00 40 01 00 00 00 00 00 00 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00 

Сначала я использовал левую колонку как на скриншоте для указания смещения внутри файла, но тогда неудобно копировать исходный текст, приходится обрезать каждую строку.

Поэтому для удобства в первой скобке каждого блока указан порядок добавления в файл, а в последней смещение в файле (Offset) по которому должен располагаться данный блок.

Например, первый блок мы вставляем по смещению 0x00000000, и он займет 64 байта (0x40 в 16-ричной системе), следующий блок мы будем вставлять уже по этому смещению 0x00000040 и т.д.

Готово, первые 64 байта записали. Теперь нужно добавить еще 64, это так называемый DOS Stub (Заглушка). Во время запуска из-под DOS, она должна уведомить пользователя что программа не предназначена для работы в этом режиме.

Но в целом, это маленькая программа под DOS которая выводит строку и выходит из программы.
Запишем наш Stub в файл и рассмотрим его детальнее.

(2) RAW DOS Stub (Offset 0x00000040)

0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 6D 6F 64 65 2E 0D 0A 24 00 00 00 00 00 00 00 00 

А теперь этот же код, но уже в дизассемблированном виде

Asm DOS Stub

0000 push cs ; Запоминаем Code Segment(CS) (где мы находимся в памяти) 0001 pop ds ; Указываем что Data Segment(DS) = CS 0002 mov dx, 0x0E ; Указываем адрес начала строки DS+DX, которая будет выводиться до символа $(Конец строки) 0005 mov ah, 0x09 ; Номер инструкции (Вывод строки) 0007 int 0x21 ; Вызов системного прерывания 0x21 0009 mov ax, 0x4C01 ; Номер инструкции 0x4C (Выход из программы) ; Код выхода из программы 0x01 (Неудача) 000c int 0x21 ; Вызов системного прерывания 0x21 000e "This program cannot be run in DOS mode.\x0D\x0A$" ; Выводимая строка 

Это работает так: сначала заглушка выводит строку о том, что программа не может быть запущена, а затем выходит из программы с кодом 1. Что отличается от нормального завершения (Код 0).

Код заглушки может немного отличатся (от компилятора к компилятору) я сравнивал gcc и delphi, но общий смысл одинаковый.

А еще забавно, что строка заглушки заканчивается как \x0D\x0D\x0A$. Скорее всего причина такого поведения в том, что c++ по умолчанию открывает файл в текстовом режиме. В результате символ \x0A заменяется на последовательность \x0D\x0A. В результате получаем 3 байта: 2 байта возврата каретки Carriage Return (0x0D) что бессмысленно, и 1 на перевод строки Line Feed (0x0A). В бинарном режиме записи (std::ios::binary) такой подмены не происходит.

Для проверки корректности записи значений я буду использовать Far с плагином ImpEx:

NT Header

Спустя 128 (0x80) байт мы добрались до NT заголовка (IMAGE_NT_HEADERS64), который содержит в себе и PE заголовок (IMAGE_OPTIONAL_HEADER64). Несмотря на название IMAGE_OPTIONAL_HEADER64 является обязательным, но различным для архитектур x64 и x86.

Структура IMAGE_NT_HEADERS64

Struct IMAGE_NT_HEADERS64 < u32 Signature // 0x4550 "PE" Struct IMAGE_FILE_HEADER < u16 Machine // 0x8664 архитектура x86-64 u16 NumberOfSections // 0x03 Количество секций в файле u32 TimeDateStamp // Дата создания файла u32 PointerToSymbolTable u32 NumberOfSymbols u16 SizeOfOptionalHeader // Размер IMAGE_OPTIONAL_HEADER64 (Ниже) u16 Characteristics // 0x2F >Struct IMAGE_OPTIONAL_HEADER64 < u16 Magic // 0x020B Указывает что наш заголовок для PE64 u8 MajorLinkerVersion u8 MinorLinkerVersion u32 SizeOfCode u32 SizeOfInitializedData u32 SizeOfUninitializedData u32 AddressOfEntryPoint // 0x1000 u32 BaseOfCode // 0x1000 u64 ImageBase // 0x400000 u32 SectionAlignment // 0x1000 (4096 байт) u32 FileAlignment // 0x200 u16 MajorOperatingSystemVersion // 0x05 Windows XP u16 MinorOperatingSystemVersion // 0x02 Windows XP u16 MajorImageVersion u16 MinorImageVersion u16 MajorSubsystemVersion // 0x05 Windows XP u16 MinorSubsystemVersion // 0x02 Windows XP u32 Win32VersionValue u32 SizeOfImage // 0x4000 u32 SizeOfHeaders // 0x200 (512 байт) u32 CheckSum u16 Subsystem // 0x02 (GUI) или 0x03 (Console) u16 DllCharacteristics u64 SizeOfStackReserve // 0x100000 u64 SizeOfStackCommit // 0x1000 u64 SizeOfHeapReserve // 0x100000 u64 SizeOfHeapCommit // 0x1000 u32 LoaderFlags u32 NumberOfRvaAndSizes // 0x16 Struct IMAGE_DATA_DIRECTORY [16] < u32 VirtualAddress u32 Size >> > 

Разберемся что хранится в этой структуре:

Описание IMAGE_NT_HEADERS64

Signature — Указывает на начало структуры PE заголовка

Далее идет заголовок IMAGE_FILE_HEADER общий для архитектур x86 и x64.

Machine — Указывает для какой архитектуры предназначен код в нашем случае для x64
NumberOfSections — Количество секции в файле (О секциях чуть ниже)
TimeDateStamp — Дата создания файла
SizeOfOptionalHeader — Указывает размер следующего заголовка IMAGE_OPTIONAL_HEADER64, ведь он может быть заголовком IMAGE_OPTIONAL_HEADER32.

Characteristics — Здесь мы указываем некоторые атрибуты нашего приложения, например, что оно является исполняемым (EXECUTABLE_IMAGE) и может работать более чем с 2 Гб RAM (LARGE_ADDRESS_AWARE), а также что некоторая информация была удалена (на самом деле даже не была добавлена) в файл (RELOCS_STRIPPED | LINE_NUMS_STRIPPED | LOCAL_SYMS_STRIPPED).

SizeOfCode — Размер исполняемого кода в байтах (секция .text)
SizeOfInitializedData — Размер инициализированных данных (секция .rodata)
SizeOfUninitializedData — Размер не инициализированных данных (секция .bss)
BaseOfCode — указывает на начало секции кода блок
SectionAlignment — Размер по которому нужно выровнять секции в памяти
FileAlignment — Размер по которому нужно выровнять секции внутри файла
SizeOfImage — Размер всех секций программы
SizeOfHeaders — Размер всех заголовков вместе (IMAGE_DOS_HEADER, DOS Stub, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER[IMAGE_FILE_HEADER.NumberOfSections]) выровненный по FileAlignment
Subsystem — Указывает тип нашей программы GUI или Console
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorSubsystemVersion, MinorSubsystemVersion — Говорят о том на какой системе можно запускать данный exe, и что он может поддерживать. В нашем случае мы берем значение 5.2 от Windows XP (x64).
SizeOfStackReserve — Указывает сколько приложению нужно зарезервировать памяти под стек. Этот параметр по умолчанию составляет 1 Мб, максимально можно указать 1Гб. Вроде как умные программы на Rust умеют считать необходимый размер стека, в отличии от программ на C++ где этот размер нужно править вручную.
SizeOfStackCommit — Размер по умолчанию составляет 4 Кб. Как должен работать данный параметр пока не разобрался.
SizeOfHeapReserve — Указывает сколько резервировать памяти под кучу. Равен 1 Мб по умолчанию.
SizeOfHeapCommit — Размер по умолчанию равен 4 Кб. Подозреваю что работает аналогично SizeOfStackCommit, то есть пока неизвестно как.

IMAGE_DATA_DIRECTORY — массив записей о каталогах. В теории его можно уменьшить, сэкономив пару байт, но вроде как все описывают все 16 полей даже если они не нужны. А теперь чуть подробнее.

У каждого каталога есть свой номер, который описывает, где хранится его содержимое. Пример:
Export(0) — Содержит ссылку на сегмент который хранит экспортируемые функции. Для нас это было бы актуально если бы мы создавали DLL. Как это примерно должно работать можно посмотреть на примере следующего каталога.

Import(1) — Этот каталог указывает на сегмент с импортируемыми функциями из других DLL. В нашем случае значения VirtualAddress = 0x3000 и Size = 0xB8. Это единственный каталог, который мы опишем.

Resource(2) — Каталог с ресурсами программы (Изображения, Текст, Файлы и т.д.)
Значения других каталогов можно посмотреть в документации.

Теперь, когда мы посмотрели из чего состоит NT-заголовок, запишем и его в файл по аналогии с остальными по адресу 0x80.

(3) RAW NT-Header (Offset 0x00000080)

50 45 00 00 64 86 03 00 F4 70 E8 5E 00 00 00 00 00 00 00 00 F0 00 2F 00 0B 02 00 00 3D 00 00 00 13 00 00 00 00 00 00 00 00 10 00 00 00 10 00 00 00 00 40 00 00 00 00 00 00 10 00 00 00 02 00 00 05 00 02 00 00 00 00 00 05 00 02 00 00 00 00 00 00 40 00 00 00 02 00 00 00 00 00 00 02 00 00 00 00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 30 00 00 B8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

В результате получаем вот такой вид IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER64 и IMAGE_DATA_DIRECTORY заголовков:

Далее описываем все секции нашего приложения согласно структуре IMAGE_SECTION_HEADER

Структура IMAGE_SECTION_HEADER

Struct IMAGE_SECTION_HEADER

Описание IMAGE_SECTION_HEADER

Name — имя секции из 8 байт, может быть любым
VirtualSize — сколько байт копировать из файла в память
VirtualAddress — адрес секции в памяти выровненный по SectionAlignment
SizeOfRawData — размер сырых данных выровненных по FileAlignment
PointerToRawData — адрес секции в файле выровненный по FileAlignment
Characteristics — Указывает какие данные хранит секция (Код, инициализированные или нет данные, для чтения, для записи, для исполнения и др.)

В нашем случае у нaс будет 3 секции.

Почему Virtual Address (VA) начинается с 1000, а не с нуля я не знаю, но так делают все компиляторы, которые я рассматривал. В результате 1000 + 3 секции * 1000 (SectionAlignment) = 4000 что мы и записали в SizeOfImage. Это полный размер нашей программы в виртуальной памяти. Вероятно, используется для выделения места под программу в памяти.

 Name | RAW Addr | RAW Size | VA | VA Size | Attr --------+---------------+---------------+-------+---------+-------- .text | 200 | 200 | 1000 | 3D | CER .rdata | 400 | 200 | 2000 | 13 | I R .idata | 600 | 200 | 3000 | B8 | I R 

I — Initialized data, инициализированные данные
U — Uninitialized data, не инициализированные данные
C — Code, содержит исполняемый код
E — Execute, позволяет исполнять код
R — Read, позволяет читать данные из секции
W — Write, позволяет записывать данные в секцию

.text (.code) — хранит в себе исполняемый код (саму программу), атрибуты CE
.rdata (.rodata) — хранит в себе данные только для чтения, например константы, строки и т.п., атрибуты IR
.data — хранит данные которые можно читать и записывать, такие как статические или глобальные переменные. Атрибуты IRW
.bss — хранит не инициализированные данные, такие как статические или глобальные переменные. Кроме того, данная секция обычно имеет нулевой RAW размер и ненулевой VA Size, благодаря чему не занимает места в файле. Атрибуты URW
.idata — секция содержащая в себе импортируемые из других библиотек функции. Атрибуты IR

Важный момент, секции должны следовать друг за другом. При чем как в файле, так и в памяти. По крайней мере когда я менял их порядок произвольно программа переставала запускаться.

Теперь, когда нам известно какие секции будет содержать наша программа запишем их в наш файл. Тут смещение оканчивается на 8 и запись будет начинаться с середины файла.

(4) RAW Sections (Offset 0x00000188)

 2E 74 65 78 74 00 00 00 3D 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60 2E 72 64 61 74 61 00 00 13 00 00 00 00 20 00 00 00 02 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40 2E 69 64 61 74 61 00 00 B8 00 00 00 00 30 00 00 00 02 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40 

Следующий адрес для записи будет 00000200 что соответствует полю SizeOfHeaders PE-Заголовка. Если бы мы добавили еще одну секцию, а это плюс 40 байт, то наши заголовки не уложились бы в 512 (0x200) байт и пришлось бы использовать уже 512+40 = 552 байта выровненные по FileAlignment, то есть 1024 (0x400) байта. А все что останется от 0x228 (552) до адреса 0x400 нужно чем-то заполнить, лучше конечно нулями.

Взглянем как выглядит блок секций в Far:

Далее мы запишем в наш файл сами секции, но тут есть один нюанс.

Как вы могли заметить на примере SizeOfHeaders, мы не можем просто записать заголовок и перейти к записи следующего раздела. Так как что бы записать заголовок мы должны знать сколько займут все заголовки вместе. В результате нам нужно либо посчитать заранее сколько понадобиться места, либо записать пустые (нулевые) значения, а после записи всех заголовков вернуться и записать уже их реальный размер.

Поэтому программы компилируются в несколько проходов. Например секция .rdata идет после секции .text, при этом мы не можем узнать виртуальный адрес переменной в .rdata, ведь если секция .text разрастется больше чем на 0x1000 (SectionAlignment) байт, она займет адреса 0x2000 диапазона. И соответственно секция .rdata будет находиться уже не в адресе 0x2000, а в адресе 0x3000. И нам будет необходимо вернуться и пересчитать адреса всех переменных в секции .text которая идет перед .rdata.

Но в данном случае я уже все рассчитал, поэтому будем сразу записывать блоки кода.

Секция .text

Asm segment .text

0000 push rbp 0001 mov rbp, rsp 0004 sub rsp, 0x20 0008 mov rcx, 0x0 000F mov rdx, 0x402000 0016 mov r8, 0x40200D 001D mov r9, 0x40 0024 call QWORD PTR [rip + 0x203E] 002A mov rcx, 0x0 0031 call QWORD PTR [rip + 0x2061] 0037 add rsp, 0x20 003B pop rbp 003C ret 

Конкретно для этой программы первые 3 строки, ровно, как и 3 последние не обязательны.
Последние 3 даже не будут исполнены, так как выход из программы произойдет еще на второй функции call.

Но скажем так, если бы это была не функция main, а подфункция следовало бы сделать именно так.

А вот первые 3 в данном случае хоть и не обязательны, но желательны. Например, если бы мы использовали не MessageBoxA, а printf то без этих строк получили бы ошибку.

Согласно соглашению о вызовах для 64-разрядных систем MSDN, первые 4 параметра передаются в регистрах RCX, RDX, R8, R9. Если они туда помещаются и не являются, например числом с плавающей точкой. А остальные передаются через стек.

По идее если мы передаем 2 аргумента функции, то должны передать их через регистры и зарезервировать под них два места в стеке, что бы при необходимости функция могла скинуть регистры в стек. Так же мы не должны рассчитывать, что нам вернут эти регистры в исходном состоянии.

Так вот проблема функции printf заключается в том, что, если мы передаем ей всего 1 аргумент, она все равно перезапишет все 4 места в стеке, хотя вроде бы должна перезаписать только одно, по количеству аргументов.

Поэтому если не хотите, чтобы программа себя странно вела, всегда резервируйте как минимум 8 байт * 4 аргумента = 32(0x20) байт, если передаете функции хотя бы 1 аргумент.

Рассмотрим блок кода с вызовами функций

MessageBoxA(0, 'Hello World!', 'MyApp', 64) ExitProcess(0) 

Сначала мы передаем наши аргументы:

rcx = 0
rdx = абсолютный адрес строки в памяти ImageBase + Sections[«.rdata»].VirtualAddress + Смещение строки от начала секции, строка читается до нулевого байта
r8 = аналогично предыдущему
r9 = 64(0x40) MB_ICONINFORMATION, значок информации

А далее идет вызов функции MessageBoxA, с которым не все так просто. Дело в том, что компиляторы стараются использовать как можно более короткие команды. Чем меньше размер команды, тем больше таких команд влезет в кэш процессора, соответственно, будет меньше промахов кэша, подзагрузок и выше скорость работы программы. Для более подробной информации по командам и внутренней работе процессора можно обратиться к документации Intel 64 and IA-32 Architectures Software Developer’s Manuals.

Мы могли бы вызвать функцию по полному адресу, но это заняло бы как минимум (1 опкод + 8 адрес = 9 байт), а с относительным адресом команда call занимает всего 6 байт.

Давайте взглянем на эту магию поближе: rip + 0x203E, это ни что иное, как вызов функции по адресу, указанному нашим смещением.

Я подсмотрел немного вперед и узнал адреса нужных нам смещений. Для MessageBoxA это 0x3068, а для ExitProcess это 0x3098.

Пора превратить магию в науку. Каждый раз, когда опкод попадает в процессор, он высчитывает его длину и прибавляет к текущему адресу инструкции (RIP). Поэтому, когда мы используем RIP внутри инструкции, этот адрес указывает на конец текущей инструкции / начало следующей.
Для первого call смещение будет указывать на конец команды call это 002A не забываем что в памяти этот адрес будет по смещению Sections[«.text»].VirtualAddress, т.е. 0x1000. Следовательно, RIP для нашего call будет равен 102A. Нужный нам адрес для MessageBoxA находится по адресу 0x3068. Считаем 0x3068 — 0x102A = 0x203E. Для второго адреса все аналогично 0x1000 + 0x0037 = 0x1037, 0x3098 — 0x1037 = 0x2061.

Именно эти смещения мы и видели в командах ассемблера.

0024 call QWORD PTR [rip + 0x203E] 002A mov rcx, 0x0 0031 call QWORD PTR [rip + 0x2061] 0037 add rsp, 0x20 

Запишем в наш файл секцию .text, дополнив нулями до адреса 0x400:

(5) RAW .text section (Offset 0x00000200-0x00000400)

55 48 89 E5 48 83 EC 20 48 C7 C1 00 00 00 00 48 C7 C2 00 20 40 00 49 C7 C0 0D 20 40 00 49 C7 C1 40 00 00 00 FF 15 3E 20 00 00 48 C7 C1 00 00 00 00 FF 15 61 20 00 00 48 83 C4 20 5D C3 00 00 00 . 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

Хочется отметить что всего лишь 4 строки реального кода содержат весь наш код на ассемблере. А все остальное нули что бы набрать FileAlignment. Последней строкой заполненной нулями будет 0x000003F0, после идет 0x00000400, но это будет уже следующий блок. Итого в файле уже 1024 байта, наша программа весит уже целый Килобайт! Осталось совсем немного и ее можно будет запустить.

Секция .rdata

Это, пожалуй, самая простая секция. Мы просто положим сюда две строки добив нулями до 512 байт.

0400 "Hello World!\0" 040D "MyApp\0" 

(6) RAW .rdata section (Offset 0x00000400-0x00000600)

48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 4D 79 41 70 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 . 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

Секция .idata

Ну вот осталась последняя секция, которая описывает импортируемые функции из библиотек.

Первое что нас ждет новая структура IMAGE_IMPORT_DESCRIPTOR

Структура IMAGE_IMPORT_DESCRIPTOR

Struct IMAGE_IMPORT_DESCRIPTOR

Описание IMAGE_IMPORT_DESCRIPTOR

OriginalFirstThunk — Адрес указывает на список имен импортируемых функций, он же Import Name Table (INT)
Name — Адрес, указывающий на название библиотеки
FirstThunk — Адрес указывает на список адресов импортируемых функций, он же Import Address Table (IAT)

Для начала нам нужно добавить 2 импортируемых библиотеки. Напомним:

func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll'] func ExitProcess(u32 code) ['kernel32.dll'] 

(7) RAW IMAGE_IMPORT_DESCRIPTOR (Offset 0x00000600)

58 30 00 00 00 00 00 00 00 00 00 00 3C 30 00 00 68 30 00 00 88 30 00 00 00 00 00 00 00 00 00 00 48 30 00 00 98 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

У нас используется 2 библиотеки, а что бы сказать что мы закончили их перечислять. Последняя структура заполняется нулями.

 INT | Time | Forward | Name | IAT --------+--------+----------+--------+-------- 0x3058 | 0x0 | 0x0 | 0x303C | 0x3068 0x3088 | 0x0 | 0x0 | 0x3048 | 0x3098 0x0000 | 0x0 | 0x0 | 0x0000 | 0x0000 

Теперь добавим имена самих библиотек:

Имена библиотек

063С "user32.dll\0" 0648 "kernel32.dll\0" 

(8) RAW имена библиотек (Offset 0x0000063С)

 75 73 65 72 33 32 2E 64 6C 6C 00 00 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00 00 00 00 

Далее опишем библиотеку user32:

(9) RAW user32.dll (Offset 0x00000658)

 78 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00 

Поле Name первой библиотеки указывает на 0x303C если мы посмотрим чуть выше, то увидим что по адресу 0x063C находится библиотека «user32.dll\0».

Подсказка, вспомните что секция .idata соответствует смещению в файле 0x0600, а в памяти 0x3000. Для первой библиотеки INT равен 3058, значит в файле это будет смещение 0x0658. По этому адресу видим запись 0x3078 и вторую нулевую. Означающую конец списка. 3078 ссылается на 0x0678 это RAW-строка

«00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00»

Первые 2 байта нас не интересуют и равны нулю. А вот дальше идет строка с названием функции, заканчивающаяся нулем. То есть мы можем представить её как «\0\0MessageBoxA\0».

При этом IAT ссылается на аналогичную таблице IAT структуру, но только в нее при запуске программы будут загружены адреса функций. Например, для первой записи 0x3068 в памяти будет значение отличное от значения 0x0668 в файле. Там будет адрес функции MessageBoxA загруженный системой к которому мы и будем обращаться через вызов call в коде программы.

И последний кусочек пазла, библиотека kernel32. И не забываем добить нулями до SectionAlignment.

(10) RAW kernel32.dll (Offset 0x00000688-0x00000800)

 A8 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 A8 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 45 78 69 74 50 72 6F 63 65 73 73 00 00 00 00 00 00 00 00 00 00 00 . 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 

Проверяем что Far смог корректно определить какие функции мы импортировали:

Отлично! Все нормально определилось, значит теперь наш файл готов к запуску.
Барабанная дробь…

Финал

Поздравляю, мы справились!

Файл занимает 2 Кб = Заголовки 512 байт + 3 секции по 512 байт.

Число 512(0x200) ни что иное, как FileAlignment, который мы указали в заголовке нашей программы.

Дополнительно:
Если хочется вникнуть чуть глубже, можно заменить надпись «Hello World!» на что-нибудь другое, только не забудьте изменить адрес строки в коде программы (секция .text). Адрес в памяти 0x00402000, но в файле будет обратный порядок байт 00 20 40 00.

Или квест чуть сложнее. Добавить в код вызов ещё одного MessageBox. Для этого придется скопировать предыдущий вызов, и пересчитать в нем относительный адрес (0x3068 — RIP).

Заключение

Статья получилась достаточно скомканной, ей бы, конечно, состоять из 3 отдельных частей: Заголовки, Программа, Таблица импорта.

Если кто-то собрал свой exe значит мой труд был не напрасен.

Думаю в скором времени создать ELF файл похожим образом, интересна ли будет такая статья?)

  • Intel 64 and IA-32 Architectures Software Developer’s Manuals
    Руководство по командам и архитектуре процессора.
  • PE (Portable Executable): На странных берегах
    Отличная статья о структуре exe файлов.
  • Хранилище документации Майкрософт
    Тут можно найти любую информацию по заголовкам, структурам, типам и их описание

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *