Начало смотри тут.
Размер ехе файла 364 байта! Wow! Но это просто, если мы сделаем выход из программы ExitProcess и все... Если
раскомментировать показ MessageBoxa, то размер
файла будет 475 байт... Вот так вот!
Как же это работает? Давайте рассмотрим самые необычные участки кода... Во-первых, в GPA_proc используется блок данных PEB для поиска базы kernel32.dll, указатель на который находится в регистре fs по смещение 0x30... Об этом много у кого написано, в конце статьи есть ссылка на описание PEB структуры... Все остальное стандартно: переходим по e_lfanew, оказываемся в
РЕ заголовке kernel’a, там переходим к DataDirectory, в котором
хранятся указатели и размеры всех таблиц РЕ
(мы все это у себя сократили :)), первым в списке как раз таблица экспорта, к ней и переходим. Перебираем все, что в массиве, на который указывает AddressOfNames, сравнивая имена функций со строкой на которую указывает параметр. Если все сходиться, то переходим к массиву AddressOfNameOrdinals, с помощью которого ищем индекс массива AddressOfFunctions, в котором и находиться смещение функции относительно
kernel’a.
Это был контрольный вопрос, который нельзя было пропустить, ибо он занимает добрых 96 байт :). Теперь рассмотрим самое интересное, а именно заголовок.
Начнем с дос заглушки. Вот ее формат:
IMAGE_DOS_HEADER STRUCT
e_magic WORD ?
e_cblp WORD ?
e_cp WORD ?
e_crlc WORD ?
e_cparhdr WORD ?
e_minalloc WORD ?
e_maxalloc WORD ?
e_ss WORD ?
e_sp WORD ?
e_csum WORD ?
e_ip WORD ?
e_cs WORD ?
e_lfarlc WORD ?
e_ovno WORD ?
e_res WORD 4 dup(?)
e_oemid WORD ?
e_oeminfo WORD ?
e_res2 WORD 10 dup(?)
e_lfanew DWORD ?
IMAGE_DOS_HEADER ENDS
Ее размер – 0x40 и сократить его нельзя, ибо в самом конце находиться указатель на собственно
РЕ заголовок... Тем не менее, мы расположили там пару жизненно важных строк, как ты заметил :). Значение полей этого заголовка не важно, ибо они не обрабатываются, если обнулены, а если не обнулены, то обрабатываются только жизненно важные поля. Поэтому мы поместили туда только 2 строки. Где-то после этих строк есть что-то, что нельзя не обнулять, иначе загрузчик ругается, мол не Win32 приложение. Судя по всему таких полей несколько, потому что эксперименты по этому поводу потерпели полный крах :(. В течение всего исходного кода встречаются такие выражения, как например, вот это:
times 60-($-header) db 0
dd 0x00000040 ; e_lfanew
Для тех, кто не знает times – это эквивалент dup masm’авского синтаксиса в nasm’e... В таком выражении мы заполняем нулями весь остаток заголовка, кроме одного последнего поля (его смещение 0x3c = 60 ;))
В принципе после этого должен располагаться код дос приложения, которое выполниться если вдруг мы запустим прогу в дос’e :). Мы понимаем, что запускать win32 приложение в досе никто в здравом уме не станет, поэтому опускаем его, экономя несколько десятков байт... Сразу перейдем к ре заголовку. Вот его формат:
IMAGE_NT_HEADERS STRUCT
Signature DWORD ?
FileHeader IMAGE_FILE_HEADER <>
OptionalHeader IMAGE_OPTIONAL_HEADER <>
IMAGE_NT_HEADERS ENDS
Сигнатура всегда PE\0\0, далее идет файловый заголовок, вот его формат:
IMAGE_FILE_HEADER STRUCT
Machine WORD ?
NumberOfSections WORD ?
TimeDateStamp DWORD ?
PointerToSymbolTable DWORD ?
NumberOfSymbols DWORD ?
SizeOfOptionalHeader WORD ?
Characteristics WORD ?
IMAGE_FILE_HEADER ENDS
Больше всего нас интересует поле SizeOfOptionalHeader, в котором можно задать размер самого большого заголовка, который мы рассмотрим далее. Это поле ОБЯЗАТЕЛЬНО для заполнения нами! Остальные поля либо лучше обнулить, либо
это константы, которые можно выцепить из любого
РЕ файла с помощью, например, PETools... Посмотрим на опциональный заголовок, размер которого мы указали в прошлом заголовке
IMAGE_OPTIONAL_HEADER32 STRUCT
Magic WORD ?
MajorLinkerVersion BYTE ?
MinorLinkerVersion BYTE ?
SizeOfCode DWORD ?
SizeOfInitializedData DWORD ?
SizeOfUninitializedData DWORD ?
AddressOfEntryPoint DWORD ?
BaseOfCode DWORD ?
BaseOfData DWORD ?
ImageBase DWORD ?
SectionAlignment DWORD ?
FileAlignment DWORD ?
MajorOperatingSystemVersion WORD ?
MinorOperatingSystemVersion WORD ?
MajorImageVersion WORD ?
MinorImageVersion WORD ?
MajorSubsystemVersion WORD ?
MinorSubsystemVersion WORD ?
Win32VersionValue DWORD ?
SizeOfImage DWORD ?
SizeOfHeaders DWORD ?
CheckSum DWORD ?
Subsystem WORD ?
DllCharacteristics WORD ?
SizeOfStackReserve DWORD ?
SizeOfStackCommit DWORD ?
SizeOfHeapReserve DWORD ?
SizeOfHeapCommit DWORD ?
LoaderFlags DWORD ?
NumberOfRvaAndSizes DWORD ?
DataDirectory IMAGE_DATA_DIRECTORY \ IMAGE_NUMBEROF_DIRECTORY_ENTRIES dup(<>)
IMAGE_OPTIONAL_HEADER32 ENDS
Тут очень много очень важных полей, которые определяют работоспособность нашей будущей программы. Начнем с того, что тут есть очередной magic, неизвестно кому он нужен, правда :\ ... Но тем не менее, загрузчик ХР четко проверяет это поле и если оно не равно заданной константе, а именно 0х10b, то говорит, что наш фаил не является Win32 приложением... Несомненно важное поле SizeOfCode, которое определяет сколько будет
выделено памяти для секции кода при загрузки приложения. Это поле можно установить в большее реального размера секции значение, но нельзя в меньшее. AddressOfEntryPoint я определял
экспериментальным путем, ища свой код в отладчике... Тем не менее эту константу можно
рассчитать:
ep equ 0x10e0 ; Entry point
0x1000 – выровненная база нашего приложения относительно ImageBase (0x00400000)
0xe0 – размер всех заголовков вместе с описанием секции.
FileAlignment очень капризно воспринимается многими загрузчиками... Установим в минимальное значение 512 байт, да и этого не соблюдем... Если ХР позволяет, то все ОК 😉
ImageSize должен быть выровнен, но нам в принципе на него наплевать 🙂
Subsystem – очень важное поле, определяет что будет у нас за приложение – консольное или просто win32, есть куча констант на этот счет, которые можно посмотреть в любом справочнике РЕ формата...
Ну и, наконец, что же мы сократили – в самом конце (как будто специально для оптимизации 🙂 есть очень много ненужных таблиц, констант, которые являются буквально либо зарезервированными, либо просто неважными загрузчику. Ну, конечно, таблица импорта не так уж и
не нужна :), но мы компенсировали ее с помощью GPA_proc. Я сделал это специально, ибо неудобно в ручную добавлять записи в таблицу импорта, поэтому просто откажемся от статической линковки в силу динамической ;). Такой фокус не прокатит если вы делаете библиотеки, так как вам понадобиться как минимум таблица экспорта, а если вы еще и не уверены, что ваша библиотека будет загружена именно по ImageBase, то и таблица релоков.
Надеюсь, что это только начало и ты найдешь метод сделать самое маленькое
РЕ приложение еще более маленьким. А если у меня будет достаточно времени и если мне будет не лень :), то, возможно, я сделаю еще несколько статьей из этой же серии, но уже про оптимизацию под другие оси из линейки windows...
Хочу поблагодарить wizi (тестирование под XP sp1), Илью Варламова (тестирование под w2k) и Zero Ice’a (тестирование под XP sp0) за помощь.
Ссылки:
Описание PEB: http://jiurl.nease.net/document/JiurlPlayWin2k/PsPeb.htm
РЕ справочник: http://2ka.mipt.ru/~andrew/cgi-bin/bbs/formats/peformat.zip