Шпаргалка по ресурсам Windows-32 (для Delphi)


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

Стандартная технология:

В тексте `Proj_L.Dpr` есть следующие примеры:

Внутренний формат ресурсов и пример 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.
Замечания:

Внутренний формат ресурсов 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. Любопытно, что способ идентификации ресурса ( целое число или ссылка на имя ) специфицирован, скорее, не на уровне стандарта, а на уровне принятых соглашений. Для поиска ресурса мы, в общем случае, задаем три параметра:

Формат ресурсов 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:

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 
Hosted by uCoz