Минимальная stub-программа — Архив WASM.RU

Все статьи

Минимальная stub-программа — Архив WASM.RU

"Stub" в переводе с английского - "пень, обломок, огрызок". Это в литературном переводе. В нелитературный перевод углубляться не будем. Скажем только, что это то самое, что показывает компьютер человеку, попытавшемуся запустить из-под DOS приложение, написанное для Windows. Обычно он показывает сообщение: "This program cannot be run in DOS mode". А вы что подумали?

В связи с понятием "stub-программа" обычно возникают три вопроса:

  1. Как она работает?
  2. Как сделать собственную stub-программу?
  3. Как уменьшить ее до размера, не раздражающего настоящего ассемблерщика?

Как она работает? Чтобы ответить на этот вопрос, следует заглянуть во внутренности формата PE-файла - стандартного исполняемого модуля Windows. Там мы обнаружим следующее:

  • PE-файл содержит в себе две программы, склеенные друг вслед за дружкой. Первая из них и есть stub-программа в обычном для MS-DOS exe-формате. Вторая - приложение для Windows.
  • stub-программа, как и положено в MS-DOS, начинается в файле с нулевого смещения и первые два байта представляют собой знаменитые инициалы MZ, коими увековечил себя кто-то из корифеев Microsoft (не могу навскидку вспомнить. Кто знает - напишите) (16.05.01 написали. Михаил Орлов сообщил, что это был Mark Zbikovski. Спасибо им обоим.) Можно сказать, вырезал свое имя на каждом пне. С этого имени начинается стандартный заголовок exe-файла MS-DOS.
  • windows-программа начинается с некоторого смещения, кратного параграфу (16 байт) и зависящему от размера stub-программы. Первые два байта windows-программы - это инициалы какого-то другого мужика со странным именем Portable Executable. Далее следует заголовок PE-файла и все остальные его элементы.
PE-формат - не единственный формат исполняемых модулей, который понимает Windows. Но мы не станем здесь рассматривать другие форматы, так как применительно к теме статьи это не имеет смысла.

Глупая DOS загружает exe-файл, обнаруживает в его начале привычный для себя заголовок и исполняет stub-программу, ничего не зная о наличии в файле еще чего-то, кроме слов "а пошел ты туда-то и туда-то".

Умная Windows поступает более изощренно:

  • по аббревиатуре MZ определяет наличие заголовка MS-DOS. Определив, продолжает процесс:
  • считывает двойное слово по смещению 3ch. Здесь компоновщик заботливо записал смещение PE-заголовка.
  • считывает PE-заголовок по указанному смещению и пытается выполнить его разбор. Если это удается - продолжает процесс загрузки приложения, который нас здесь больше не интересует.

Как сделать собственную stub-программу? Да очень просто! Напишите любую, сколь угодно сложную или простую DOS-программу. Только постарайтесь обойтись без извращений вроде перевода процессора в защищенный режим или показа картинок в режимах VESA: помните, что stub-программа должна сработать с флоппи-диска на 8086-й машине с текстовым монитором. Модель памяти можно использовать любую, кроме tiny - она для stub-программы не годится.

И это, к сожалению, неизбежно, как бы ни печалилась по этому поводу душа настоящего ассемблерщика. Ведь модель tiny порождает com-формат исполняемого модуля, не имеющий, как известно, никакого заголовка. А раз нет заголовка - то даже умная Windows не сможет узнать размер stub-программы и, соответственно, вычислить начало PE-заголовка. Глупая DOS ненамного легче переварит эту ситуацию. Обнаружив, что файл является исполняемым (имеет расширение exe, как положено в Windows), она выяснит также, что его фактический формат - com (отсутствует сигнатура MZ), и попытается-таки его загрузить и исполнить. Однако stub-программа сработает только в том случае, если размер исполняемого файла не превысит 64К, что для приложений Windows (даже написанных на ассемблере) - довольно редкое исключение. Скорее всего, ваш пользователь вместо цветного пожелания перейти, в конце концов, на новую ОС, получит сухое монохромное сообщение командного процессора о чрезмерном размере исполняемого модуля.

Важно! При компоновке stub-программы в командной строке компоновщика укажите недокументированную и загадочно звучащую опцию /KNOWEAS. Это опция вносит некие изменения в заголовок exe-файла. Если опцию не указать, то компоновка Windows-приложения со stub-программой окажется некорректна, о чем вы получите соответствующее предупреждение от линкера.

Подозреваем, что не все компоновщики поддерживают эту опцию. Однако link.exe из состава MS Developer Studio, конечно же, поддерживает.

Написанную stub-программу используйте на этапе компоновки своего Windows-приложения. Для этого в командную строку компоновщика добавьте опцию /STUB:"filename.exe", где filename.exe, как вы можете догадаться - это имя вашей stub-программы, при необходимости с путем. Обнаружив эту опцию, компоновщик заменит стандартную stub-программу на вашу.

