Сверхбыстрый импорт API-функций

         

Экстремальная оптимизация


Дизассемблировав notepad.exe или наш оптимизированный test.exe, мы увидим, что все API-функции вызываются косвенным образом, что совсем не способствует производительности.

.text:0040115F 68 FF 00 00 00            push   0FFh   ; uExitCode

.text:00401164 FF 15 08 50 40 00  call   ds:[ExitProcess]



Как утереть нос Microsoft


Самое простое решение, которое только приходит на ум— это тащить за собой editbin (благо лицензия этого вроде бы не запрещает) и делать биндинг непосредственно при установке программы. Не желающие связываться с Microsoft могут реализовать утилиту для биндинга самостоятельно или воспользоваться линкером ulink от Юрия Харона, который это тоже умеет и уж точно не имеет проблем с лицензированием.

Но, прежде чем открывать пиво и праздновать победу, задумаемся: что произойдет если пользователь обновит систему после установки нашей программы? Правильно! Биндинг тут же перестанет работать, скорость загрузки упадет в разы, а это нехорошо. Можно, конечно, порекомендовать пользователю переустанавливать нашу программу после всякого обновления системы, но это не гуманно и вообще жестоко. Гораздо проще поступить так.

Пусть при каждом запуске наша программа проверяет TimeDateStamp всех импортируемых DLL и если он изменился, запускает editbin (или другую утилиту) для ре-биндинга. Поскольку, править активный процесс нельзя, его необходимо завершить, породив перед этим дочерний субпроцесс или запустив bat-файл, который бы ре-биндил нашу программу и тут же перезапускал ее вновь, чтобы эти махинации протекали прозрачно для пользователя и не высаживали его на измену.



Коварство и любовь от Microsoft


Рассмотрим устройство стандартной таблицы импорта. На вершине иерархии находится структура ImportDirectory 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 {

       DWORD  Characteristics;     // 0 for terminating null import descriptor

       DWORD  OriginalFirstThunk;  // RVA to original unbound IAT

       };

       DWORD  TimeDateStamp;             // 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)

       DWORD  ForwarderChain;      // -1 if no forwarders

       DWORD  Name;

       DWORD  FirstThunk;          // RVA to IAT

} IMAGE_IMPORT_DESCRIPTOR;



прототип структуры IMAGE_IMPORT_DESCRIPTOR


До загрузки файла в память таблица импортируемых адресов дублирует lookup-таблицу, что (теоретически) позволяет загрузчику обходится одной лишь таблицей виртуальных адресов, избавляясь от прыжков по памяти, но практически он игнорирует ее.

Создадим простейшую программу test.c и откомпилируем ее компилятором Microsoft Visual C++ с настройками по умолчанию.

#include <stdio.h>

main()

{

       printf("hello, world!\n");

}



простейшая экспериментальная программа 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

             

              2DF    WriteFile

              174    GetVersion

              7D     ExitProcess

...



импорт нашей программы 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



содержимое таблицы адресов — 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



содержимое lookup-таблицы — RVA адреса имен импортируемых функций


Как видно, обе таблицы действительно полностью совпадают и указывают на массив имен/ординалов импортируемых функций:

.004056D8:DF 02 57 72-69 74 65 46 61 70 41 6C-6C 6F 63 00 -OWriteFile



содержимое таблицы имен — имена импортируемых функций


А теперь пропустим через 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

77E99F42      1EF  LocalUnlock

77E8B7F4      1AE  GlobalUnlock

77E8CCA3      1A7  GlobalLock



импорт "Блокнота" от 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



содержимое таблицы адресов — эффективные виртуальные адреса импортируемых функций!


Благодаря этой хитрости, системному загрузчику уже не нужно тратить время на импорт функций. Он просто смотрит на поле временной отметки (TimeDateStamp) импортируемой DLL и если оно совпадет с DLL, установленной на компьютере, реальный импорт _не_ производится. В противном случае, конечно, приходится напрягаться и тратить такты процессора на загрузку, но Microsoft обновляет свои прикладные приложения синхронно с обновлением системных библиотек, поэтому ее программы получают огромное преимущество над конкурентами. Какое коварство!!!

Такая техника импорта функций называется биндингом (binding) и при желании может быть реализована с помощью утилиты editbin, позаимствованной все из того же компилятора (editbin /BIND test.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

7944639C      2DF  WriteFile

79450D1D      174  GetVersion

794569BE      7D   ExitProcess



импорт нашей тестовая


Ура! Теперь и наша программа будет загружаться не хуже, чем у Microsoft!!! А вот и ни хрена подобного! Это на _вашей_ системе она будет загружаться "не хуже", а вот у большинства остальных пользователей временная отметка DLL наверняка не совпадет с вашей, и вся оптимизация пойдет насмарку, тем более, что Microsoft имеет тенденцию обновлять DLL не только с каждой версией операционной системы, но даже с установкой очередного Service Pack'а! Кажется, что ситуация ласты, но это не так...



косвенный вызов 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).



Сверхбыстрый импорт API-функций


крис касперски ака мыщъх, no-email

окруженный компьютерами, опутанный проводами, мыщъх сидел в глубине своей хакерской норы и точил зверский план, который должен был обогнать Microsoft и ведь обогнал! да еще как обогнал! скорость импорта возросла на порядок, отлично работая как на древней 9x, так и на новом Windows Server 2003, включая все промежуточные системы, причем без грамма ассемблерного кода! все на 100% Си!



существенный процент от общего времени


Импорт API-функций "отъедает" существенный процент от общего времени загрузки исполняемых файлов и возникает естественное желание его сократить. Системный загрузчик крайне неэффективен и выполняет множество лишних проходов. Разбирая стандартную таблицу импорта, для каждой импортируемой функции он выполняет _полный_ _поиск_ соответствующего имени/ординала в таблице экспорта, не обращая внимания на то, что экспорт KERNEL32.DLL да и других системных библиотек упорядочен по алфавиту и, если таким же образом упорядочить импорт пользовательских программ, все API-функции можно слинковать за _один_ проход, используя минимум операций сравнения.
В принципе, не заставляет нас пользоваться стандартным загрузчиком. Формат таблиц экспорта хорошо описан и при желании необходимые API-функции можно импортировать и "вручную". В частности, линкер ulink от Юрия Харона именно так и поступает, загружая необходимые ему API-функции по вышеописанному алгоритму (о чем подробно описывают "записки мыщъх'а" выложенные на ftp://nezumi.org.ru), однако, это еще не предел оптимизации и далеко не предел.

Это только кажется, что Windows


Это только кажется, что Windows истоптана вдоль и поперек! На самом деле, потенциал оптимизации еще не исчерпан и творчески мыслящий программист всегда найдет неординарное решение, обгоняющее по скорости саму Microsoft!