Меня всегда раздражало, что я не могу запустить 64-битную игру на C# под MS-DOS. Сегодня я это исправил.
Исполняемые файлы под Windows состоят из двух частей:
В некотором смысле все .exe-файлы являются программами для DOS, но не делают ничего полезного. И вот однажды я нашел проект на Github, который заслуживает гораздо больше звезд, чем у него есть:
github.com/Baron-von-Riedesel/Dos64-stub
Dos64-stub — это маленькая программа, которая заменяет бесполезное сообщение «не может быть запущено под DOS» на загрузку Windows-секции исполняемого файла и «телепортацию» процесса в 21 век. Под «телепортацией» я подразумеваю настройку страничной памяти и перевод процессора в 64-битный режим («long»).
Для начала я взял свою игру «Змейка» под Windows, которую недавно ужал до 8 кб без зависимостей:
Конечно, в DOS недоступны привычные методы Windows API, поэтому пришлось переписать слой общения игры со внешним миром. Вот так выглядит теперь
Когда у нас есть количество тиков, можно сделать
А потом и
Ну а дальше все просто: компилятор C# -> компилятор CoreRT AOT -> линковщик. Мы указываем линковщику использовать Dos64-stub вместо того, чтобы генерировать бесполезный заголовок по умолчанию.
А вот и весь исходный код в виде пулл-реквеста.
Исполняемые файлы под Windows состоят из двух частей:
- Программа для DOS, которая выводит на экран «Данна программа не может быть запущена в режиме DOS»
- Заголовок исполняемого файла, который понимает Windows
В некотором смысле все .exe-файлы являются программами для DOS, но не делают ничего полезного. И вот однажды я нашел проект на Github, который заслуживает гораздо больше звезд, чем у него есть:
github.com/Baron-von-Riedesel/Dos64-stub
Dos64-stub — это маленькая программа, которая заменяет бесполезное сообщение «не может быть запущено под DOS» на загрузку Windows-секции исполняемого файла и «телепортацию» процесса в 21 век. Под «телепортацией» я подразумеваю настройку страничной памяти и перевод процессора в 64-битный режим («long»).
Для начала я взял свою игру «Змейка» под Windows, которую недавно ужал до 8 кб без зависимостей:
Конечно, в DOS недоступны привычные методы Windows API, поэтому пришлось переписать слой общения игры со внешним миром. Вот так выглядит теперь
Environment.TickCount
:public static unsafe long TickCount64
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
{
// Читаем область данных BIOS - счетчик прерываний 1AH.
// Да, мы просто обращаемся к памяти напрямую по некому "случайному" смещению.
// Область данных BIOS - это документированная структура,
// которую BIOS заполняет при старте компьютера.
// Приложения под DOS могут ее читать.
// По смещению 0x46C лежит прошедшее время в интервалах по 55мс.
uint timerTicks = * (uint *) 0x46C;
return (long) timerTicks * 55;
}
}
Когда у нас есть количество тиков, можно сделать
Thread.Sleep
:public static unsafe void Sleep(int delayMs)
{
// Помещаем на стек последовательность байтов: 0xF4 0xC3
// Эти байты соответствуют ассемблерным инструкциям:
//
// hlt
// ret
ushort hlt = 0xc3f4;
long expected = Environment.TickCount64 + delayMs;
while(Environment.TickCount64 < expected)
{
// Вызываем код, который мы положили на стек, чтобы ненадолго
// поставить процессор на паузу
// (Безопасники сейчас рыдают в углу)
ClassConstructorRunner.Call<int>(new IntPtr(&hlt;));
}
}
А потом и
Console.WriteLine
:public static unsafe void Write(char c)
{
byte* biosDataArea = (byte*)0x400;
// Находим начало VRAM, считывая данные из BIOS
byte* vram = (byte*)0xB8000;
if(*(biosDataArea + 0x63) == 0xB4)
vram = (byte*)0xB0000;
// Находим смещение активной видеостраницы
vram += * (ushort*)(biosDataArea + 0x4E);
// Транслируем символы юникода в символы хардварной кодировки IBM
byte b = c switch
{
'¦' => (byte)0xB3,
'-' => (byte)0xDA,
'¬' => (byte)0xBF,
'-' => (byte)0xC4,
'L' => (byte)0xC0,
'-' => (byte)0xD9,
_ => (byte)c,
};
// TODO: считать число колонок из BIOS
vram[(s_cursorY * 80 * 2) + (s_cursorX * 2)] = b;
vram[(s_cursorY * 80 * 2) + (s_cursorX * 2) + 1] = (byte)s_consoleAttribute;
// TODO: скроллить или переносить на другую сторону?
s_cursorX++;
}
Ну а дальше все просто: компилятор C# -> компилятор CoreRT AOT -> линковщик. Мы указываем линковщику использовать Dos64-stub вместо того, чтобы генерировать бесполезный заголовок по умолчанию.
А вот и весь исходный код в виде пулл-реквеста.
Siemargl
С появлением AOT наконец то первые шаги C# в эмбеддед (про интерпретатор в курсе — не в счет).
Не понял по исходникам про наличие управления памятью и String. Похоже, заглушки.
impwx Автор
В первой статье автора о том, как он упаковал змейку в 8кб, все рассказано — рантайм полностью вырезан, пользоваться reference-типами нельзя (т.е. только структуры, никаких аллокаций). Ограничения жесткие и далеко не любой код можно так зарефакторить. Однако змейка работает :)
dponyatov
Для embedded еще нужен hardRT, многозадачность, и полноценное управление динамической памятью, думаете есть шансы что кто-то сможет оторвать от модели языка C# сборщик мусора?
KvanTTT
Уже есть Zero Garbage Collector, который просто ничего не делает с мусором :)
kekekeks
Ну собственно в Unity вполне успешно оторвали для performance critical кода
Siemargl
Особых отличий не вижу