Как стать автором
Обновить

Комментарии 49

Почему Virtual Address (VA) начинается с 1000, а не с нуля я не знаю, но так делают все компиляторы, которые я рассматривал.

Видимо потому, что в ImageBase (в памяти) должен располагаться заголовок. В экзотических случаях заголовок может занимать две страницы (больше 4кб).

по адресу 0 ставят страницу с защитой от записи. чтобы ловить переход на null указатели.

В данном контексте (IMAGE_SECTION_HEADER.VirtualAddress) это RVA, то есть адрес относительно начала модуля в памяти

Когда-то (во времена MS DOS), насколько помню, от нуля до 0x1000 располагались векторы прерываний. А код — с адреса 0x1000. В .exe файлах, а были еще .com, у последних такого требования не было, и размер их был хоть единицы байт, но не более 64 КБ.

До 0x1000 лежала не только IVT, там ещё жила облать BDA (Bios Data Area), в которой были всякие настройки, конфигурация железа, буфер клавиатуры и прочее. А с COM файлами было всё просто — они не могли быть сегментированными, поэтому должны были влезать в один сегмент, размер которого и был 64кб.
Ну, был вариант линковки COM в два сегмента: 64к для кода и 64к для данных.
Чень, конечно, авторитет, но я помню, как в годах 1993-4 создавал com-файлы из двух сегментов с помощью Zortech C++ 3.1. Ну, может чуть позже, давно было, не помню. Это было расширением компилятора, но DOS такому не препятствовал, а c0.obj все-равно, чем DS инициализировать и что в этот сегмент грузить. Ограничение на размер выполнимого файла вроде не было, это просто по умолчанию утилита debug СS и DS инициализировала одинаковыми значениями. Так что если нет дальних вызовов и обращений к памяти — все будет работать: код в CS загрузит системный загрузчик, а данные в DS перенесет код инициализации.
Ну это уже из разряда Unreal Mode на x86 — не описан, но работает.
И описан, и работает. Дело обычное — на ассемблере такие com-файлы делать неудобно, бо надо самому инициализировать .data. Поэтому и не задумывались. А когда стали писать резиденты на C, и не только резиденты… Сначала буфера ввода-вывода, а затем и сегмент данных переехали в другой сегмент. Надобилось нечасто, но это была общеупотребительная практика. Время оставило только «официальную» документацию…
А что такое Unreal Mode на x86?
У него куча всяких других названий (Big Real, Flat Real). Это хитрый режим работы процессоров начиная с 80386, в котором процессор находится в 16 битном режиме, но ему доступны все 4 Гб памяти. Вход в него осуществляется переходом в защищённый режим с установкой сегмента размером в 4 Гб, на который ссылаются нужные сегментные регистры, после чего производится выход из защищённого режима. В результате получается казалось бы странная картина — нам в реальном режиме доступна вся память, надо только префикс 066h перед командами поставить (т.е. использовать всякие mov/push/pop и т.д. от защищённого режима). Работает за счёт того, что процессор для адресации использует теневые копии сегментных регистров и в них сохранены значения от защищённого режима. Но как только мы сделаем что-то типа pop ds, то по этому сегменту мы вылетим из нереального режима обратно в реальный. К примеру можно легко и непринуждённо прогрузить в память 20-30 мегабайт графики и потом спокойно использовать всё это в реальном режиме.
Вот скриншот из эмулятора:

Интересно, а под 6.22/7.0/7.1 досом оно вылетает также?
Сейчас перепроверил под PC-DOS 7.0 — да, так же.
Вы путаете красное с мягким. И .text, и .data + .bss + буфера ввода-вывода не могут каждый превышать 64к. Но код инициализации C может установить DS и загрузить туда .data и все остальное. После этого доступ к данным будет осуществляться по DS и все будет работать. А в лоб, конечно, не получиться.
Просто я так делал и это работало. Картинок, жаль не осталось, но тогда и социальных сетей не было…
Если у вас в COM ещё и секции были, то это точно был не DOS.
Вы еще скажите, что DOS-экстендеры 286 и 386 — не DOS. А теория Маркса правильна, потому что она верна, да.
Вы могли забыть, что у COM даже заголовка нет, он грузится в память просто сплошным куском.
Опс, покопался в старом, насчет .data и .bss я нафантазировал: в отдельный сегмент переносились буфера stdin/stdout и куча.
Покажите COM размером больше 64 КБ, который бы запускался под DOS.

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

