В минувшие выходные я участвовал в Ludum Dare #31. Но даже до объявления тем конференции из-за своего недавнего увлечения я хотел сделать олдскульную игру под DOS. Целевой платформой выбрана DOSBox. Это самый практичный способ запуска DOS-приложений несмотря на то, что все современные процессоры x86 полностью обратно совместимы со старыми, вплоть до 16-битного 8086.
Я успешно создал и показал на конференции игру DOS Defender. Программа работает в реальном режиме 32-битного 80386. Все ресурсы встроены в исполняемый COM-файл, никаких внешних зависимостей, так что игра целиком упакована в бинарник 10 килобайт.
- https://github.com/skeeto/dosdefender-ld31
- DOSDEF.COM (10 КБ, v1.1.0, работает в DOSBox)
Для игры понадобится джойстик или геймпад. Я включил поддержку мыши в релиз для Ludum Dare ради презентации, но потом удалил её, потому что она не очень хорошо работала.
Наиболее технически интересная часть заключается в том, что для создания игры не понадобились никакие инструменты разработки DOS! Я использовал только обычный компилятор Linux C (gcc). В реальности даже нельзя собрать DOS Defender под DOS. Я рассматриваю DOS только как встроенную платформу, что и есть единственная форма, в которой DOS всё ещё существует сегодня. Вместе с DOSBox и DOSEMU это довольно удобный набор инструментов.
Графический интерфейс. Как создать программу на C++
Если вас интересует только практическая часть разработки, перейдите к разделу «Обманываем 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 asm volatile («mov $0x09, %%ahn» «int $0x21n» : /* no output */ : «d»(string) : «ah»); >
Код объявлен volatile , поскольку у него побочный эффект (печать строки). Для GCC ассемблерный код непрозрачен, и оптимизатор полагается на ограничения выхода/входа/клоббера (последние три строки). Для таких DOS-программ любой встроенный ассемблер будет с побочными эффектами. Это потому что он пишется не для оптимизации, а для доступа к аппаратным ресурсам и DOS — вещей, недоступных простому C.
Как создать МОБИЛЬНОЕ ПРИЛОЖЕНИЕ без программирования? ПОШАГОВЫЙ гайд.
Нужно также позаботиться о вызывающем операторе, потому что GCC не знает, что память, на которую указывает string , когда-либо читалась. Вероятно, массив, который поддерживает строку, тоже придётся объявить volatile . Всё это предвещает неизбежное: любые действия в такой среде превращаются в бесконечную борьбу с оптимизатором. Не все из этих битв можно выиграть.
Теперь к основной функции. Её название по идее не важно, но я избегаю называть её main() , потому что у MinGW есть забавные идеи, как обрабатывать конкретно такие символы, даже если его просят не делать этого.
int dosmain(void)
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.
-Wl,—nmagic,—script=com.ld
—nmagic отключает выравнивание страниц разделов. Во-первых, нам оно не требуется. Во-вторых, оно впустую отнимает драгоценное пространство. В моих тестах это не кажется необходимой мерой, но я на всякий случай оставляю эту опцию.
Параметр —script указывает, что мы хотим использовать особый скрипт компоновщика. Это позволяет точно разместить разделы ( text , data , bss , rodata ) нашей программы. Вот скрипт com.ld .
OUTPUT_FORMAT(binary) SECTIONS < . = 0x0100; .text : < *(.text); >.data : < *(.data); *(.bss); *(.rodata); >_heap = ALIGN(4); >
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 сделает за нас бóльшую часть установки, нам просто нужно перейти к точке входа.
asm («.code16gccn» «call dosmainn» «mov $0x4C, %ahn» «int $0x21n»);
.code16gcc сообщает ассемблеру, что мы собираемся работать в реальном режиме, так что он сделает правильную настройку. Несмотря на название, это не выдаст 16-битный код! Сначала вызывается функция dosmain , которую мы написали ранее. Затем он сообщает DOS с помощью функции 0x4C («закончить с кодом возврата»), что мы закончили, передавая код выхода в 1-байтовый регистр al (уже установленный функцией dosmain ). Этот встроенный ассемблер автоматически volatile , потому что не имеет входов и выходов.
Всё вместе
Вот вся программа на C.
asm («.code16gccn» «call dosmainn» «mov $0x4C,%ahn» «int $0x21n»); static void print(char *string) < asm volatile («mov $0x09, %%ahn» «int $0x21n» : /* no output */ : «d»(string) : «ah»); >int dosmain(void)
Не буду повторять com.ld . Вот вызов GCC.
gcc -std=gnu99 -Os -nostdlib -m32 -march=i386 -ffreestanding -o hello.com -Wl,—nmagic,—script=com.ld hello.c
И его тестирование в 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 , потому что у нас нет стандартной библиотеки.
typedef unsigned short size_t; extern char _heap; static char *hbreak = &_heap; static void *sbrk(size_t size)
Он просто устанавливает указатель на _heap и увеличивает его по мере необходимости. Немного более умный sbrk() также будет осторожен с выравниванием.
В процессе создания DOS Defender произошла интересная вещь. Я (неправильно) посчитал, что память от моего sbrk() обнулилась. Так было после первой игры. Однако DOS не обнуляет эту память между программами. Когда я снова запустил игру, она продолжилась точно там, где остановилась, потому что те же структуры данных с тем же содержимым были загружены на свои места.
Довольно прикольное совпадение! Это часть того, что делает забавной эту встроенную платформу.
- Assembler
- Разработка игр
- Компиляторы
Источник: habr.com
Глава 6 Программы в com-файлах
то в программе после оператора SEGMENT кодируется директива ORG 100H.
О б р а б о т к а. Для программ в EXE и COM форматах выполняется
ассемблирование для получения OBJ-файла, и компановка для получения
EXE-файла. Если программа создается для выполнения как EXE-файл, то ее уже
можно выполнить. Если же программа создается для выполнения как COM-файл,
то компановщиком будет выдано сообщение:
Warning: No STACK Segment
(Предупреждение: сегмент стека не определен)
Это сообщение можно игнорировать, так как определение стека в
программе не предполагалось. Для преобразования EXE-файла в COM-файл
используется программа EXE2BIN. Предположим, что EXE2BIN имеется на
дисководе A, а скомпонованный файл по имени CALC.EXE — на дисководе B.
Так как первый операнд всегда предполагает EXE файл, то можно не
кодировать тип EXE. Второй операнд может иметь другое имя (не CALC.COM).
Если не указывать тип COM, то EXE2BIN примет по умолчанию тип BIN, который
впоследствии можно переименовать в COM. После того как преобразование
будет выполнено можно удалить OBJ- и EXE-файлы.
Если исходная программа написана для EXE-формата, то можно, используя
редактор, заменить команды в исходном тексте для COM файла.
Программа EXCOM1, приведенная на рис.6.1, аналогична программе на
рис.4.3, но изменена согласно требований COM-формата. Обратите внимание на
следующие изменения в этой COM-программе:
— Стек и сегмент данных отсутствует.
— Оператор ASSUME указывает ассемблеру установить относительные
адреса с начала сегмента кодов. Регистр CS также содержит этот адрес,
являющийся к тому же адресом префикса программного сегмента (PSP).
Директива ORG служит для резервирования 100 (шест.) байт от
начального адреса под PSP.
— Директива ORG 100H устанавливает относительный адрес для
начала выполнения программы. Программный загрузчик использует этот
адрес для командного указателя.
— Команда JMP используется для обхода данных, определенных в
Ниже показаны шаги для обработки и выполнения этой программы:
MASM [ответы на запросы обычные]
LINK [ответы на запросы обычные]
DEL B:EXCOM1.OBJ,B:EXCOM1.EXE (удаление OBJ и EXE-файлов)
Размеры EXE- и COM-программ — 788 и 20 байт соответственно. Учитывая
такую эффективность COM-файлов, рекомендуется все небольшие программы
создавать для COM-формата. Для трассировки выполнения программы от начала
(но не включая) команды RET введите DEBUG B:EXCOM1.COM.
Некоторые программисты кодируют элементы данных после команд так, что
первая команда JMP не требуется. Кодирование элементов данных перед
командами позволяет ускорить процесс ассемблирования и является методикой,
рекомендуемой в руководстве по ассемблеру.
TITLE XCOM1 COM-программа для пересылки и сложения
CODESG SEGMENT PARA ‘Code’
ORG 100H ;Начало в конце PSP
BEGIN: JMP MAIN ;Обход через данные
FLDA DW 250 ;Определение данных
MOV AX,FLDA ;Переслать 0250 в AX
ADD AX,FLDB ;Прибавить 0125 к AX
MOV FLDC,AX ;Записать сумму в FLDC
RET ;Вернуться в DOS
Рис.6.1. Пример COM-программы.
СТЕК ДЛЯ COM-ПРОГРАММЫ
Для COM-файла DOS автоматически определяет стек и устанавливает
oдинаковый общий сегментный адрес во всех четырех сегментных pегистрах.
Если для программы размер сегмента в 64К является достаточным, то DOS
устанавливает в регистре SP адрес конца cегмента — шест.FFFE. Это будет
верх стека. Если 64К байтовый сегмент не имеет достаточно места для стека,
то DOS устанавливает стек в конце памяти. В обоих случаях DOS записывает
затем в стек нулевое слово.
Возможность использования стека зависит от размера программы и
ограниченности памяти. С помощью команды DIR можно определить pазмер файла
и вычислить необходимое пространство для стека.
Все небольшие программы в этой книге в основном расчитаны на
Несоблюдение хотя бы одного требования COM-формата может послужить
причиной неправильной работы программы. Если EXE2BIN обнаруживает oшибку,
то выдается сообщение о невозможности преобразования файла без указания
конкретной причины. Необходимо проверить в этом случае директивы SEGMENT,
ASSUME и END. Если опущен ORG 100H, то на данные в префиксе программного
сегмента будут установлены неправильные ссылки с непредсказуемым
результатом при выполнении.
При выполнении COM-программы под управлением отладчика DEBUG
необходимо использовать команду D CS:100 для просмотра данных и команд. Не
следует выполнять в отладчике команду RET; предпочтительнее использовать
команду Q отладчика. Некоторые программисты используют INT 20H вместо
Попытка выполнить EXE-модуль программы, написанной для COM-формата,
не имеет успеха.
ОСНОВНЫЕ ПОЛОЖЕНИЯ НА ПАМЯТЬ
— Объем COM-файла ограничен 64К.
— COM-файл меньше, чем соответствующий EXE-файл.
— Программа, написанная для выполнения в COM-формате не содержит
стека и сегмента данных и не требует инициализации регистра DS.
— Программа, написанная для выполнения в COM-формате использует
директиву ORG 100H после директивы SEGMENT для выполнения с адреса после
префикса программного сегмента.
— Программа EXE2BIN преобразует EXE-файл в COM-файл, обусловленный
указанием типа COM во втором операнде.
— Операционная система DOS определяет стек для COM-программы или в
конце программы, если позволяет размер, или в конце памяти.
ВОПРОСЫ ДЛЯ САМОПРОВЕРКИ
6.1. Каков максимальный размер COM-файла?
6.2. Какие сегменты можно определить в программе, которая будет
преобразована в COM-файл?
6.3. Как обходится COM-файл при выполнении с фактом отсутствия
6.4. Программа в результате компановки получала имя SAMPLE.EXE.
Напишите команду DOS для преобразования ее в COM-файл.
6.5. Измените программу из вопроса 4.6 для COM-формата, обработайте
ее и выполните под управлением отладчика DEBUG.
Источник: studfile.net
Пошаговое руководство. Создание объектов COM с помощью Visual Basic
При создании новых приложений или компонентов лучше всего создавать платформа .NET Framework сборки. Однако Visual Basic также упрощает предоставление платформа .NET Framework компонента com. Это позволяет предоставлять новые компоненты для более ранних наборов приложений, для которых требуются com-компоненты. В этом пошаговом руководстве показано, как использовать Visual Basic для предоставления платформа .NET Framework объектов в виде COM-объектов как с шаблоном класса COM, так и без нее.
Самый простой способ предоставить com-объекты — использовать шаблон класса COM. Этот шаблон создает новый класс, а затем настраивает проект для создания класса со слоем взаимодействия в качестве COM-объекта и регистрирует его в операционной системе.
Хотя вы также можете предоставить класс, созданный в Visual Basic, в качестве COM-объекта для использования неуправляемого кода, он не является истинным COM-объектом и не может использоваться в Visual Basic. Дополнительные сведения см. в разделе COM-взаимодействие в приложениях платформа .NET Framework.
Отображаемые на компьютере имена или расположения некоторых элементов пользовательского интерфейса Visual Studio могут отличаться от указанных в следующих инструкциях. Это зависит от имеющегося выпуска Visual Studio и используемых параметров. Дополнительные сведения см. в разделе Персонализация среды IDE.
Создание COM-объекта с помощью шаблона класса COM
- Откройте новый проект приложения Windows в меню Файл , выбрав Пункт Создать проект.
- В диалоговом окне Новый проект в поле Типы проектов проверка, что выбрано значение Windows. Выберите Библиотека классов в списке Шаблоны и нажмите кнопку ОК. Отобразится новый проект.
- Выберите Добавить новый элемент в меню Проект . Откроется диалоговое окно Добавление нового элемента.
- Выберите КЛАСС COM в списке Шаблоны и нажмите кнопку Добавить. Visual Basic добавляет новый класс и настраивает новый проект для COM-взаимодействия.
- Добавьте в com-класс код, например свойства, методы и события.
- Выберите Класс сборкиLibrary1 в меню Сборка . Visual Basic создает сборку и регистрирует COM-объект в операционной системе.
Создание COM-объектов без шаблона класса COM
Класс COM также можно создать вручную, а не с помощью шаблона класса COM. Эта процедура полезна при работе из командной строки или при большем контроле над определением COM-объектов.
Настройка проекта для создания COM-объекта
- Откройте новый проект приложения Windows в меню Файл , выбрав Пункт СоздатьПроект.
- В диалоговом окне Новый проект в поле Типы проектов проверка, что выбрано значение Windows. Выберите Библиотека классов в списке Шаблоны и нажмите кнопку ОК. Отобразится новый проект.
- В обозревателе решений щелкните правой кнопкой мыши на проект и выберите пункт Свойства. Отобразится конструктор проектов .
- Откройте вкладку Компиляция.
- Установите флажок Регистрация для проверка COM-взаимодействия.
Настройка кода в классе для создания COM-объекта
- В Обозреватель решений дважды щелкните Class1.vb, чтобы отобразить его код.
- Переименуйте класс в ComClass1 .
- Добавьте следующие константы в ComClass1 . Они будут хранить константы глобально уникального идентификатора (GUID), которые должны иметь COM-объекты.
Public Const ClassId As String = «» Public Const InterfaceId As String = «» Public Const EventsId As String = «»
Public Const ClassId As String = «2C8B0AEE-02C9-486e-B809-C780A11530FE»
Public Const InterfaceId As String = «3D8B5BA4-FB8C-5ff8-8468-11BF6BD5CF91» Public Const EventsId As String = «2B691787-6ED7-401e-90A4-B3B9C0360E31»
Примечание Убедитесь, что идентификаторы GUID являются новыми и уникальными; В противном случае компонент COM может конфликтовать с другими компонентами COM.
Public Class ComClass1
Public Sub New() MyBase.New() End Sub
Примечание Com-объекты, создаваемые с помощью Visual Basic, не могут использоваться другими приложениями Visual Basic, так как они не являются истинными COM-объектами. При попытке добавить ссылки на такие COM-объекты возникает ошибка. Дополнительные сведения см. в разделе Com-взаимодействие в приложениях платформа .NET Framework.
См. также раздел
- ComClassAttribute
- COM-взаимодействие
- Пошаговое руководство. Реализация наследования с использованием COM-объектов
- Директива #Region
- COM-взаимодействие в приложениях .NET Framework
- Устранение неполадок взаимодействия
Источник: learn.microsoft.com