Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:c-tricks-iii-q-import [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== c-tricks-III-q-import ======
 +<​sub>​{{c-tricks-III-q-import.odt|Original file}}</​sub>​
 +
 +====== сверхбыстрый импорт API-функций ======
 +
 +крис касперски ака мыщъх, no-email
 +
 +**окруженный компьютерами,​ опутанный проводами,​ мыщъх сидел в глубине своей хакерской норы и точил зверский план, который должен был обогнать ****Microsoft**** и ведь обогнал! да еще как обогнал! скорость импорта возросла на порядок,​ отлично работая как на древней 9****x****, так и на новом ****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.
 +
 +typedef struct _IMAGE_IMPORT_DESCRIPTOR {
 +
 +union {
 +
 +DWORDCharacteristics;//​ 0 for terminating null import descriptor
 +
 +DWORDOriginalFirstThunk;//​ RVA to original unbound IAT
 +
 +};
 +
 +DWORDTimeDateStamp;//​ 0 if not bound,
 +
 +// -1 if bound, and real date\time stamp
 +
 +// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new)
 +
 +// O.W. date/time stamp of DLL bound to (old)
 +
 +DWORDForwarderChain;//​ -1 if no forwarders
 +
 +DWORDName;
 +
 +DWORDFirstThunk;//​ RVA to IAT
 +
 +} IMAGE_IMPORT_DESCRIPTOR;​
 +
 +Листинг 1 прототипструктуры IMAGE_IMPORT_DESCRIPTOR
 +
 +До загрузки файла в память таблица импортируемых адресов дублирует lookup-таблицу,​ что (теоретически) позволяет загрузчику обходится одной лишь таблицей виртуальных адресов,​ избавляясь от прыжков по памяти,​ но практически он игнорирует ее.
 +
 +Создадим простейшую программу test.c и откомпилируем ее компилятором Microsoft Visual C++ с настройками по умолчанию.
 +
 +#include <​stdio.h>​
 +
 +main()
 +
 +{
 +
 +printf("​hello,​ world!\n"​);​
 +
 +}
 +
 +Листинг 2 простейшая экспериментальная программа test.c
 +
 +Образовавшийся файл test.exe пропустим через утилиту dumpbin, входящую в состав MS VC (dumpbin /​IMPORTS test.exe >​ out),​ и посмотрим,​ что хорошего она нам скажет:​
 +
 +Dump of file test.exe
 +
 +KERNEL32.dll
 +
 +405000 Import Address Table
 +
 +4054AC Import Name Table
 +
 +0 time date stamp
 +
 +0 Index of first forwarder reference
 +
 +
 +
 +2DFWriteFile
 +
 +174GetVersion
 +
 +7DExitProcess
 +
 +...
 +
 +Листинг 3 импорт нашей программы test.exe, выданный утилитой dumpbin
 +
 +Ага, таблица адресов располагается по адресу 405000h, а lookup-таблица — по 4054ACh. Заглянув туда hiew'​ом мы увидим следующее:​
 +
 +.00405000:​D8 56 00 00-62 55 00 00-70 55 00 00-7E 55 00 00
 +
 +.00405010:​92 55 00 00-A6 55 00 00-C2 55 00 00-D8 55 00 00
 +
 +.00405020:​F2 55 00 00-0C 56 00 00-22 56 00 00-3A 56 00 00
 +
 +Листинг 4 содержимое таблицы адресов — RVA адреса имен импортируемых функций
 +
 +.004054AC:​D8 56 00 00-62 55 00 00-70 55 00 00-7E 55 00 00
 +
 +.004050DC:​92 55 00 00-A6 55 00 00-C2 55 00 00-D8 55 00 00
 +
 +.004050EC:​F2 55 00 00-0C 56 00 00-22 56 00 00-3A 56 00 00
 +
 +Листинг 5 содержимое lookup-таблицы — RVA адреса имен импортируемых функций
 +
 +Как видно, обе таблицы действительно полностью совпадают и указывают на массив имен/​ординалов импортируемых функций:​
 +
 +.004056D8:​DF 02 57 72-69 74 65 46 61 70 41 6C-6C 6F 63 00 ▀☻WriteFile
 +
 +Листинг 6 содержимое таблицы имен — имена импортируемых функций
 +
 +А теперь пропустим через dumpbin "​Блокнот"​ из стандартной поставки NT (dumpbin /​IMPORTS notepad.exe >​ out) и увидим в чем разница.
 +
 +KERNEL32.dll
 +
 +1001080 Import Address Table
 +
 +1006784 Import Name Table
 +
 +FFFFFFFF time date stamp
 +
 +FFFFFFFF Index of first forwarder reference
 +
 +77E99F421EF ​ LocalUnlock
 +
 +77E8B7F41AE ​ GlobalUnlock
 +
 +77E8CCA31A7 ​ GlobalLock
 +
 +Листинг 7 импорт "​Блокнота"​ от Microsoft'​а
 +
 +Таблица адресов еще _до_ загрузки файла в память _уже_ содержит готовые эффективные виртуальные адреса! Если не верите — смотрите hiew'​ом:​
 +
 +.010012D4:​22 6A AF 76-47 26 AF 76-9E DB AE 76-5F FC AF 76
 +
 +.010012E4:​32 6A AF 76-E2 16 AE 76-71 6F AF 76-C2 AC AF 76
 +
 +.010012F4:​9C 1D AF 76-00 00 00 00-00 00 00 00-00 00 00 00
 +
 +Листинг 8 содержимое таблицы адресов — эффективные виртуальные адреса импортируемых функций!
 +
 +Благодаря этой хитрости,​ системному загрузчику уже не нужно тратить время на импорт функций. Он просто смотрит на поле временной отметки (TimeDateStamp) импортируемой DLL и если оно совпадет с DLL, установленной на компьютере,​ реальный импорт _не_ производится. В противном случае,​ конечно,​ приходится напрягаться и тратить такты процессора на загрузку,​ но Microsoft обновляет свои прикладные приложения синхронно с обновлением системных библиотек,​ поэтому ее программы получают огромное преимущество над конкурентами. Какое коварство!!!
 +
 +Такая техника импорта функций называется **биндингом** (**binding**) и при желании может быть реализована с помощью утилиты editbin, позаимствованной все из того же компилятора (editbin /​BINDtest.exe). Посмотрим,​ что она сделала с нашим тестовым файлом?​ А сделала она с ним вот что:
 +
 +Dump of file test.exe
 +
 +KERNEL32.dll
 +
 +405000 Import Address Table
 +
 +4054AC Import Name Table
 +
 +44B17B02 time date stamp Mon Jul 10 01:54:10 2006
 +
 +13 Index of first forwarder reference
 +
 +7944639C2DF ​ WriteFile
 +
 +79450D1D174 ​ GetVersion
 +
 +794569BE7D ExitProcess
 +
 +Листинг 9 импорт нашей тестовая утилита после биндинга – RVA адреса имен API-функций сменились эффективные виртуальные адресами самих API-функций
 +
 +Ура! Теперь и наша программа будет загружаться не хуже, чем у Microsoft!!! А вот и ни хрена подобного! Это на _вашей_ системе она будет загружаться "не хуже",​ а вот у большинства остальных пользователей временная отметка DLL наверняка не совпадет с вашей, и вся оптимизация пойдет насмарку,​ тем более, что Microsoft имеет тенденцию обновлять DLL не только с каждой версией операционной системы,​ но даже с установкой очередного ServicePack'​а! Кажется,​ что ситуация ласты, но это не так...
 +
 +===== как утереть нос Microsoft =====
 +
 +Самое простое решение,​ которое только приходит на ум — это тащить за собой editbin (благо лицензия этого вроде бы не запрещает) и делать биндинг непосредственно при установке программы. Не желающие связываться с Microsoft могут реализовать утилиту для биндинга самостоятельно или воспользоваться линкером ulink от Юрия Харона,​ который это тоже умеет и уж точно не имеет проблем с лицензированием.
 +
 +Но, прежде чем открывать пиво и праздновать победу,​ задумаемся:​ что произойдет если пользователь обновит систему после установки нашей программы?​ Правильно! Биндинг тут же перестанет работать,​ скорость загрузки упадет в разы, а это нехорошо. Можно, конечно,​ порекомендовать пользователю переустанавливать нашу программу после всякого обновления системы,​ но это не гуманно и вообще жестоко. Гораздо проще поступить так.
 +
 +Пусть при каждом запуске наша программа проверяет TimeDateStamp всех импортируемых DLL и если он изменился,​ запускает editbin (или другую утилиту) для ре-биндинга. Поскольку,​ править активный процесс нельзя,​ его необходимо завершить,​ породив перед этим дочерний субпроцесс или запустив bat-файл,​ который бы ре-биндил нашу программу и тут же перезапускал ее вновь, чтобы эти махинации протекали прозрачно для пользователя и не высаживали его на измену.
 +
 +===== экстремальная оптимизация =====
 +
 +Дизассемблировав notepad.exe или наш оптимизированный test.exe, мы увидим,​ что все API-функции вызываются косвенным образом,​ что совсем не способствует производительности.
 +
 +.text:​0040115F 68 FF 00 00 00push0FFh; uExitCode
 +
 +.text:​00401164 FF 15 08 50 40 00callds:​[ExitProcess]
 +
 +Листинг 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!
 +
 +