Вы про размер файла или про объём используемой памяти после загрузки? Файлов больше 64 кб быть не может. А примеров программ, занимавших больше 64 кб памяти, у меня в избытке. Посмотрите скриншоты с графиками и фракталами в моём посте по ссылке — это программы в формате COM, запущенные в DOS Box. Раньше они запускались и в штатном NTVDM в Windows.
У настоящего COM-а нет сегментов вообще. Чисто в теории можно создать COM, который при открытии сам сделает себе сегменты и загрузит туда то, что надо, обойдя при этом загрузчик из состава XX-DOS (который поидее должен ругаться и посылать всё >64Kb COM). Но это будет такой-же .com файл, что и rar архив, переименованный в .jpg
Примерно это я и делал. Только обходить загрузчик для этого вовсе необязательно. После того, как он отработал, я волен распоряжаться сегментными регистрами по своему усмотрению. Ни NTVDM, ни DOS Box не жаловался. И вообще, странно предполагать, будто кто-то будет следить за моими манипуляциями с сегментными регистрами после завершения работы загрузчика.
Это да, но надо, чтобы изначальный размер был менее 64Кб, иначе DOS пошлёт, как было показано на скриншотах в комментариях. И можно дальше пойти по принципу «proga.com + proga.ovl», где .com будет простым мелким загрузчиком, который раскидает .ovl по памяти. Но возникает вопрос: если у нас нет чего-то типа CP/M86 — зачем заниматься извращениями с .com, если можно сделать .exe?
Это уже другой вопрос. Сначала — просто не хотелось заморачиваться с заполнением заголовков. Потом аппетиты выросли, но файл так и остался COM — однако уже многосегментный, да ещё и с 32-битным кодом (в смысле регистров, а не адресов). Затем родилась самокомпилируемая версия под Windows. А теперь — вообще новый проект с компиляцией в байт-код.
Хорошая статья, мне бы эта информация очень пригодилась когда я разбирался с этой темой для своего компилятора, разработка которого кстати ведется полностью на видео (на англ.).

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

Писать секции в файл не обязательно в этом порядке. Я в итоге сделал так что .data пишется первой и тогда всё расчитывается в один проход, просто RIP-относительный оффсет получится отрицательным.
Спасибо за интересную подсказку с одним проходом.

Порядок секций в файле может быть любым, но насколько я помню он должен совпадать с порядком в IMAGE_SECTION_HEADER (хотя вот это может быть и не критично), а вот что точно должно соблюдаться это порядок VA.

Если секция с VA = 0x2000 шла перед 0x1000 то exe переставал запускаться.
Спасибо за статью. Для полноты еще нужен свой лоадер ;)
Помнится, получалось и в 1кб ужаться, из функционала - только код возврата)
image

На просторах интернета находил и совсем фантастические варианты (97 байт?!), но такое запускается при ну очень специфических условиях
НЛО прилетело и опубликовало эту надпись здесь
Только что проверил на W10.
Файлы размером 1 Кб и 973 байта спокойно запускаются.
Минимальное значение выравнивания не мешает открывать файлы меньше этого значения :-)
НЛО прилетело и опубликовало эту надпись здесь
батюшки думал pe header в прошлом для хабира!
НЛО прилетело и опубликовало эту надпись здесь
На самом деле ещё меньше, 268 байт для современных Windows. Это будет файл, в котором и код, и данные, и заголовки PE-файла находятся вместе, в «нулевой» секции (там где обычно только заголовки).
Всё-таки создание EXE это задача линкера, а не компилятора.
Согласен, задача компилятора преобразовать исходный код в машинный, а задача линкера собрать из кусочков машинного кода исполняемый файл.

В моем компиляторе всего один файл который занимается и компиляцией и линковкой.
Поэтому везде и написано компилятор, хотя внутри у него есть отдельный модуль с названием Linker.
Наверное лучше всё таки разделить компиляцию и компоновку, это позволит проще (в плане масштабируемости и совместимости в будущем) решать проблемы размещения данных в секциях и определения смещения к ним в коде. Например, генерить relocation'ы смещений в секциях, которые компоновщик исправит потом на актуальные, как это сделано в объектниках COFF.
image
Конечно придётся ещё и таблицу символов эмитить, но кмк это в целом будет иметь бОльшую практическую пользу, чем генератор PE/ELF из хелоувордов. Реализация формата не сложна как со стороны компилятора, так и со стороны компоновщика, можно даже в сорсы llvm не лазить.
С этим уже можно подойти и к совместимости с другими инструментами. Сделать сначала свой компилятор, результаты его деятельности скармливать компоновщику из VS или LLVM. Потом и компоновщик свой, который сможет переварить OBJ от стороннего компилятора.
Кстати, сорсы NT'шного загрузчика вполне доступны для изучения — это как минимум избавит от вопросов о правилах размещения секций PE в АП.
НЛО прилетело и опубликовало эту надпись здесь
Спасибо, взял на заметку.
Действительно очень хорошие представления, что бы рассматривать их вместе с документацией.
Остаётся добиться того, чтобы полученным файлом был доволен не только загрузчик Windows, но и все антивирусы. Я в своём старом проекте так и не смог надёжно победить Windows Defender, и каждую неделю отправлял очередную жалобу в Microsoft. Кажется, с этим столкнулись практически все разработчики любительских компиляторов под Windows.

Ну кстати пока встроенный в windows 10 антивирус ничего не сказал.


Но помню что это было реальной проблемой когда рисовал через GDI, что на паскале, что на C++. Интересно, что достаточно было поменять 2 строки кода местами как антивирус переставал ругаться.

Думаю, из пары десятков программ он обязательно на что-то ругнётся. Я сначала искал проблему в заголовках, потом — в «любительских» конструкциях в самом машинном коде. Однако если Microsoft заявляет, что применяет машинное обучение для отлова подозрительных файлов, то задача разработчика компилятора серьёзно усложняется: может быть, теперь даже сами авторы антивируса не смогли бы сказать, на что именно антивирус ругается.
Иногда проблема касается и профессионалов: разработчики Go тоже жаловались.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории