7. Особенности написания com и exe программ на Ассемблере
из ассемблерного файла путем компиляции получается файлы промежуточного объектного кода, имеющего расширение .obj. Такой файл содержит оптимизированный машинный код, при условии, что не встретились синтаксические и семантические ошибки. Если в исходном файле с программой на языке ассемблера обнаруживаются ошибки, то программисту выдается список обнаруженных ошибок, в котором ошибки указываются с номером строки, в которой они обнаружены. Программист циклически выполняет действия по редактированию и компиляции до тех пор, пока не будут устранены все ошибки в исходном файле. На этом этапе уже возможно получение готовой программы, но чаще всего в ней не хватает некоторых компонентов. Если компилятор по какой-либо причине (неверно прописан путь к такому файлу или файл отсутствует) не может найти inc-файл, то выдается предупреждение и obj-файл получен не будет.
Ассемблирование, как правило, проходит в два приема. При первом проходе переводятся мнемонические команды, десятеричные числа и символы в соответствующие машинные коды, подсчитывается, сколько какая команда занимает места, обнаруженные имена, введенные пользователем (константы, метки, переменные) их тип и числовое значение записывается в таблицу с каких адресов начинаются процедуры, адреса меток, адреса начала/конца сегментов и т. д., при втором проходе подставляются адреса начала процедур, заменяются названия меток на адреса.
В результате ассемблирования получается так называемый «объектный файл». В качестве дополнительной возможности ассемблер может создать файл листинга программы.
Обычно для получения файлов объектного кода необходимо выполнить соответствующую программу ассемблера (программы MASM.EXE и ML.EXE фирмы Microsoft и TASM.EXE или TASM32.EXE фирмы Borland) указав в командной строке имя файла с текстом программы. Например, если у текстового файла с исходным текстом программы название prog.asm
Следующая стадия (.obj —> .exe/.dll/.com), называется компоновкой и служит для замещения символьных имен, используемых программистом на реальные адреса.
В EXE-файле присутствует та же последовательность байтов, что и в OBJ-файле. Но помимо этого еще присутствует: имя ассемблированного файла, версия ассемблера, «имя собственное» сегмента и так далее.
Это «служебная» информация, предназначенная для тех случаев, когда ваш исполнимый файл вы хотите собрать из нескольких. При разработке больших приложений исходный текст состоит, как правило, из нескольких модулей. Каждый модуль по раздельности компилируется в отдельный файл с объектным кодом. В каждом из этих файлов прописаны свои сегменты кода/данных/стека, которые затем надо объединить в одно целое. А исполнимый файл нам нужно получить только один – с единым сегментом кода/данных/стека. Именно это LINK и делает: завершает определение адресных ссылок и объединяет, если это требуется, несколько программных модулей в один. И этот один у нас и является исполнимым.
Исполнимый код – это законченная программа с расширением COM, DLL или EXE, которую можно запустить на компьютере с установленной операционной системой, для которой эта программа создавалась.
Процесс объединения объектных модулей в один файл осуществляется специальной программой-компоновщиком или сборщиком (программа LINK.EXE фирмы Microsoft и TLINK.EXE фирмы Borland), которая выполняет связывание объектных модулей и машинного кода стандартных функций, находя их в библиотеках, и формирует на выходе работоспособное приложение – исполнимый код для конкретной платформы.
Разница между COM и EXE в том, что:
1. В EXE-файлах присутствует заголовочная часть с некоторым набором служебных таблиц для ОС(размером 512 байт или более), а COM-файлы заголовка не имеют и представляют собой обычный бинарник предназначенный для работы в пределах одного сегмента памяти DOS.
2. Стандартный размер COM-файла не может быть более 64 кБ, а EXE-файл может иметь любой объем.
Как создать com файл
Вы используете устаревший браузер. Этот и другие сайты могут отображаться в нём некорректно.
Необходимо обновить браузер или попробовать использовать другой.
COM в Ассемблере — Часть I
COM в Ассемблере — Часть I — Архив WASM.RU
В этой статье будет расказано о том, как использовать COM-интерфейсы в ваших программах, написанных на ассемблере. Не будет обсуждаться, что такое COM и как он применяется, но как его можно использовать, программируя на ассемблере. Здесь будет затронуто только применение существующих интерфейсов, а не реализация своих собственных, это будет рассмотрено в другой статье.
О COM
Это краткое введение в основы COM.
Получить доступ к COM-объекту можно только через один или большее количество наборов связанных с ним функций. Эти наборы функций называются интерфейсами, а функции интерфейса называются методами. COM требует, чтобы существовал только один путь доступа к методам интерфейса — через указатель на интерфейс.
По терминологии COM, интерфейс — это «контракт», состоящий из группы связанных друг с другом прототипов функций, чье использование определено, а реализация — нет. Определение интерфейса задает функции интерфейса, называемые методами, типы возвращаемых ими значений, количество и типы их параметров, и что они должны делать. С интерфейсом не ассоциируется какая-то конкретная его реализация. Реализация интерфейса — это код, который предоставляет программист для выполнения действий, заданных определением интерфейса.
Экземпляр реализации интерфейса — это указатель на массив указателей на методы (таблица указателей, ссылающиеся на реализацию всех методов, указанных в интерфейсе). Любой код, у которого есть подобный указатель, может вызывать методы этого интерфейса.
Использование COM-объекта в Ассемблере
Доступ к COM-объекту осуществляется через указатель, который указывает на таблицу указателей на функции (эту таблицу еще называют таблицей виртуальных функций или vtable для краткости). Эта таблица содержит адреса каждого из методов объекта. Чтобы вызывать метод, вы косвенно вызываете его через эту таблицу указателей.
Здесь приводится пример интерфейса на C++, и как называются его методы:
Как собрать досовский COM-файл компилятором GCC
В минувшие выходные я участвовал в Ludum Dare #31. Но даже до объявления тем конференции из-за своего недавнего увлечения я хотел сделать олдскульную игру под DOS. Целевой платформой выбрана DOSBox. Это самый практичный способ запуска DOS-приложений несмотря на то, что все современные процессоры x86 полностью обратно совместимы со старыми, вплоть до 16-битного 8086.
Я успешно создал и показал на конференции игру DOS Defender. Программа работает в реальном режиме 32-битного 80386. Все ресурсы встроены в исполняемый COM-файл, никаких внешних зависимостей, так что игра целиком упакована в бинарник 10 килобайт.
Для игры понадобится джойстик или геймпад. Я включил поддержку мыши в релиз для Ludum Dare ради презентации, но потом удалил её, потому что она не очень хорошо работала.
Наиболее технически интересная часть заключается в том, что для создания игры не понадобились никакие инструменты разработки DOS! Я использовал только обычный компилятор Linux C (gcc). В реальности даже нельзя собрать DOS Defender под DOS. Я рассматриваю DOS только как встроенную платформу, что и есть единственная форма, в которой DOS всё ещё существует сегодня. Вместе с DOSBox и DOSEMU это довольно удобный набор инструментов.
Если вас интересует только практическая часть разработки, перейдите к разделу «Обманываем GCC», где мы напишем DOS COM программу “Hello, World” с GCC Linux.
Поиск правильных инструментов
Когда я начал этот проект, то не думал о GCC. В реальности я пошёл по этому пути, когда обнаружил пакет bcc (Bruce’s C Compiler) для Debian, который собирает 16-битные бинарники для 8086. Его держат для компиляции загрузчиков x86 и прочего, но bcc также можно использовать для компиляции DOS COM файлов. Это меня заинтересовало.
Для справки: 16-битный микропроцессор Intel 8086 вышел в 1978 году. У него не было никаких причудливых функций современных процессоров: ни защиты памяти, ни инструкций с плавающей запятой и только 1 МБ адресуемой RAM. Все современные десктопы и ноутбуки x86 всё ещё могут притвориться этим 16-битным процессором 8086 сорокалетней давности, с такой же ограниченной адресацией и всё такое. Это нехилая обратная совместимость. Такая функция называется реальным режимом. Это режим, в котором загружаются все компьютеры x86. Современные ОС сразу переключаются в защищённый режим с виртуальной адресацией и безопасной многозадачностью. DOS так не поступал.
К сожалению, bcc — не компилятор ANSI C. Он поддерживает подмножество K&R C, а также встроенный ассемблерный код x86. В отличие от других компиляторов 8086 C, у него нет понятия «дальних» или «длинных» указателей, поэтому для доступа к другим сегментам памяти (VGA, тактовые импульсы и т. д.) необходим встроенный ассемблерный код. Примечание: остатки этих «длинных указателей» 8086 до сих сохранились в Win32 API: LPSTR , LPWORD , LPDWORD и др. Тот встроенный ассемблер даже близко не сравнится со встроенным ассемблером GCC. На ассемблере нужно вручную загружать переменные из стека, а поскольку bcc поддерживает два разных соглашения о вызовах, то переменные в коде следует жёстко закодировать в соответствии с одним или другим соглашением.
Учитывая такие ограничения, я решил искать альтернативы.
DJGPP
DJGPP — порт GCC под DOS. Реально очень впечатляющий проект, который переносит под DOS почти весь POSIX. Многие портированные под DOS программы сделаны на DJGPP. Но он создаёт только 32-битные программы для защищённого режима. Если в защищённом режиме нужно работать с аппаратным обеспечением (например, VGA), то программа делает запросы к сервису интерфейса защищённого режима DOS (DPMI). Если бы я взял DJGPP, то не смог бы ограничиться единственным автономным бинарником, потому что пришлось бы поиметь и сервер DPMI. Производительность тоже страдает от запросов к DPMI.
Получить необходимые инструментальные средства для DJGPP сложно, мягко говоря. К счастью, я нашел полезный проект build-djgpp, который всё запускает, по крайней мере, на Linux.
Либо там серьёзная ошибка, либо официальные бинарники DJGPP опять заразились вирусом, но при при запуске моих программ в DOSBox постоянно возникала ошибка “Not COFF: check for viruses”. Для дополнительной проверки, что вирусы не на моей собственной машине, я настроил среду для DJGPP на своём Raspberry Pi, который действует как чистая комната. Это устройство на базе ARM невозможно заразить вирусом x86. И всё равно возникала та же проблема, и все двоичные хэши совпадали между машинами, так что это не моя вина.
Так что учитывая это и проблему DPMI, я начал искать дальше.
Обманываем GCC
На чём я в итоге остановился — так это на хитром трюке по «обману» GCC для сборки DOS COM-файлов реального режима. Трюк работает до 80386 (что обычно и нужно). Процессор 80386 выпущен в 1985 году и стал первым 32-битным x86 микропроцессором. GCC по-прежнему придерживается этого набора инструкций, даже в среде x86-64. К сожалению, GCC никак не может производить 16-битный код, так что от изначальной цели сделать игру для 8086 пришлось отказаться. Впрочем, это не имеет значения, потому что целевая платформа DOSBox по сути является эмулятором 80386.
В теории трюк должен работать и в компиляторе MinGW, но там есть давняя ошибка, которая мешает ему работать правильно (“cannot perform PE operations on non PE output file”). Впрочем, её можно обойти, и я делал это сам: следует удалить директиву OUTPUT_FORMAT и добавить дополнительный шаг objcopy ( objcopy -O binary ).
Hello World в DOS
Для демонстрации создадим досовскую COM-программу “Hello, World” с помощью GCC на Linux.
В этом способе есть главное и значительное препятствие: стандартной библиотеки не будет. Это как писать операционную систему с нуля, за исключением нескольких служб, которые обеспечивает DOS. Это значит, нет printf() и тому подобного. Вместо этого мы попросим DOS вывести строку в консоль. Создать запрос к DOS требует запуска прерывания, что означает встроенный ассемблерный код!
В DOS девять прерываний: 0x20, 0x21, 0x22, 0x23, 0x24, 0х25, 0x26, 0x27, 0x2F. Самое главное, которое нас интересует, это 0x21, функция 0x09 (вывести строку). Между DOS и BIOS есть тысячи функций, названных по такому шаблону. Я не собираюсь пытаться объяснить ассемблер x86, но вкратце номер функции забивается в регистр ah — и прерывание 0x21 срабатывает. Функция 0x09 также принимает аргумент — указатель на строку для печати, который передается в регистрах dx и ds .
Вот функция print() встроенного ассемблера GCC. Строки, передаваемые этой функции, должны заканчиваться символом $. Почему? Потому что DOS.
Код объявлен volatile , поскольку у него побочный эффект (печать строки). Для GCC ассемблерный код непрозрачен, и оптимизатор полагается на ограничения выхода/входа/клоббера (последние три строки). Для таких DOS-программ любой встроенный ассемблер будет с побочными эффектами. Это потому что он пишется не для оптимизации, а для доступа к аппаратным ресурсам и DOS — вещей, недоступных простому C.
Нужно также позаботиться о вызывающем операторе, потому что GCC не знает, что память, на которую указывает string , когда-либо читалась. Вероятно, массив, который поддерживает строку, тоже придётся объявить volatile . Всё это предвещает неизбежное: любые действия в такой среде превращаются в бесконечную борьбу с оптимизатором. Не все из этих битв можно выиграть.
Теперь к основной функции. Её название по идее не важно, но я избегаю называть её main() , потому что у MinGW есть забавные идеи, как обрабатывать конкретно такие символы, даже если его просят не делать этого.
COM-файлы ограничены размером 65279 байт. Это связано с тем, что сегмент памяти x86 составляет 64 КБ, а DOS просто загружает COM-файлы в адрес 0x0100 сегмента и выполняет. Заголовков нет, только чистый бинарник. Поскольку программа COM в принципе не может иметь значительный размер, то не должно происходить и никакой реальной компоновки (freestanding), вся вещь компилируется как одна единица трансляции. Это будет один вызов GCC с кучей параметров.
Параметры компилятора
Вот основные параметры компилятора.
-std=gnu99 -Os -nostdlib -m32 -march=i386 -ffreestanding
Поскольку стандартные библиотеки не используются, то единственное различие между gnu99 и c99 заключается в отключенных триграфах (как и должно быть), и встроенный ассемблер можно записать как asm вместо __asm__ . Это не бином Ньютона. Проект будет настолько тесно связан с GCC, что я всё равно не озабочен расширениями GCC.
Параметр -Os насколько возможно уменьшает результат компиляции. Так и программа будет работать быстрее. Это важно с прицелом на DOSBox, потому что эмулятор по умолчанию работает медленно как машина 80-х. Я хочу вписаться в это ограничение. Если оптимизатор вызывает проблемы, то временно поставим -O0 , чтобы определить, тут ваша ошибка или оптимизатора.
Как видите, оптимизатор не понимает, что программа будет работать в реальном режиме с соответствующими ограничениями адресации. Он выполняет всевозможные невалидные оптимизации, которые ломают ваши совершенно валидные программы. Это не баг GCC, ведь мы сами тут делаем сумасшедшие вещи. Мне пришлось несколько раз переделывать код, чтобы помешать оптимизатору сломать программу. Например, пришлось избегать возврата сложных структур из функций, потому что они иногда заполнялись мусором. Настоящая опасность в том, что будущая версия GCC станет ещё умнее и будет ломать ещё больше кода. Здесь ваш друг volatile .
Следующий параметр -nostdlib , поскольку мы не сможем залинковаться ни с какими валидными библиотеками, даже статически.
Параметры -m32-march=i386 командуют компилятору выдавать код 80386. Если бы я писал загрузчик для современного компьютера, то прицел на 80686 тоже был бы нормальный, но DOSBox — это 80386.
Аргумент -ffreestanding требует, чтобы GCC не выдавал код, который обращается к функциям хелпера встроенной стандартной библиотеки. Иногда он вместо реально рабочего кода выдаёт код для вызова встроенной функции, особенно с математическими операторами. У меня это была одна из основных проблем с bcc, где такое поведение невозможно отключить. Такой параметр чаще всего используется при написании загрузчиков и ядер ОС. А теперь и досовских COM-файлов.
Параметры компоновщика
Параметр -Wl используется для передачи аргументов компоновщику ( ld ). Нам это нужно, поскольку мы всё делаем за один вызов GCC.
—nmagic отключает выравнивание страниц разделов. Во-первых, нам оно не требуется. Во-вторых, оно впустую отнимает драгоценное пространство. В моих тестах это не кажется необходимой мерой, но я на всякий случай оставляю эту опцию.
Параметр —script указывает, что мы хотим использовать особый скрипт компоновщика. Это позволяет точно разместить разделы ( text , data , bss , rodata ) нашей программы. Вот скрипт com.ld .
OUTPUT_FORMAT(binary) говорит не помещать это в файл ELF (или PE и т. д.). Компоновщик должен просто сбросить чистый код. COM-файл — это просто чистый код, то есть мы даём команду компоновщику создать файл COM!
Я говорил, что COM-файлы загружаются в адрес 0x0100 . Четвёртая строка смещает туда бинарник. Первый байт COM-файла по-прежнему остаётся первым байтом кода, но будет запускаться с этого смещения в памяти.
Далее следуют все разделы: text (программа), data (статичные данные), bss (данные с нулевой инициализацией), rodata (строки). Наконец, я отмечаю конец двоичного файла символом _heap . Это пригодится позже при написании sbrk() , когда мы закончим с “Hello, World”. Я указал выровнять _heap по 4 байтам.
Запуск программы
Компоновщик обычно знает нашу точку входа ( main ) и настраивает её для нас. Но поскольку мы запросили «двоичную» выдачу, то придётся разбираться самим. Если первой запустится функция print() , то выполнение программы начнётся с неё, что неправильно. Программе нужен небольшой заголовок для начала работы.
В скрипте компоновщика для таких вещей есть опция STARTUP , но мы для простоты внедрим её прямо в программу. Обычно подобные штуки называются crt0.o или Boot.o , на случай, если вы где-то на них наткнётесь. Наш код обязан начинаться с этого встроенного ассемблера, перед любыми включениями и тому подобным. DOS сделает за нас бóльшую часть установки, нам просто нужно перейти к точке входа.
.code16gcc сообщает ассемблеру, что мы собираемся работать в реальном режиме, так что он сделает правильную настройку. Несмотря на название, это не выдаст 16-битный код! Сначала вызывается функция dosmain , которую мы написали ранее. Затем он сообщает DOS с помощью функции 0x4C («закончить с кодом возврата»), что мы закончили, передавая код выхода в 1-байтовый регистр al (уже установленный функцией dosmain ). Этот встроенный ассемблер автоматически volatile , потому что не имеет входов и выходов.
Всё вместе
Вот вся программа на C.
Не буду повторять com.ld . Вот вызов GCC.
И его тестирование в DOSBox:

Тут если вы хотите красивой графики, то вопрос всего лишь в вызове прерывания и записи в память VGA. Если хотите звука, используйте прерывание PC Speaker. Я ещё не разобрался, как вызвать Sound Blaster. Именно с этого момента вырос DOS Defender.
Выделение памяти
Чтобы покрыть ещё одну тему, помните тот _heap ? Можем использовать его для реализации sbrk() и динамического выделения памяти в основном разделе программы. Это реальный режим и нет виртуальной памяти, поэтому можем писать в любую память, к которой мы можем обратиться в любой момент. Некоторые участки зарезервированы (например, нижняя и верхняя память) для оборудования. Так что реальной нужды в использовании sbrk() нет, но интересно попробовать.
Как обычно на x86, ваша программа и разделы находятся в нижней памяти (0x0100 в данном случае), а стек — в верхней (в нашем случае в районе 0xffff). В Unix-подобных системах память, возвращаемая malloc() , поступает из двух мест: sbrk() и mmap() . Что делает sbrk() , так это выделяет память чуть выше сегментов программы/данных, приращивая её «вверх» навстречу стеку. Каждый вызов sbrk() будет увеличивать это пространство (или оставлять его точно таким же). Данная память будет управляться malloc() и подобными.
Вот как можно реализовать sbrk() в программе COM. Обратите внимание, что нужно определить собственный size_t , потому что у нас нет стандартной библиотеки.
Он просто устанавливает указатель на _heap и увеличивает его по мере необходимости. Немного более умный sbrk() также будет осторожен с выравниванием.
В процессе создания DOS Defender произошла интересная вещь. Я (неправильно) посчитал, что память от моего sbrk() обнулилась. Так было после первой игры. Однако DOS не обнуляет эту память между программами. Когда я снова запустил игру, она продолжилась точно там, где остановилась, потому что те же структуры данных с тем же содержимым были загружены на свои места. Довольно прикольное совпадение! Это часть того, что делает забавной эту встроенную платформу.
Тема: Создание com/exe файла
Начал изучать Ассемблер . В учебнике прочитал небольшую иструкцию :
Допустим, вы создали в текстовом редакторе файл с именем PROG.ASM.
Если вы используете MASM 6.11—6.13, в командной строке необходимо указать следующее:
> ML.EXE PROG.ASM /AT
В результате будут созданы два файла: PROG.OBJ и PROG.COM. Файл PROG.OBJ, скорее всего, вам больше не понадобится, и его можно удалить, а PROG.COM можно запускать на выполнение. Это и будет машинный код ассемблерной программы. Параметр /AT указывает программе-ассемблеру (MASM), что после ассемблирования, в случае, если ошибок не будет обнаружено, следует запустить компоновщик (LINK.EXE) и передать ему параметры для создания файла типа COM.
Программу в формате asm создал , а что делать дальше ? Обьясните пожалуйста в какую именно командную строку это надо ввести , или как запустить MASM , в общем как создать exe/com файл ??