Здесь настоящего ассемблерщика подстерегает серьезное разочарование. Проблема в следующем. Обычно при компоновке exe-программ для DOS в исполняемом файле резервируется довольно большое место под таблицу перемещения. Например, в порядке вещей, если таблица перемещения вольготно располагается между смещениями 1ch (конец заголовка) и 200h (начало сегмента кода). Даже если ваша stub-программа не содержит ни одного фрагмента, подлежащего перемещению, все равно 486 байт чистых нулей будет бессмысленно вбацано в нежное девственное тело вашего Windows-приложения. Задачка, стоит ли избегать такого варварского разбазаривания дискового пространства и как это сделать, предлагается для самостоятельного решения. Кое-какие идеи читайте далее.

Минимальная stub-программа. Заглянув внутрь исполняемого модуля какого-нибудь Windows-приложения, вы обнаружите, что стандартная stub-программа чаще всего занимает 128 байт. Можно ли уменьшить этот размер и до какой величины? Отвечаем: можно, до 64 байт.

Возьмите любой редактор бинарных файлов и создайте с его помощью вот такой файл:

000000   4D 5A 00 00 01 00 00 00  02 00 00 00 FF FF 00 00
000010   40 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00
000020   B4 4C CD 21 00 00 00 00  00 00 00 00 00 00 00 00
000030   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

Присвойте ему имя, например, stub.exe и используйте в качестве stub-программы.

Вот расшифровка содержимого файла stub.exe с комментариями:

смещение значение назначение комментарий
+0 5A4D Подпись exe-файла ('MZ')  
+2 0000 Длина последней неполной страницы образа, байт Игнорируется операционной системой
+4 0001 Длина образа, страниц (страница = 512 байт) Программа занимает менее одной страницы
+6 0000 Число элементов в таблице перемещения В этой программе перемещаемых элементов нет
+8 0002 Размер exe-заголовка, параграфов (параграф = 16 байт) Указывается размер базовой части заголовка. С учетом остальных значений параметров в данном случае означает, что исполняемый код в файле начинается со смещения 20h, и стартовый адрес находится в начале исполняемого кода
+0ah 0000 Минимум требуемой памяти за концом программы (параграфов) В данном случае смысла не имеет
+0ch FFFF Максимум требуемой памяти за концом программы (параграфов) Традиционно отводится вся доступная память
+0eh 0000 Сегментное смещение сегмента стека (для установки регистра ss)  
+10h 0040 Значение регистра sp (указателя стека) при запуске В этой программе стек не имеет значения
+12h 0000 Контрольная сумма исполняемого модуля Не используется
+14h 0000 Значение регистра ip (указателя команд) при запуске Стартовая точка совпадает с началом кодового сегмента
+16h 0000 Cегментное смещение кодового сегмента (для установки регистра cs)  
+18h 0040 Cмещение в файле 1-го элемента перемещения В этой программе ни одного элемента перемещения нет, а исполняемый код находится внутри заголовка, поэтому данное значение совпадает с концом программы
+1ah 0000 Номер оверлея Здесь не используется
+1eh 0000
0000
Резерв - 4 байта  
+20h 4CB4
21CD
Исполняемый код:
mov ah,4ch
int 21h
В данном случае программа просто возвращает управление операционной системе. Максимальный размер исполняемого кода для данных значений заголовка - 28 байт (смещения 20h...3bh). Обратите внимание, что исполняемый код с целью экономии размера программы находится в области, отведенной под exe-заголовок!
+3ch 0000
0000
Зарезервированное двойное слово Используется компоновщиком Windows-приложения для размещения смещения PE-заголовка

Вероятно, уменьшить размер stub-программы до величины, меньшей 64 байт, невозможно. А вот интересный вопрос: возможно ли вообще изъять stub-программу из модуля? Не спешите отвечать "нет"! 16-битные компоновщики от Microsoft имели в синтаксисе def-файла конструкцию вида STUB NONE, которая, согласно документации, предназначалась для использования при сборке dll-библиотек. В 32-битных компоновщиках эта возможность пропала. А жаль...

PS:

Вот уж в самом деле, "никогда не говори "никогда"! 24 июля 2001 года, в день падения 114-летнего рекорда жары в Москве, пришло письмо:

"На вашем сайте assembler.ru в статье "Минимальная stub-программа" автором упомянуто, что создать stub короче 40h байт, вероятнее всего, невозможно.

Привожу пример stub'а длиной в 20h байт:

00000000 4D 5A 00 00 01 00 00 00 01 00 00 00 FF FF 00 00 MZ..............
00000010 B4 4C CD 21 00 00 00 00 40 00 00 00 00 00 00 00 .L.!....@.......

