Версия для печати
Шпаргалка по ресурсам Windows-32 (для Delphi)
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=60Серафим Бочкарев
дата публикации 15-07-1999 00:00Шпаргалка по ресурсам Windows-32 (для Delphi) Этот текст - попытка сжатого ответа на большинство заданных в конференции вопросов по ресурсам Windows. Возможно, Вы найдете здесь (в неявном виде) объяснение части связанных с ресурсами сложностей в Delphi.В тексте `Proj_L.Dpr` есть следующие примеры:
- Обычная коллекция строк в ini-файле. Это не имеет прямого отношения к ресурсам Windows. Я просто хочу напомнить что не меньше половины распространенных прикладных программ и игрушек держат сообщения в таком виде ( или в других собственных форматах ), чтобы лишный раз не возиться с компиляторами и линкерами ресурсов, UNICODE-шрифтами и кодовыми страницами.
- Встроенные ресурсы строк Delphi . В Delphi-4 есть дополнительный плюс - ее линкер умеет удалять из исполняемого файла объявленные, но неиспользуемые строки ресурсов, если они определены в секции resourcestring.
- Доступ к бинарным ( двоичным, произвольным пользовательским ) ресурсам через Windows-API .
- Доступ к бинарным ресурсам через класс VCL tResourceStream .
- Доступ к строковым ресурсам через Windows-API .
Внутренний формат ресурсов и пример DELPHI\DEMOS\RESXPLOR\*.*:
Очень короткое описание внутреннего формата ресурсов Windows. Вместо остсутствующих комментариев в примере viewer`а ресурсов из стандартной поставки Delphi. Заимствовано, большей частью, из Microsoft SDK.
Стандартная технология доступа к ресурсам
Для компиляции примера надо создать на диске перечисленные исходные файлы (все в текстовом формате). Я не привел примеров для ресурсов типа BitMap`ов, Icon`ов и курсоров, поскольку обращения к ним достаточно тривиальны и не содержат каких-либо неоднозначностей, и, во-вторых, они ( декларации ресурсов ) недостаточно компактно записываются в виде текста.
Файл `#_Msg.Ini`
Список строк в текстовом файле
msgHello= Здавствуйте ! msgBye= До свидания ...Файл `#_Msg.RC`
Скрипт компилятора ресурсов. В двоичном ресурсе с именем RC1 записана ASCIIz-строка `QWERTY`.
RC1 RCDATA { '51 57 45 52 54 59 00' } STRINGTABLE { 1000, "Здравствуйте ." 1001, "До свидания ..." }Файл `Proj_L.Dpr`:
Мы используем Delphi как линкер, чтобы дописать стандартный заголовок исполняемых файлов Windows к файлу `#_Msg.Res`. Последний делается компилятором ресурсов из скрипта `#_Msg.RC`. IDE может ругаться при загрузке этого проекта из-за отсутствия секции `uses` - дура.
{$IMAGEBASE $40000000} {$APPTYPE CONSOLE} library Proj_L; {$R #_MSG.RES} BEGIN END.Файл `Make_DLL.Bat`:
Компилируем скрипт `#_Msg.RC` в файл `#_Msg.Res`; компилируем и линкуем проект `Proj_L.Dpr`. Получаем файл `Proj_L.Dll`.
rem -- may be used BRC32 or BRCC32 rem c:\del3\bin\brc32 -r #_msg.rc c:\del3\bin\brcc32 #_msg.rc c:\del3\bin\dcc32 /b proj_l.dpr pauseФайл `Proj.Dpr`
Замечания:{$APPTYPE GUI} {$D+,O-,S-,R-,I+,A+,G+} {$IfOpt D-} {$O+} {$EndIf} program Proj; {$IfNDef WIN32} error: it works only under Win32 {$EndIf} uses Windows, SysUtils, Classes; {//////////////////////////////////////////////} procedure i_MsgBox( const ACap,AStr:String ); { service routine: simple message-box } begin Windows.MessageBox( 0, pChar(AStr), pChar(ACap), MB_OK or MB_ICONINFORMATION ); end; {///// TestSList ////} procedure TestSList; { load strings from ini-file via tStringList } const cFName = '#_MSG.INI'; var qSList : tStringList; begin qSList := tStringList.Create; with qSList do try LoadFromFile( ExtractFilePath(ParamStr(0))+cFName ); i_MsgBox( 'strings collection via VCL:', Trim(Values['msghello'])+#13+Trim(Values['MSGBYE']) ); finally Free; end; end; {//// TestBuiltInStrRes ////} RESOURCESTRING sMsgHello = 'ЯВЕРТЫяверты'; sMsgBye = 'явертыЯВЕРТЫ'; procedure TestBuiltInStrRes; { load strings from resources via Delphi`s Linker } begin i_MsgBox( 'built-in string resources:', sMsgHello+#13+sMsgBye ); end; {//////////////////////////////////////////////} type tFH_Method = procedure( AFHandle:tHandle ); { `AFHandle` must be a handle of instance of image (of memory-map) of a PE-file (EXE or DLL) } procedure i_Call_FH_Method( AProc:tFH_Method ); { it is wrapper to load and free a instance of binary file with resource; also it calls to "AProc()" with given instance-handle } const cLibName = 'PROJ_L.DLL'; var qFHandle : tHandle; begin qFHandle := Windows.LoadLibrary( pChar(ExtractFilePath(ParamStr(0))+cLibName) ); if qFHandle=0 then i_MsgBox( 'Error loading library', Format('Code# %xh',[Windows.GetLastError]) ) else try AProc( qFHandle ); finally Windows.FreeLibrary( qFHandle ); end; end; {//// TestBinRes_WinAPI ////} procedure TestBinRes_WinAPI( AFHandle:tHandle ); { loading binary resource via usual windows-API } var qResH, qResInfoH : tHandle; begin qResInfoH := Windows.FindResourceEx( AFHandle , RT_RCDATA, 'RC1', 0 ); qResH := Windows.LoadResource( AFHandle, qResInfoH ); try i_MsgBox( 'binary resource (Win API):', pChar(Windows.LockResource(qResH)) ); finally Windows.FreeResource( qResH ); end; end; {//// TestBinRes_VCLStream ////} procedure TestBinRes_VCLStream( AFHandle:tHandle ); { loading binary resource via VCL`s stream } var qResStream : tResourceStream; begin qResStream := tResourceStream.Create( AFHandle, 'RC1', RT_RCDATA ); try i_MsgBox( 'binary resource (VCL stream):', pChar(qResStream.Memory) ); finally qResStream.Free; end; end; {//// TestStrRes_WinAPI ////} procedure TestStrRes_WinAPI( AFHandle:tHandle ); { loading string resource via usual windows-API } const cBufSize = 512; var qBuf : array[0..1,0..cBufSize-1]of Char; begin Windows.LoadStringA( AFHandle, 1000, qBuf[0], cBufSize ); Windows.LoadStringA( AFHandle, 1001, qBuf[1], cBufSize ); i_MsgBox( 'string resources (Win API):', StrPas(qBuf[0])+#13+StrPas(qBuf[1]) ); end; BEGIN TestSList; TestBuiltInStrRes; i_Call_FH_Method( TestBinRes_WinAPI ); i_Call_FH_Method( TestBinRes_VCLStream ); i_Call_FH_Method( TestStrRes_WinAPI ); END.
- Rесурсы частично вынесены во внешнюю DLL только для демонстрации, поскольку большинство вопросов в конференции подразумевает именно такое их использование.
- Если ресурсы слинкованы не в отдельную DLL, а в исполняемый файл проекта, в параметре AFHandle надо везде передавать `0` или значение переменной System.HInstance.
- Вместо функции Windows.FindResource() я предпочитаю FindResourceEx() с лишним явным параметром - `LanguageId`. Дело в том, что первая не всегда находит ресурсы, сделанные борландовскими компиляторами - семантика LanguageId по умолчанию определена MS не совсем однозначно.
- Для однозначности, я явно указал имя функции Windows.LoadStringA(). В NT работает еще функция LoadStringW(), которая возвращает строки UNICODE. В Win95 LoadStringW() возвращает код ошибки `not implemented`.
Внутренний формат ресурсов Windows.
В каталоге DELPHI\DEMOS\RESXPLOR есть пример работы с ресурсами Windows на самом `фундаментальном` уровне - непосредствено с форматом PE COFF ( Portable Executable Common Object File Format ) для Win32. Данный раздел написан, в основном, для тех, кто захочет разобраться в этом стандартном примере Delphi.
Сами по себе ресурсы - индексированный набор данных с записями переменной длины. Чтобы конкретную запись ресурса можно было найти, у нее есть один из двух идентификаторов - имя (строка символов UNICODE) или целое число. Целыми числами идентифицируются, например, каталоги стандартных типов ресурсов и строки в таблицах. Большинство записей ресурсов стандартных типов идентифицируются именами. Практически, в именах ресурсов разумно использовать только подмножетсво стандартных символов ASCII (коды от 0 до 255). Описание стандартных типов ресурсов Windows можно посмотреть в on-line help`е любой IDE C или Delphi. Любопытно, что способ идентификации ресурса ( целое число или ссылка на имя ) специфицирован, скорее, не на уровне стандарта, а на уровне принятых соглашений. Для поиска ресурса мы, в общем случае, задаем три параметра:
Тип - один из стандартных кодов типа ресурса. В вызовах API это может быть либо адресом строки, содержащей одно из стандартных имен, либо - одна из констант RT_xxx из DELPHI\SOURCE\RTL\WIN\WINDOWS.PAS.
Идентификатор. В зависимости от типа ресурса, это может быть целое число или имя.
Язык ресурса. Кодируется целым числом.
Формат ресурсов PE COFF ориентирован чтобы:
- максимально быстро находить нужный ресурс по указаным трем параметрам,
- расположить ресурсы достаточно компактно,
- переносить скомпилированные ресурсы между процессорами с разными правилами адресации.Далее используется термин RVA (relative virtual address), я его поясню. Все адреса в защищенных многозадачных системах (не только на x286..586) обычно делаются `виртуальными`: То есть, пользовательское приложение не должно иметь шанс узнать что-либо о физических адресах - иначе оно теоретически может разрушить любую защиту операционной системы. В Windows строгой защиты в этом смысле нет, но есть еще одна причина `виртуальности` адресов - динамическая загрузка/выгрузка данных из ОЗУ на диск для организации виртуальной памяти. Процессор аппаратно, `на лету`, транслирует виртуальные адреся в физические по таблицам, созданным ядром операционной системы.
Теперь о слове `relative`. Операционной системе, по большому счету, без разницы, какой именно виртуальный адрес дать первому байту образа исполняемого файла в ОЗУ. А линкеру и самой программе, в ряде случаев, удобнее работать с конкретным значением. Оно называется `ImageBase`; линкер записывает его в заголовке PE-файла. По техническим причинам, оно не может быть произвольным для Windows-программ. В Delphi есть директива `{$ImageBase ...}`. Так вот, RVA объекта - это его смещение относительно значения `ImageBase`. Обычный адрес объекта (он, кстати, тоже виртуальный) есть сумма значений глобальной переменной `ImageBase` и `RVA` данного объекта.
В тексте использована ассемблерная мнемоника: `DD` и `DW` (Define Double и Define Word), что означает, соответственно, 32- и 16-разрядное слово. Символ `|` означает `или`, `либо`.Описание формата ресурсов в MS PE COFF.
Я делаю сокращенное изложение фрагмента документации PE COFF. Я полагаю, этого более-менее достаточно, чтобы разобраться, при желании, с текстом примера Delphi. Файл PE.TXT ( author Micheal J. O'Leary ) взят из документации Microsoft C. Он же входит в MS Software Developers Kit (SDK) и в комплект поставки большинства компиляторов C для Win32. Если Вам интересно положение корневого каталога ресурсов в заголовке PE COFF или более подробный формат заголовка - можно смотреть исходные тексты проекта проекта RSEXPLOR или, разумеется, сам первоисточник - PE.TXTРесурсы индексированы как многоуровневое двоичное дерево. Технологически возможно 2**31 уровней, но в Windows стандартно используются только три: первый - TYPE (тип), далее - NAME (имя), далее - LANGUAGE (язык). Ресурсы должны быть отсортированы по определенным правилам - для ускорения поиска.
Типичное расположение ресурсов в файле: сначала лежит `RESOURCE DIRECTORY` (каталог/каталоги ресурсов), затем - `RESOURCE DATA` (собственно данные ресурсов).
Каталог ресурсов довольно похож, по структуре, на каталоги дисков. Он содержит записи (`DIR ENTRIES` - см. далее), которые указывают либо на ресурсы, либо на другие каталоги (точнее - подкаталоги) ресурсов. В отличие от дисков, сами данные не разносятся по кластерам, а наоборот - их стараются плотнее прижать друг к другу, поскольку никто не собирается вставлять туда дополнительные данные после сборки (линковки) исполняемого файла.Каталог ресурсов начинается с заголовка (четыре 32-битных слова):
DD RESOURCE FLAGS DD TIME/DATE STAMP DW MAJOR VERSION, DW MINOR VERSION DW # NAME ENTRY, DW # ID ENTRY декларация в RXTypes.Pas: IMAGE_RESOURCE_DIRECTORY = packed record Characteristics : DWORD; TimeDateStamp : DWORD; MajorVersion : WORD; MinorVersion : WORD; NumberOfNamedEntries : WORD; NumberOfIdEntries : WORD; end;Здесь важны два поля: `# NAME ENTRY` - число точек входа, имеющих имена, и `# ID ENTRY` - число точек входа, имеющих вместо имен целочисленные идентификаторы.
За заголовком следует массив из записей `RESOURCE DIR ENTRIES` (точек входа каталога). Там лежат `# NAME ENTRY`+ `# ID ENTRY` записей типа `DIR ENTRY`. Формат записи `DIR ENTRY` - два 32-битных слова:DD NAME RVA | INTEGER ID DD DATA ENTRY RVA | SUBDIR RVA декларация в RXTypes.Pas: IMAGE_RESOURCE_DIRECTORY_ENTRY = packed record Name: DWORD; // Or ID: Word (Union) OffsetToData: DWORD; end;Первое поле содержит либо `NAME RVA` - адрес строки (UNICODE) с именем, либо - `INTEGER ID` - целочисленный идентификатор. `INTEGER ID` может быть, например, одним из стандартных кодов типа ресурса или заданным пользователем кодом строки в таблице строк.
Самый старший бит второго поля (31-й бит) называется `Escape-флагом`. Если он установлен в `1`, считается что данная `DIR ENTRY` - ссылка на другой подкаталог ресурсов. Если сброшен в `0` - данная запись ссылка на данные ресурса. Понятно, при вычислении адреса этот бит всегда должен считаться `0`.
Строка, на которую указывает `NAME RVA`, очень похожа на паскалевскую short-string, только вместо байтов она состоит из 16-битные слов. Самое первое слово - длина строки, за ним лежат 16-битные символы UNICODE. Физически линкер кладет эти строки переменной длиины между каталогами и собственно данными ресурсов.
Понятно, что `SUBDIR RVA` указывает на совершенно аналогичную таблицу подкаталога.
`DATA ENTRY RVA` указывает на запись `RESOURCE DATA ENTRY` такого вида:DD DATA RVA DD SIZE DD CODEPAGE DD RESERVED декларация в RXTypes.Pas: IMAGE_RESOURCE_DATA_ENTRY = packed record OffsetToData : DWORD; Size : DWORD; CodePage : DWORD; Reserved : DWORD; end;`DATA RVA` - адрес бинарных данных, `SIZE` - их размер. `CODEPAGE` (кодовая страницa) обычно имеет снысл только для строковых ресурсов. Оговаривается, что в Win32 это должна быть одна из стандартных страниц UNICODE. Сами бинарные данные могут жить либо прямо за полем `RESERVED`, либо где-то в другом месте - смотря куда линкер их положит.
Дамп памяти (взят из PE.TXT).
Далее я привожу целиком фрагмент файла PE.TXT. Это - конкретный пример размещения ресурсов с подробным дампом памяти.
The following is an example for an app. which wants to use the following data as resources:
Напомню, что весь этот текст имеет чисто утилитарный смысл - пояснение конкретного стандартного примера Delphi. Полагаю, произвольное изложение на русском языке не затрагивает авторские права Microsoft на описание COFF. Вы можете использовать этот текст по своему усмотрению.TypeId# NameId# Language ID Resource Data 00000001 00000001 0 00010001 00000001 00000001 1 10010001 00000001 00000002 0 00010002 00000001 00000003 0 00010003 00000002 00000001 0 00020001 00000002 00000002 0 00020002 00000002 00000003 0 00020003 00000002 00000004 0 00020004 00000009 00000001 0 00090001 00000009 00000009 0 00090009 00000009 00000009 1 10090009 00000009 00000009 2 20090009 Then the Resource Directory in the Portable format looks like: Offset Data 0000: 00000000 00000000 00000000 00030000 (3 entries in this directory) 0010: 00000001 80000028 (TypeId #1, Subdirectory at offset 0x28) 0018: 00000002 80000050 (TypeId #2, Subdirectory at offset 0x50) 0020: 00000009 80000080 (TypeId #9, Subdirectory at offset 0x80) 0028: 00000000 00000000 00000000 00030000 (3 entries in this directory) 0038: 00000001 800000A0 (NameId #1, Subdirectory at offset 0xA0) 0040: 00000002 00000108 (NameId #2, data desc at offset 0x108) 0048: 00000003 00000118 (NameId #3, data desc at offset 0x118) 0050: 00000000 00000000 00000000 00040000 (4 entries in this directory) 0060: 00000001 00000128 (NameId #1, data desc at offset 0x128) 0068: 00000002 00000138 (NameId #2, data desc at offset 0x138) 0070: 00000003 00000148 (NameId #3, data desc at offset 0x148) 0078: 00000004 00000158 (NameId #4, data desc at offset 0x158) 0080: 00000000 00000000 00000000 00020000 (2 entries in this directory) 0090: 00000001 00000168 (NameId #1, data desc at offset 0x168) 0098: 00000009 800000C0 (NameId #9, Subdirectory at offset 0xC0) 00A0: 00000000 00000000 00000000 00020000 (2 entries in this directory) 00B0: 00000000 000000E8 (Language ID 0, data desc at offset 0xE8 00B8: 00000001 000000F8 (Language ID 1, data desc at offset 0xF8 00C0: 00000000 00000000 00000000 00030000 (3 entries in this directory) 00D0: 00000001 00000178 (Language ID 0, data desc at offset 0x178 00D8: 00000001 00000188 (Language ID 1, data desc at offset 0x188 00E0: 00000001 00000198 (Language ID 2, data desc at offset 0x198 00E8: 000001A8 (At offset 0x1A8, for TypeId #1, NameId #1, Language id #0 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 00F8: 000001AC (At offset 0x1AC, for TypeId #1, NameId #1, Language id #1 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0108: 000001B0 (At offset 0x1B0, for TypeId #1, NameId #2, 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0118: 000001B4 (At offset 0x1B4, for TypeId #1, NameId #3, 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0128: 000001B8 (At offset 0x1B8, for TypeId #2, NameId #1, 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0138: 000001BC (At offset 0x1BC, for TypeId #2, NameId #2, 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0148: 000001C0 (At offset 0x1C0, for TypeId #2, NameId #3, 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0158: 000001C4 (At offset 0x1C4, for TypeId #2, NameId #4, 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0168: 000001C8 (At offset 0x1C8, for TypeId #9, NameId #1, 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0178: 000001CC (At offset 0x1CC, for TypeId #9, NameId #9, Language id #0 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0188: 000001D0 (At offset 0x1D0, for TypeId #9, NameId #9, Language id #1 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) 0198: 000001D4 (At offset 0x1D4, for TypeId #9, NameId #9, Language id #2 00000004 (4 bytes of data) 00000000 (codepage) 00000000 (reserved) And the data for the resources will look like: 01A8: 00010001 01AC: 10010001 01B0: 00010002 01B4: 00010003 01B8: 00020001 01BC: 00020002 01C0: 00020003 01C4: 00020004 01C8: 00090001 01CC: 00090009 01D0: 10090009 01D4: 20090009