|
|||||||||||||||||||||||||||||||||||||||||||||
Автор: крис каперски ака мыщъх. Дата публикации: 21.02.2008
|
Сверхбыстрый импорт API-функцийОкруженный компьютерами, опутанный проводами, мыщъх сидел в глубине своей хакерской норы и точил зверский план, который должен был обогнать Microsoft и ведь обогнал! да еще как обогнал! скорость импорта возросла на порядок, отлично работая как на древней 9x, так и на новом Windows Server 2003, включая все промежуточные системы, причем без грамма ассемблерного кода! все на 100% Си! Введение Импорт API-функций "отъедает" существенный процент от общего времени загрузки исполняемых файлов и возникает естественное желание его сократить. Системный загрузчик крайне неэффективен и выполняет множество лишних проходов. Разбирая стандартную таблицу импорта, для каждой импортируемой функции он выполняет _полный_ _поиск_ соответствующего имени/ординала в таблице экспорта, не обращая внимания на то, что экспорт KERNEL32.DLL да и других системных библиотек упорядочен по алфавиту и, если таким же образом упорядочить импорт пользовательских программ, все API-функции можно слинковать за _один_ проход, используя минимум операций сравнения. В принципе, не заставляет нас пользоваться стандартным загрузчиком. Формат таблиц экспорта хорошо описан и при желании необходимые API-функции можно импортировать и "вручную". В частности, линкер ulink от Юрия Харона именно так и поступает, загружая необходимые ему API-функции по вышеописанному алгоритму (о чем подробно описывают "записки мыщъх’а" выложенные на ftp://nezumi.org.ru), однако, это еще не предел оптимизации и далеко не предел. Коварство и любовь от Microsoft Рассмотрим устройство стандартной таблицы импорта. На вершине иерархии находится структура Import Directory Table, представляющая собой массив структур IMAGE_IMPORT_DESCRIPTOR, завершаемых нулевым элементом. Каждый IMAGE_IMPORT_DESCRIPTOR содержит ссылки на две подчиненные структуры – lookup-таблицу, содержащую имена и/или ординалы импортируемых функций (Import Name Table), и таблицу импортируемых адресов (Import Address Table), так же известную как Thunk Table. В процессе загрузки файла сюда записываются эффективные адреса импортируемых функций. Обе таблицы представляют собой массив 32-битных элементов, индексы которых взаимно соответствуют друг другу. То есть, если необходимая нам функция some_func находится в i элементе lookup-таблицы, тогда (после загрузки файла в память) i-индекс таблицы импортируемых адресов будет содержать эффективный виртуальный адрес some_func.
Листинг 1 прототип структуры IMAGE_IMPORT_DESCRIPTOR До загрузки файла в память таблица импортируемых адресов дублирует lookup-таблицу, что (теоретически) позволяет загрузчику обходится одной лишь таблицей виртуальных адресов, избавляясь от прыжков по памяти, но практически он игнорирует ее. Создадим простейшую программу test.c и откомпилируем ее компилятором Microsoft Visual C++ с настройками по умолчанию.
Листинг 2 простейшая экспериментальная программа test.c Образовавшийся файл test.exe пропустим через утилиту dumpbin, входящую в состав MS VC (dumpbin /IMPORTS test.exe > out), и посмотрим, что хорошего она нам скажет:
Листинг 3 импорт нашей программы test.exe, выданный утилитой dumpbin Ага, таблица адресов располагается по адресу 405000h, а lookup-таблица — по 4054ACh. Заглянув туда hiew’ом мы увидим следующее:
Листинг 4 содержимое таблицы адресов — RVA адреса имен импортируемых функций
Листинг 5 содержимое lookup-таблицы — RVA адреса имен импортируемых функций Как видно, обе таблицы действительно полностью совпадают и указывают на массив имен/ординалов импортируемых функций:
Листинг 6 содержимое таблицы имен — имена импортируемых функций А теперь пропустим через dumpbin "Блокнот" из стандартной поставки NT (dumpbin /IMPORTS notepad.exe > out) и увидим в чем разница.
Листинг 7 импорт "Блокнота" от Microsoft’а Таблица адресов еще _до_ загрузки файла в память _уже_ содержит готовые эффективные виртуальные адреса! Если не верите — смотрите hiew’ом:
Листинг 8 содержимое таблицы адресов — эффективные виртуальные адреса импортируемых функций! Благодаря этой хитрости, системному загрузчику уже не нужно тратить время на импорт функций. Он просто смотрит на поле временной отметки (TimeDateStamp) импортируемой DLL и если оно совпадет с DLL, установленной на компьютере, реальный импорт _не_ производится. В противном случае, конечно, приходится напрягаться и тратить такты процессора на загрузку, но Microsoft обновляет свои прикладные приложения синхронно с обновлением системных библиотек, поэтому ее программы получают огромное преимущество над конкурентами. Какое коварство!!! Такая техника импорта функций называется биндингом (binding) и при желании может быть реализована с помощью утилиты editbin, позаимствованной все из того же компилятора (editbin /BIND test.exe). Посмотрим, что она сделала с нашим тестовым файлом? А сделала она с ним вот что:
Листинг 9 импорт нашей тестовая утилита после биндинга – RVA адреса имен API-функций сменились Эффективные виртуальные адресами самих API-функций Ура! Теперь и наша программа будет загружаться не хуже, чем у Microsoft!!! А вот и ни хрена подобного! Это на _вашей_ системе она будет загружаться "не хуже", а вот у большинства остальных пользователей временная отметка DLL наверняка не совпадет с вашей, и вся оптимизация пойдет насмарку, тем более, что Microsoft имеет тенденцию обновлять DLL не только с каждой версией операционной системы, но даже с установкой очередного Service Pack’а! Кажется, что ситуация ласты, но это не так... Как утереть нос Microsoft Самое простое решение, которое только приходит на ум — это тащить за собой editbin (благо лицензия этого вроде бы не запрещает) и делать биндинг непосредственно при установке программы. Не желающие связываться с Microsoft могут реализовать утилиту для биндинга самостоятельно или воспользоваться линкером ulink от Юрия Харона, который это тоже умеет и уж точно не имеет проблем с лицензированием. Но, прежде чем открывать пиво и праздновать победу, задумаемся: что произойдет если пользователь обновит систему после установки нашей программы? Правильно! Биндинг тут же перестанет работать, скорость загрузки упадет в разы, а это нехорошо. Можно, конечно, порекомендовать пользователю переустанавливать нашу программу после всякого обновления системы, но это не гуманно и вообще жестоко. Гораздо проще поступить так. Пусть при каждом запуске наша программа проверяет TimeDateStamp всех импортируемых DLL и если он изменился, запускает editbin (или другую утилиту) для ре-биндинга. Поскольку, править активный процесс нельзя, его необходимо завершить, породив перед этим дочерний субпроцесс или запустив bat-файл, который бы ре-биндил нашу программу и тут же перезапускал ее вновь, чтобы эти махинации протекали прозрачно для пользователя и не высаживали его на измену. Экстремальная оптимизация Дизассемблировав notepad.exe или наш оптимизированный test.exe, мы увидим, что все API-функции вызываются косвенным образом, что совсем не способствует производительности.
Листинг 10 косвенный вызов API-функций, сгенерированный компилятором Прямой call addr намного быстрее, чем call [addr] (особенно в циклах), так почему бы не извернуться и не "вживить" в программу эффективные адреса API-функций, определяемые на стадии установки через GetProcAddress (естественно, не забывая о контроле отметки времени). Ни одна из известных мыщъх’у утилит этого делать не умеет, поэтому приходится шевелить хвостом и кодить на Си самостоятельно. Разбирая таблицу импорта откомпилированной программы, находим все перекрестные ссылки на API-функции и если там будет FFh 15h XXh XXh XXh XXh (косвенный call) записываем поверх него EB YYh YYh YYh YYh 90h (непосредственный CALL + NOP; зачем нам нужен NOP? а затем, что непосредственный вызов на байт короче), где YYh YYh YYh YYh – относительный адрес API-функции, отсчитываемый от конца инструкции CALL) После этого выбрасываем таблицу импорта на хрен, оставляя лишь KERNEL32.DLL с единственной импортируемой функцией (неважно какой). Дело в том, что системный загрузчик Windows 2000 содержал ошибку и отказывался загружать программы, не импортирующие ни одной функции из KERNEL32.DLL, а, значит, не проецирующих ее на свое адресное пространство. Поскольку, сам загрузчик нуждался в KERNEL32.DLL, но забывал проверить: а была ли она вообще спроецирована или нет, приложения без таблицы импорта падали с исключением. В конечном счете, мы: а) сократим размер файла за счет отказа от таблицы импорта; б) ускорим загрузку файла; в) слегка оптимизируем вызов API-функций (впрочем, поскольку выполнение подавляющего большинства API-функций занимает существенное время, разница между прямым и косвенным вызовом будет не столь уж и заметной, однако, существуют API-функции содержащие всего несколько строк, например, GetLastError). Заключение Это только кажется, что Windows истоптана вдоль и поперек! На самом деле, потенциал оптимизации еще не исчерпан и творчески мыслящий программист всегда найдет неординарное решение, обгоняющее по скорости саму Microsoft!
|
|
| ||||||||||||||||||||||||||||||||||||||||||