Как увеличить ресурсы в десять раз
Ежели по тысяче в год,
то все-таки на триста лет мне хватит
к/ф «Женитьба Бальзаминова»
Прошу прощения за заголовок, похожий на желтые СМИ, и странный эпиграф, который я объясню ниже. Речь пойдет не о том, как увеличить скорость процессора или емкость диска на порядок, а всего лишь о разновидности данных, которые могут быть включены в исполняемый модуль формата EXE. Эти данные, на мой взгляд, не совсем удачно названы (или же зря буквально переведены) как «ресурсы».
Для тех, кто не интересовался подобными деталями, поясню, что формат, под привычной сейчас всем аббревиатурой EXE, в отличие от самого примитивного COM-формата (т.е. просто готового образа выполняемых команд), имеет внутри себя различные таблицы настроек. Главным образом, это было сделано для того, чтобы такой EXE-модуль можно было загружать в произвольное место памяти. Затем с помощью этих таблиц можно до собственно запуска программы настроить адреса команд и данных на нужные значения, если где-то применена абсолютная, а не относительная адресация.
В эпоху Windows EXE-формат еще усложнился, и закономерно появилась возможность хранить в нем как неотъемлемую часть не только команды и простые данные, но и, например, картинки или элементы интерактивного диалога. В самом деле, если Ваша программа рисует красивый курсор в виде какой-нибудь стрелочки «выточенной из стали», неудобно же таскать вместе с программой еще и отдельный файл с изображением этой стрелки. Гораздо удобнее поместить изображение прямо внутрь EXE-файла, указав, что это не просто картинка, а именно курсор. Кстати, при создании ярлыка программы, Windows ищет в ресурсах EXE-файла элемент типа «иконка» и высвечивает его как значок ярлыка по умолчанию.
Кроме всяких иконок, курсоров и прочих меню ресурсы EXE-файла могут содержать и просто «сырые» произвольные данные (их назвали RCDATA-ресурсы) в двоичном виде.
Именно возможностью хранить в EXE-файле произвольные данные и легко получать к ним доступ по имени и типу с помощью двух стандартных Windows API: FindResource и LoadResource, мы и решили воспользоваться.
Дело в том, что для работы нашей программы требуются небольшие по объему, но многочисленные файлы данных. В год их набегает свыше тысячи. И хотя триста лет как Миша Бальзаминов (см. эпиграф) мы жить не собираемся, но на тридцать лет эксплуатации программы все же приходится рассчитывать. При этом двадцать лет эксплуатации уже прошли и двадцать тысяч файлов уже накопилось. Хорошо было бы разместить все эти данные как ресурсы внутри самой программы и иметь такой архив всегда под рукой, а при очередной перетрансляции периодически добавлять к архиву новые элементы.
Поначалу задача казалось простой — на один-два дня работы. Создается примитивная программа, которая читает подряд все наши файлы данных, преобразуя их в один большой текстовый файл, где эти же данные записаны уже в формате тех самых RCDATA-ресурсов.
Вот фрагмент данных в формате RCDATA-ресурсов произвольного типа «BL». На самом деле так компактно записаны числа с плавающей точкой.
...
S10122201 BL
BEGIN
0x01AE 0x2D9F 0x083E
0x0C2B 0xB8DB 0xA406 0x3CDB 0x2F0B 0x3C7B 0x6C3E 0x45D4
0x2ADA 0x45AE 0xF083 0x3F1E 0xCBF1 0x430B 0xBBE7 0x4287
END
S10122301 BL
BEGIN
0x01AE 0x2DA0 0x084D
0xE974 0xB900 0xE158 0xBCDB 0xA0A3 0xBC69 0x6CC5 0x45D4
0x2AC1 0x45AE 0xF14E 0x3F1E 0xD92E 0x4306 0x9138 0xC2DE
END
...
Далее этот файл (с расширением .RC) пропускается через транслятор ресурсов RC.EXE и получается RES-файл, содержащий все эти же данные, но уже в двоичном виде. Теперь осталось только запустить редактор связей ILINK32, который склеит нашу программу и эти ресурсы в единый файл, и мы фактически бесплатно получаем простую и удобную для наших целей базу данных прямо внутри EXE-файла, т.е. при работе программы загружаемую вместе с ней в память. После этого уже не надо будет открывать файлы и читать их построчно, а вместо этого достаточно воспользоваться лишь двумя указанными Windows API для работы с ресурсами.
Однако на практике мы тут же получили сообщение от ILINK32 «Too many resources to handle», после чего желание использовать механизм ресурсов поубавилось. Лично меня подобные сообщения сильно раздражают. Ну ладно, в стародавние времена, когда ресурсы (в прямом значении этого слова) были ограничены, невозможно было тратить мегабайты направо и налево. Но сейчас-то! Суммарный объем наших данных едва превышает мегабайт. Неужели редактору связей нельзя обработать этот жалкий мегабайт именно так, как нам требуется? Он ведь может и гигабайты памяти себе выделять.
Первое, что мы попытались сделать — понять какой модуль виноват и найти его самую свежую версию. Может, рассосется все само собой… Увы, виноватого-то найти легко, собственно сообщение-то было не от самого ILINK32, а от вызываемого в нем «редактора связей ресурсов» RLINK32, но даже самая новая версия RLINK32.DLL, которую удалось найти (2009 год), не помогла. Не нашли мы и никаких подходящих ключей в бесчисленных настройках ILINK32.
И встал извечный вопрос подобных проектов: что делать? 1) Плюнуть на все, 2) городить какую-то свою «базу данных» или 3) все-таки заставить редактор связей проглотить наши 20 тысяч маленьких «ресурсиков»? Был выбран третий вариант: разобраться и исправить редактор связей.
Беда подобных подходов в том, что они требуют много времени, а в результате может ничего не получиться. И чем больше тратишь время, тем все более жалко бросить разбор, а с другой стороны понимаешь, что за этот же срок уже можно было бы сделать свое, а не разбираться в чужом. Но, тем не менее, процесс разбора пошел.
Пришлось выполнять ILINK32 по командам под WinDbg. Первый самый тоскливый шаг — выполнение по подпрограммам, т.е. по директиве отладчика «P» до тех пор, пока в консольном окне не появится то самое сообщение об ошибке. Адрес этой подпрограммы записывается, повторяется запуск программы с остановкой по данному адресу и директивой «T» спускаемся внутрь подпрограммы и опять шагами «P» ищем теперь уже вложенную подпрограмму, на которой выдается сообщение об ошибке на экран. И так много раз. Да, уж, увлекательное занятие, нечего сказать, на целый час, а то и на два.
Выполнение очередной подпрограммы по адресу 281557 привело к выдаче сообщения «Too many resources to handle»Сложность здесь в том, что в итоге, как правило, находится не сама подпрограмма, выдавшая ошибку, а всего лишь стандартная подпрограмма, выводящая строку на экран. Но и это уже хлеб. В данном случае повезло, что сразу обратили внимание на мелькнувшее странное число 1D90H.
Число 1D90H похоже на номер сообщения об ошибке, точнее на «handle» выдаваемой на экран строки.Было сделано правильное предположение, что это что-то вроде указателя выдаваемой строки, т.е. сообщения об ошибке. Ну, тогда поиск сразу ускоряется — просто ищем команду, включающую константу 1D90H, глобальным поиском и быстро ее находим:
mov ax,word ptr [RLINK32!__CPPdebugHook+0x23fa (00610520)]
cmp ax,word ptr [RLINK32!__CPPdebugHook+0x23fc (00610522)]
jb RLINK32!WriteResObj+0x336 (006024aa)
mov eax,1D90h
call RLINK32+0x1454 (00601454)
d 610522
00610522 bf 0c 00 00 00 00 00 00-00 00 00 00 00 00 00 00
Ага! А тогда выше — как раз условие, которое и приводит к ошибке. Там сравнивается очередной номер ресурса почему-то с пределом в 0CBFH. Что это за странное число и откуда оно взялось?
Оказывается, оно вот здесь получается делением 0FF00H (число в стеке) на 14H.
movzx eax,word ptr [esp]
mov ecx,14h
xor edx,edx
div eax,ecx
mov ecx,eax
mov word ptr [RLINK32!__CPPdebugHook+0x23fc (00610522)],cx
А вот и выделяемый объем памяти 0FF00H нашелся
mov ebx,0FF00h
push ebx
call RLINK32!RemoveFromExe+0xd60 (006052b0)
pop ecx
test eax,eax
je RLINK32+0x1282 (00601282)
mov word ptr [esi],bx
pop esi
Наконец-то ограничение стало понятно. Редактор ресурсов RLINK32 не просто слепляет файлы ресурсов в один ком, но и создает их общее содержание. Именно по этому содержанию ищется конкретный ресурс с помощью FindResource, когда в параметрах этого API указывается имя и тип. Каждый элемент содержания включает ссылки на имя, на тип и на собственно данные и занимает 20 байт. А выделяет RLINK32 для этого содержания внутри себя всего лишь 65280 (0FF00H) байт. Таким образом, RLINK32 может собрать не более 3264 ресурсов.
20 лет назад разработчики RLINK32 просто не подумали, что кому-то потребуется больше. Действительно, зачем держать в EXE-файле так много курсоров или иконок. Поэтому даже объем памяти для таблицы содержания задается двумя, а не четырьмя байтами.
Раз все стало понятно, можно исправлять коды RLINK32.DLL В команде
mov ebx,0FF00h
легко изменить константу, например
mov ebx,0AFF00h
и после этого допустимое количество ресурсов сразу увеличивается в 10 раз!
Кроме этого, далее придется исправлять объем памяти с двухбайтового числа на четырехбайтовое, т.е. команду
mov word ptr [esi],bx
придется исправить на
mov [esi],ebx
просто заменив префикс 066H на 090H
Наконец команду
movzx eax,word ptr [esp]
необходимо заменить на более короткую команду
move eax,[esp]
Вместо константы 2 байта везде стала константа в 4 байта и дело в шляпе.
Вот все, чем отличается исправленный файл RLINK32.DLL от исходного
fc /b rlink32.ddd rlink32.dll
Сравнение файлов rlink32.ddd и RLINK32.DLL
0000086F: 00 0A
0000087C: 66 90
00001B11: 0F 90
00001B12: B7 8B
Конечно, можно было бы не лезть в недра RLINK32, а все же поискать более современные средства работы с ресурсами, не имеющие такого глупого ограничения на общее число элементов.
Но, во-первых, еще неизвестно какие другие ограничения там всплывут. Ведь и древний RLINK32.DLL, казалось бы, не имел никакой объективной необходимости ограничиваться лишь тремя тысячами.
А, во-вторых, эти старые средства уже были много лет задействованы в нашем проекте и менять что-либо с риском внесения ошибок не хотелось.
Могут возразить, что при очередном добавлении ресурсов не RLINK32, так сам транслятор RC.EXE упрется в какое-нибудь ограничение. Однако в этом случае ресурсы можно разбивать на отдельные модули и транслировать их раздельно. Узкое место — это именно все объединяющий редактор связей.
Таким образом, потратив нескольких часов и изменив лишь 4 байта в древней библиотеке, мы теперь с уверенностью смотрим в будущее на ближайшие 10 лет, хотя бы с точки зрения размещения внутри нашей программы все новых и новых ресурсов. Поскольку до их допустимого теперь предела примерно в 36 тысяч элементов остается еще более 10 тысяч.