В отличие от примера, приведенного в статье, размер заголовка уменьшен до одного параграфа. В терминах статьи это означает, что stub-код располагается по смещению 10h от начала файла.

Очевидно, после такого "сокращения" области заголовка от 10h и выше продолжают использоваться. Но в данном случае это неважно, так как на месте 4CB4h находится initial SP, а на месте 21CDh - checksum. Ни то, ни другое роли не играют. К сожалению, stub-код ограничен в 4 байта, потому что уже следующий word - initial IP.

Далее, в файле со stub'ом в 20h байт смещение 3Ch от начала файла приходится на смещение 1Ch PE-заголовка. Здесь расположен dword, который, по-видимому, ни на что не влияет, так что можно спокойно ставить туда 00000020h:

00000020 50 45 00 00 4C 01 07 00 21 B1 97 36 00 00 00 00 PE..L...!..6....
00000030 00 00 00 00 E0 00 0E 01 0B 01 04 14 20 00 00 00 ............ ...

Такую штуку вряд ли поддерживают компоновщики (мне от них этого добиться не удалось), я лично правил EXE вручную. Зато работает.

Напоследок - пример тоже рабочего stub'а длиной в 18h байт - меньше, по-моему, некуда:

00000000 4D 5A 00 00 01 00 00 00 00 00 00 00 B0 21 CD 29 MZ...........!.)
00000010 B4 4C CD 21 0C 00 00 00 50 45 00 00 4C 01 07 00 .L.!....PE..L...
00000020 21 B1 97 36 00 00 00 00 00 00 00 00 E0 00 0E 01 !..6............
00000030 0B 01 04 14 20 00 00 00 00 2E 00 00 18 00 00 00 .... ...........

Из-под DOS он выводит восклицательный знак (21h).

Ну как, размер достоин настоящего ассемблерщика?"

автор - Grief (soul_inspector@chat.ru)

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

  1. Когда делается попытка запустить программу из-под DOS, загрузчик, определив по сигнатуре "MZ", что это исполняемый файл, считывает по смещению 08h размер exe-заголовка, а по смещению 14h стартовое значение регистра ip, чтобы узнать, где находится исполняемый код. В предложенном Grief'ом варианте stub'а длиной 20h он таким образом выясняет, что передавать управление следует на смещение 10h (просто выход в DOS посредством функции 4Ch), а в варианте длиной 18h - на смещение 0Ch (здесь перед выходом успевает еще напечататься восклицательный знак). И неважно, что занятые исполняемым кодом байты на самом деле являются полями exe-заголовка "Максимум требуемой памяти за концом программы", "Сегментное смещение сегмента стека", "Значение регистра sp" и "Контрольная сумма исполняемого модуля". Все эти поля в данном случае не влияют работу программы.
  2. Если же программа запускается из-под Windows,то загрузчик, опять же разобравшись с назначением файла, первым делом считывает двойное слово по смещению 03ch, чтобы узнать, где находится PE-заголовок. Так вот, здесь это смещение оказывается внутри самого PE-заголовка! В варианте "18h" оно попадает на поле, содержащее суммарный размер секций неинициализированных данных в образе приложения, а в варианте "20h" - на поле, содержащее размер секций кода. Использование этих полей в современных нам версиях Windows неочевидно. По одним источникам - они используются для первичного отведения памяти под приложение. По другим - не используются вообще. Во всяком случае, практическая проверка показывает, что приложения с таким stub'ом работают нормально. (Что будет дальше - пожалуй, неизвестно даже Microsoft'у.)

Конечно же, заставить какой-нибудь компоновщик создать исполняемый файл с предлагаемыми stub'ами не удастся. Такие трюки придется делать вручную, с помощью какого-нибудь бинарного редактора: переместить образ приложения внутри файла на нужное смещение, отредактировать exe-заголовок, PE-заголовок, заголовки секций - ну, в общем, кто знает формат - тот справится. (А лучше доверить эту рутину какой-нибудь маленькой программуле.)

Ну и напоследок, опровергнув свои же слова что "...меньше некуда...", Grief прислал вообще шедевр - вполне работающий stub длиной всего 0Ch!!! Разобраться, как он устроен, вы сможете самостоятельно, руководствуясь уже достаточно ясной идеей: в заголовках полным-полно никому не нужных полей, которые мы можем использовать для своих целей. Итак:

00000000 4D 5A 00 00 01 00 00 00 01 00 00 00 50 45 00 00 MZ..........PE..
00000010 4C 01 07 00 08 00 00 00 B0 21 CD 29 B4 4C CD 21 L........!.).L.!
00000020 E0 00 0E 01 0B 01 04 14 20 00 00 00 00 2E 00 00 ........ .......
00000030 18 00 00 00 00 E0 00 00 00 10 00 00 0C 00 00 00 ................ 

2002-2013 (c) wasm.ru