Дизассемблировав 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]
Самое простое решение, которое только приходит на ум— это тащить за собой editbin (благо лицензия этого вроде бы не запрещает) и делать биндинг непосредственно при установке программы. Не желающие связываться с Microsoft могут реализовать утилиту для биндинга самостоятельно или воспользоваться линкером ulink от Юрия Харона, который это тоже умеет и уж точно не имеет проблем с лицензированием.
Но, прежде чем открывать пиво и праздновать победу, задумаемся: что произойдет если пользователь обновит систему после установки нашей программы? Правильно! Биндинг тут же перестанет работать, скорость загрузки упадет в разы, а это нехорошо. Можно, конечно, порекомендовать пользователю переустанавливать нашу программу после всякого обновления системы, но это не гуманно и вообще жестоко. Гораздо проще поступить так.
Пусть при каждом запуске наша программа проверяет TimeDateStamp всех импортируемых DLL и если он изменился, запускает editbin (или другую утилиту) для ре-биндинга. Поскольку, править активный процесс нельзя, его необходимо завершить, породив перед этим дочерний субпроцесс или запустив bat-файл, который бы ре-биндил нашу программу и тут же перезапускал ее вновь, чтобы эти махинации протекали прозрачно для пользователя и не высаживали его на измену.
Рассмотрим устройство стандартной таблицы импорта. На вершине иерархии находится структура 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;
До загрузки файла в память таблица импортируемых адресов дублирует lookup-таблицу, что (теоретически) позволяет загрузчику обходится одной лишь таблицей виртуальных адресов, избавляясь от прыжков по памяти, но практически он игнорирует ее.
Создадим простейшую программу test.c и откомпилируем ее компилятором Microsoft Visual C++ с настройками по умолчанию.
#include <stdio.h>
main()
{
printf("hello, world!\n");
}
Образовавшийся файл 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
...
Ага, таблица адресов располагается по адресу 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
.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
Как видно, обе таблицы действительно полностью совпадают и указывают на массив имен/ординалов импортируемых функций:
.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
Таблица адресов еще _до_ загрузки файла в память _уже_ содержит готовые эффективные виртуальные адреса! Если не верите — смотрите 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'а! Кажется, что ситуация ласты, но это не так...
Прямой 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).
крис касперски ака мыщъх, no-email
окруженный компьютерами, опутанный проводами, мыщъх сидел в глубине своей хакерской норы и точил зверский план, который должен был обогнать Microsoft и ведь обогнал! да еще как обогнал! скорость импорта возросла на порядок, отлично работая как на древней 9x, так и на новом Windows Server 2003, включая все промежуточные системы, причем без грамма ассемблерного кода! все на 100% Си!