создание приложений для dos
Привет из восьмидесятых: пишем код в легендарном текстовом редакторе Edlin для DOS
Я использую текстовый редактор Edlin, когда мне хочется переместиться в 80-е годы. Согласен, развлечение это своеобразное, но у всех свои причуды. Кто со мной?
Когда-то стандартным текстовым редактором в DOS был Edlin. Его создал Тим Патерсон — для первой версии DOS, которая тогда называлась 86-DOS, а позже получила название PC-DOS и MS-DOS. Патерсон говорил, что хотел со временем заменить этот редактор другим. И только десять лет спустя в MS-DOS 5 (1991) вместо Edlin появился Edit, полноэкранный текстовый редактор.
Здесь я буду использовать версию Edlin для FreeDOS. Это операционная система с открытым исходным кодом. Её можно использовать для игры в классические игры DOS, запуска старого программного обеспечения или разработки встроенных систем. FreeDOS поддерживает совместимость с MS-DOS и включает утилиты и программы, созданные по образу и подобию своих аналогов из MS-DOS.
Одна из таких программ — это open source реализация легендарного редактора Edlin, которая распространяется под лицензией GNU General Public License v2. Портированием Edlin на FreeDOS занимался Грегори Питч. У него получился GPL-лицензированный клон Edlin, который, ко всему прочему, умеет работать с длинными файлами. Он поставляется вместе с дистрибутивом FreeDOS. Клон Edlin также можно установить в MS-DOS. При желании вы даже можете скомпилировать Edlin для Linux- и Unix-систем.
В своей книге 23 Years of FreeDOS Грегори писал, что у редактора трёхуровневая архитектура: верхний уровень анализирует ввод пользователя и вызывает средний уровень, библиотеку edlib, которая, в свою очередь, вызывает код обработки строк и массивов для выполнения всей грязной работы. Но помимо технических достоинств, Edlin гораздо приятнее использовать, чем другие подобные продукты. На мой субъективный взгляд, конечно.
В FreeDOS 1.3 RC4 установлен Edlin 2.18. Это не самая последняя версия. Вы можете загрузить Edlin 2.19 из архива файлов FreeDOS на Ibiblio. Вы найдёте там два файла: edlin-2.19.zip содержит исходный код, а edlin-219exe.zip — это исполняемый файл DOS. Загрузите файл edlin-219exe.zip и распакуйте его в свою систему FreeDOS. Я поместил свой экземпляр редактора в C:\EDLIN.
Нужно немного попрактиковаться, чтобы «въехать в тему» и прочувствовать всю прелесть работы с Edlin. Поэтому давайте создадим и отредактируем новый файл, выполнив ряд интересных манипуляций.
Основы работы с Edlin
C:\EDLIN> edlin hello.c
Обратите внимание: здесь я ввёл команду FreeDOS в нижнем регистре. Но вообще FreeDOS нечувствительна к регистру, поэтому можно писать названия команд и имена файлов хоть в в верхнем, хоть в нижнем регистре — работать всё будет одинаково. Можете проверить это: введите edlin, EDLIN и Edlin. Во всех случаях ОС будет запускать редактор Edlin. Точно так же с именем файла: FreeDOS поймёт, какой файл вы имеете в виду, если вы напишете hello.c, HELLO.C или Hello.C.
C:\EDLIN> edlin hello.c
edlin 2.19, copyright © 2003 Gregory Pietsch
This program comes with ABSOLUTELY NO WARRANTY.
It is free software, and you are welcome to redistribute it
under the terms of the GNU General Public License — either
version 2 of the license, or, at your option, any later
hello.c: 0 lines read
Когда редактор запустится и отработает введённую команду, он переведёт нас на новую строку. В начале этой строки будет стоять символ «*» (звёздочка). Интерфейс Edlin минималистичен: у него нет «меню» или поддержки мыши. Чтобы начать редактирование, изменить отдельные строки файла, выполнить поиск и замену, сохранить свою работу или выйти из редактора, просто введите соответствующие команды после звёздочки.
В данном случае сообщение hello.c: 0 lines read ожидаемо. Это новый файл, он пустой, нам нужно добавить туда новые строки. Мы сделаем это с помощью команды insert, набрав i. Знак «*» изменится на «:» после этого можно вводить новый текст. Для форматирования текста в процессе набора можно использовать перевод строки (ENTER). Когда закончите добавлять новый текст, введите точку (.) в отдельной строке.
Чтобы просмотреть введённый вами текст, используйте команду list, введя l в приглашении *. Edlin будет отображать строки по одному экрану за раз, предполагая, что на дисплее будет 23-25 строк. Но для такой короткой программы, как Hello world исходный код тем более поместится на одном экране:
Вы заметили знак «*» в строке 7, перед закрывающей фигурной скобкой? Это специальная отметка, указывающая текущую позицию для вставки следующего текста. Если вы вставите новый текст в файл, Edlin добавит его на место строки 7, сместив её ниже.
Давайте обновим исходный код так, чтобы функция main() возвращала результат. Для этого нам нужно добавить строку «return 0;», начиная с текущей позиции. Мы вновь используем команду i. Не забудьте ввести точку на отдельной строке, чтобы прекратить ввод нового текста.
После повторного чтения содержимого файла вы увидите, что вставили новый текст в правильное место, а закрывающая фигурная скобка переместилась на строку 8.
Но что, если вам нужно отредактировать отдельную строку с номером N? И тут проблем не будет: в командной строке после * просто введите номер строки, которую хотите отредактировать. Далее вам нужно будет повторно ввести всю строку с внесёнными изменениями.
К примеру, давайте обновим сигнатуру функции main(). Она находится на 4-й строке, поэтому после * введите 4 и затем полностью введите изменённую строку.
После повторного чтения содержимого файла отображается обновлённая строка 4.
Когда вы внесёте все необходимые изменения, не забудьте сохранить файл. Для этого после * просто введите w. Чтобы выйти из Edlin, введите q.
Больше возможностей Edlin
Выше описаны основные команды для редактирования файлов. Но Edlin делает больше, чем просто «вставляет, редактирует и сохраняет». Вот удобная шпаргалка, расширенные возможности Edlin. В этой таблице text обозначает обычную текстовую строку, filename — имя файла вместе с путём, а num — число.
? | Показать раздел «Помощь» |
num | Отредактировать строку номер num |
a | Добавить строку ниже текущей |
[num]i | Добавить строки выше текущей |
[num1][,num2]l | Прочитать из файла диапазон строк от номера num1 до num2. Если диапазон не указан, команда выдаст первые 23 строки файла |
[num1][,num2]p | Прочитать из файла диапазон строк от номера num1 до num2. Если диапазон не указан, команда выдаст все строки файла |
[num1],[num2],num,[num3]c | Скопировать строки с номерами num1..num2 на позицию строки с номером num. Число num3 определяет количество копий |
[num1],[num2],num m | Переместить строки с номерами num1..num2 на позицию строки с номером num |
[num][,num][?]s text | Найти строку text |
[num1][,num2][?]r text1,text2 | Заменить строку text1 из диапазона от num1 до num2 на строку text2 |
[num1][,num2]d | Удалить строки из диапазона от num1 до num2 |
[num]t filename | Начиная с заданной позиции, вставить содержимое другого файла |
[num]w[filename] | Сохранить файл на диск |
q | Выйти из Edlin |
e[filename] | Сохранить и выйти |
Бонус
В Edlin можно вводить специальные символы, используя соответствующие коды:
\a | alert |
\b | backspace |
\e | escape |
\f | formfeed |
\t | horizontal tab |
\v | vertical tab |
\» | double quote |
\’ | single quote |
\. | period |
\\ | backslash |
\xXX | hexadecimal number |
\dNNN | decimal number |
\OOO | octal number |
\^C | control character |
Выделенный сервер VDS с быстрыми NVMе-дисками и посуточной оплатой у хостинга Маклауд.
Создание программы для MS-Dos’а
Даже не спрашивайте для чего это мне понадобилось, но понадобилось. А возможно ли сейчас написать программу для ms-dos используя visual с++ 2005? Как и чем заменить те библиотеки которые были выброшены со временем из стандарта, но без которых невозможно такие программы писать?
Вообщем-то потребности небольшие:
— вывод символа в любом месте и любого цвета
— как минимум 256 цветов и больше
— возможность рисования панелей, как. ну например в norton commander или любой другой программе времен Dos.
— возможность вывода русских букв без особых танцев с бубном и молитв великому Ктулху
Попробуй djgpp + библиотеку SEAL
0iStalker
он же сказал, под Microsoft visual с++ 2005, а не под DJGPP. Да и оф. сайт лет 10 не обновлялся.
kosiak
> он же сказал, под Microsoft visual с++ 2005
А почему обязательно Microsoft Visual C++? Другими компиляторами совсем-совсем пользоваться нельзя? Странно всё это
что то новенькое. тогда Dos не умер бы. 🙂
Viaceslav(C)
> что то новенькое. тогда Dos не умер бы. 🙂
А что, нельзя разве? ВЕСА 1024 на 768, 256 цветов поддерживает.
Можно и больше цветов, если строчек 30 вызовов прерываний расписать.
ДОС умер не из-за этого.
TarasB
ну я имел в виду вывод в самой консоле.
Viaceslav(C)
> ну я имел в виду вывод в самой консоле.
Тогда не знаю. Говорят, СВГА что-то прикольное умел.
TarasB
> Говорят, СВГА что-то прикольное умел.
больше их не слушай 🙂
кста, ты уже свга режим описал
>>А почему обязательно Microsoft Visual C++?
Не всмысле именно среду, я хотел сказать что используя текущий стандарт языка c++
>>как в norton commander, означает работать в текстовом режиме, и это 16 цветов, 256 цветов
Я имел ввиду только панели, а не кол-во цветов в данной записи. То есть мне GUI вообще не нужен, только панели, даже кнопки на самом деле не особо нужны
>>что то новенькое. тогда Dos не умер бы. 🙂
Ну вообще-то на turbo pascal это легко делается и именно в консоли.
Не думаю что сейчас есть современные компилеры поддерживающие дос. Уж гсс насколько многоплатформеннен, но доса в перечне я не нашел.
Так что придется довольствоваться bcc4.5.
=A=L=X=
> Не думаю что сейчас есть современные компилеры поддерживающие дос.
=A=L=X=
> Так что придется довольствоваться bcc4.5.
Assembler. Установка интерпретатора и запуск первой программы через DOSBox
В данной статье разбирается способ установки интерпретатора и запуск файла EXE через DOSBox. Планировалось погрузить читателя в особенности программирования на TASM, но я согласился с комментаторами. Есть много учебников по Ассемблер и нет смысла перепечатывать эти знания вновь. Лично мне в изучении очень помог сайт av-assembler.ru. Рекомендую. В комментариях также вы найдёте много другой литературы по Assembler. А теперь перейдём к основной теме статьи.
Для начала давайте установим наш старенький интерпретатор.
Ссылка
Почему именно vk.com?
Я прекрасно понимаю, что это ещё тот колхоз делиться файлами через обсуждения VK, но кто знает, во что может превратиться эта маленькая группа в будущем.
После распаковки файлов, советую сохранить их в папке Asm на диск C, чтобы иметь меньше расхождений с представленным тут материалом. Если вы разместите директорию в другое место, изменится лишь путь до файлов, когда вы будете использовать команду mount.
Для запуска интерпретатора нам так же потребуется эмулятор DOSBox. Он и оживит все наши компоненты. Скачаем и установим его!
Ссылка
В папке Asm я специально оставил файл code.asm. Именно на нём мы и потренируемся запускать нашу программу. Советую сохранить его копию, ибо там хранится весь код, который в 99% случаев будет присутствовать в каждом вашем проекте.
Итак. Запускаем наш DOSBox и видим следующее:
Для простоты сопоставим имя пути, по которому лежит наша папка Asm. Чтобы это сделать, пропишем следующую команду:
Здесь вместо d: мы можем использовать любую другую букву. Например назвать i или s. А C это наш реальный диск. Мы прописываем путь до наших файлов ассемблера.
Теперь, откроем смонтированный диск:
Прописав команду dir, мы сможем увидеть все файлы, которые там хранятся. Здесь можно заметить и наш файл CODE с расширением ASM, а также дату его создания.
И только теперь мы начинаем запускать наш файл! Бедные программисты 20 века, как они только терпели всё это? Пропишем следующую команду:
После мы увидим следующее сообщение, а наша директория пополнится новым файлом с расширением OBJ.
Теперь пропишем ещё одну команду:
В нашей папке появилась ещё пара файлов – CODE.MAP и CODE.EXE. Последний как раз и есть исполняемый файл нашего кода assembler.
Если он появился, значит, мы можем запустить режим отладки нашей программы, введя команду последнюю команду. Обратите внимание, теперь мы не указываем расширение файла, который запускаем.
Этот старинный интерфейс насквозь пропитан духом ушедшей эпохи старых операционных систем. Тем не менее…
Нажав F7 или fn + F7 вы сможете совершить 1 шаг по коду. Синяя строка начнёт движение вниз, изменяя значения регистров и флагов. Пока это всего лишь шаблон, на котором мы потренировались запускать нашу программу в режиме дебага. Реальное “волшебство” мы увидим лишь с полноценным кодом на asm.
Небольшой пример для запуска
Прога проверяет, было ли передано верное число открывающих и закрывающих скобок:
Давайте ознакомимся с имеющимися разделами.
Code segment – место, где turbo debug отражает все найденные строки кода. Важное замечание – все данные отражаются в TD в виде 16-ричной системы. А значит какая-нибудь ‘12’ это на самом деле 18, а реальное 12 это ‘C’. CS аналогичен разделу “Begin end.” на Pascal или функции main.
Data segment, отражает данные, которые TD обнаружил в d_s. Справа мы видим их символьную (char) интерпретацию. В будущем мы сможем увидеть здесь наш “Hello, world”, интерпретируемый компилятором в числа, по таблице ASCII. Хорошей аналогией DS является раздел VAR, как в Pascal. Для простоты можно сказать, что это одно и тоже.
Stack segment – место хранения данных нашего стека.
Регистры
Все эти ax, bx, cx, si, di, ss, cs и т. д. – это наши регистры, которые используются как переменные для хранения данных. Да, это очень грубое упрощение. Переменные из Pascal и регистры Assembler это не одно и тоже, но надеюсь, такая аналогия даёт более чёткую картину. Здесь мы сможем хранить данные о циклах, арифметических операциях, системных прерываниях и т. д.
Флаги
Все эти c, z, s, o, p и т.д. это и есть наши флаги. В них хранится промежуточная информация о том, например, было ли полученное число чётным, произошло ранее переполнение или нет. Они могут хранить результат побитого сдвига. По опыту, могу сказать, на них обращаешь внимание лишь при отладке программы, а не во время штатного исполнения.
Маленькая шпаргалка для заметок:
mount d: c:\asm – создаём виртуальный диск, где корень –папка asm
tasm code.asm – компилируем исходный код
tlink code.obj – создаём исполняемый файл
td code – запускаем debug
F7 – делаем шаг в программе
Буду ждать комментарии от всех, кому интересен Assembler. Чувствую, я где-то мог накосячить в терминологии или обозначении того или иного элемента. Но статья на Habr отличный повод всё повторить.
Создаем EXE
Самоизоляция это отличное время приступить к тому, что требует много времени и сил. Поэтому я решил заняться тем, чем всегда хотел — написать свой компилятор.
Сейчас он способен собрать Hello World, но в этой статье я хочу рассказать не про парсинг и внутреннее устройство компилятора, а про такую важную часть как побайтовая сборка exe файла.
Начало
Хотите спойлер? Наша программа будет занимать 2048 байт.
Обычно работа с exe файлами заключается в изучении или модификации их структуры. Сами же исполняемые файлы при этом формируют компиляторы, и этот процесс кажется немного магическим для разработчиков.
Но сейчас мы с вами попробуем это исправить!
Для сборки нашей программы нам потребуется любой HEX редактор (лично я использовал HxD).
Для старта возьмем псевдокод:
Первые две строки указывают на функции импортируемые из библиотек WinAPI. Функция MessageBoxA выводит диалоговое окно с нашим текстом, а ExitProcess сообщает системе о завершении программы.
Рассматривать отдельно функцию main нет смысла, так как в ней используются функции, описанные выше.
DOS Header
Для начала нам нужно сформировать корректный DOS Header, это заголовок для DOS программ и влиять на запуск exe под Windows не должен.
Более-менее важные поля я отметил, остальные заполнены нулями.
Самое главное, что этот заголовок содержит поле e_magic означающее, что это исполняемый файл, и e_lfanew — указывающее на смещение PE-заголовка от начала файла (в нашем файле это смещение равно 0x80 = 128 байт).
Отлично, теперь, когда нам известна структура заголовка DOS Header запишем ее в наш файл.
Сначала я использовал левую колонку как на скриншоте для указания смещения внутри файла, но тогда неудобно копировать исходный текст, приходится обрезать каждую строку.
Поэтому для удобства в первой скобке каждого блока указан порядок добавления в файл, а в последней смещение в файле (Offset) по которому должен располагаться данный блок.
Например, первый блок мы вставляем по смещению 0x00000000, и он займет 64 байта (0x40 в 16-ричной системе), следующий блок мы будем вставлять уже по этому смещению 0x00000040 и т.д.
Готово, первые 64 байта записали. Теперь нужно добавить еще 64, это так называемый DOS Stub (Заглушка). Во время запуска из-под DOS, она должна уведомить пользователя что программа не предназначена для работы в этом режиме.
Но в целом, это маленькая программа под DOS которая выводит строку и выходит из программы.
Запишем наш Stub в файл и рассмотрим его детальнее.
А теперь этот же код, но уже в дизассемблированном виде
Это работает так: сначала заглушка выводит строку о том, что программа не может быть запущена, а затем выходит из программы с кодом 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.
Разберемся что хранится в этой структуре:
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).
IMAGE_DATA_DIRECTORY — массив записей о каталогах. В теории его можно уменьшить, сэкономив пару байт, но вроде как все описывают все 16 полей даже если они не нужны. А теперь чуть подробнее.
У каждого каталога есть свой номер, который описывает, где хранится его содержимое. Пример:
Export(0) — Содержит ссылку на сегмент который хранит экспортируемые функции. Для нас это было бы актуально если бы мы создавали DLL. Как это примерно должно работать можно посмотреть на примере следующего каталога.
Import(1) — Этот каталог указывает на сегмент с импортируемыми функциями из других DLL. В нашем случае значения VirtualAddress = 0x3000 и Size = 0xB8. Это единственный каталог, который мы опишем.
Resource(2) — Каталог с ресурсами программы (Изображения, Текст, Файлы и т.д.)
Значения других каталогов можно посмотреть в документации.
Теперь, когда мы посмотрели из чего состоит NT-заголовок, запишем и его в файл по аналогии с остальными по адресу 0x80.
В результате получаем вот такой вид IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER64 и IMAGE_DATA_DIRECTORY заголовков:
Далее описываем все секции нашего приложения согласно структуре IMAGE_SECTION_HEADER
В нашем случае у нaс будет 3 секции.
Почему Virtual Address (VA) начинается с 1000, а не с нуля я не знаю, но так делают все компиляторы, которые я рассматривал. В результате 1000 + 3 секции * 1000 (SectionAlignment) = 4000 что мы и записали в SizeOfImage. Это полный размер нашей программы в виртуальной памяти. Вероятно, используется для выделения места под программу в памяти.
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 и запись будет начинаться с середины файла.
Следующий адрес для записи будет 00000200 что соответствует полю SizeOfHeaders PE-Заголовка. Если бы мы добавили еще одну секцию, а это плюс 40 байт, то наши заголовки не уложились бы в 512 (0x200) байт и пришлось бы использовать уже 512+40 = 552 байта выровненные по FileAlignment, то есть 1024 (0x400) байта. А все что останется от 0x228 (552) до адреса 0x400 нужно чем-то заполнить, лучше конечно нулями.
Взглянем как выглядит блок секций в Far:
Далее мы запишем в наш файл сами секции, но тут есть один нюанс.
Как вы могли заметить на примере SizeOfHeaders, мы не можем просто записать заголовок и перейти к записи следующего раздела. Так как что бы записать заголовок мы должны знать сколько займут все заголовки вместе. В результате нам нужно либо посчитать заранее сколько понадобиться места, либо записать пустые (нулевые) значения, а после записи всех заголовков вернуться и записать уже их реальный размер.
Но в данном случае я уже все рассчитал, поэтому будем сразу записывать блоки кода.
Конкретно для этой программы первые 3 строки, ровно, как и 3 последние не обязательны.
Последние 3 даже не будут исполнены, так как выход из программы произойдет еще на второй функции call.
Но скажем так, если бы это была не функция main, а подфункция следовало бы сделать именно так.
А вот первые 3 в данном случае хоть и не обязательны, но желательны. Например, если бы мы использовали не MessageBoxA, а printf то без этих строк получили бы ошибку.
Согласно соглашению о вызовах для 64-разрядных систем MSDN, первые 4 параметра передаются в регистрах RCX, RDX, R8, R9. Если они туда помещаются и не являются, например числом с плавающей точкой. А остальные передаются через стек.
По идее если мы передаем 2 аргумента функции, то должны передать их через регистры и зарезервировать под них два места в стеке, что бы при необходимости функция могла скинуть регистры в стек. Так же мы не должны рассчитывать, что нам вернут эти регистры в исходном состоянии.
Так вот проблема функции printf заключается в том, что, если мы передаем ей всего 1 аргумент, она все равно перезапишет все 4 места в стеке, хотя вроде бы должна перезаписать только одно, по количеству аргументов.
Поэтому если не хотите, чтобы программа себя странно вела, всегда резервируйте как минимум 8 байт * 4 аргумента = 32(0x20) байт, если передаете функции хотя бы 1 аргумент.
Рассмотрим блок кода с вызовами функций
Сначала мы передаем наши аргументы:
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.
Именно эти смещения мы и видели в командах ассемблера.
Хочется отметить что всего лишь 4 строки реального кода содержат весь наш код на ассемблере. А все остальное нули что бы набрать FileAlignment. Последней строкой заполненной нулями будет 0x000003F0, после идет 0x00000400, но это будет уже следующий блок. Итого в файле уже 1024 байта, наша программа весит уже целый Килобайт! Осталось совсем немного и ее можно будет запустить.
Это, пожалуй, самая простая секция. Мы просто положим сюда две строки добив нулями до 512 байт.
Ну вот осталась последняя секция, которая описывает импортируемые функции из библиотек.
Первое что нас ждет новая структура IMAGE_IMPORT_DESCRIPTOR
Для начала нам нужно добавить 2 импортируемых библиотеки. Напомним:
У нас используется 2 библиотеки, а что бы сказать что мы закончили их перечислять. Последняя структура заполняется нулями.
Теперь добавим имена самих библиотек:
Далее опишем библиотеку user32:
Поле Name первой библиотеки указывает на 0x303C если мы посмотрим чуть выше, то увидим что по адресу 0x063C находится библиотека «user32.dll\0».
«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.
Проверяем что Far смог корректно определить какие функции мы импортировали:
Отлично! Все нормально определилось, значит теперь наш файл готов к запуску.
Барабанная дробь…
Финал
Поздравляю, мы справились!
Файл занимает 2 Кб = Заголовки 512 байт + 3 секции по 512 байт.
Число 512(0x200) ни что иное, как FileAlignment, который мы указали в заголовке нашей программы.
Или квест чуть сложнее. Добавить в код вызов ещё одного MessageBox. Для этого придется скопировать предыдущий вызов, и пересчитать в нем относительный адрес (0x3068 — RIP).
Заключение
Статья получилась достаточно скомканной, ей бы, конечно, состоять из 3 отдельных частей: Заголовки, Программа, Таблица импорта.
Если кто-то собрал свой exe значит мой труд был не напрасен.
Думаю в скором времени создать ELF файл похожим образом, интересна ли будет такая статья?)