Blog. Just Blog

Работа с INI-файлами на Ассемблере

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Работа с INI-файлами на Ассемблере
Работа с INI-файлами на Ассемблере

Конфигурационные ini-файлы появились в самых первых версиях Windows. Изначально в них хранились только настройки Windows, а затем они стали использоваться для хранения параметров других приложений. Начиная с Windows 95, Microsoft объявил ini-файлы устаревшими и с тех пор предлагает использовать системный реестр для хранения всех настроек и данных программ. Лично я считаю, что приложения должны быть легко переносимыми между компьютерами, а также легко и полностью деинсталлироваться, поэтому внедрение в систему должно быть минимальным. Хранение всех настроек в ini-файле или в xml-файле в папке с программой - это, на мой взгляд, самое правильное решение, а в реестр нужно залезать только в случае крайней необходимости.

Несложная структура формата ini-файлов позволяет легко обрабатывать их программно и имеет достаточно понятный вид для чтения и изменения человеком. Работать с ini-файлами из приложения одно удовольствие: чтобы читать параметры из файла конфигурации достаточно всего трех функций GetPrivateProfileString (строковые данные), GetPrivateProfileInt (числовые данные) и GetPrivateProfileStruct (двоичные данные), а вот для записи данных обратно есть только две функции WritePrivateProfileString (строковые данные) и WritePrivateProfileStruct (двоичные данные). Отдельной функции для записи числовых значений по какой-то причине не предусмотрено. Но это все хорошо, когда вы обрабатываете ini-файл с известной структурой, когда в приложении заранее определены имена секций и названия ключей.

Пример INI-файла
Пример INI-файла

А вот случай посложнее, когда в связи с особенностью работы приложения, структура ini-файла неизвестна. Или если известны имена секций, но количество и список ключей и их значений могут меняться. Парсить ini-файл с неизвестным содержимым самостоятельно - задача не самая тривиальная. При кажущейся простоте структуры, в файле могут быть комментарии, строки могут быть отформатированы разными способами, ключи и значения могут быть разделены как пробелами, так и табуляциями, да мало ли еще может быть вариантов. К счастью, разработчики WinAPI позаботились об этом, и система способна полностью парсить ini-файлы самостоятельно. Сегодня я расскажу, как на Ассемблере прочитать структуру секций, все ключи и их значения из произвольного ini-файла.

Сперва определим в сегменте данных переменные, в которые будут записаны все нужные нам значения.
  1. section '.data' data readable writeable
  2.  
  3. ; Список секций
  4. sections rb 1024
  5. ; Список ключений и значений
  6. keys     rb 1024*32
  7. ; Имя секции
  8. sname    rb 100h
  9. ; Название ключа
  10. key      rb 100h
  11. ; Значение ключа
  12. value    rb 100h
Список секций ini-файла можно получить при помощи функции GetPrivateProfileSectionNames. Она вернет список имен секций в виде последовательности ASCIIZ-строк, окончание списка - нулевой байт. Количество секций и размер блока памяти для них нигде в документации не оговорены, мне кажется, что одного килобайта для этого будет достаточно.

Список имен секций
Список имен секций

Содержимое секций можно прочитать другой функцией - GetPrivateProfileSection. Она вернет список ключей и их значений в виде ASCIIZ-строк в формате "ключ=значение". Окончание списка также определяется нулевым байтом. Все комментарии удаляются, а строки, которые не соответствуют формату ini-файла, игнорируются. При этом максимальный размер полезного содержимого секции, как сказано в описании функции, не должен превышать 32 килобайта. Может быть это утверждение справедливо для более старых версий Windows, а на современных системах, как показала практика, штатными средствами совершенно спокойно обрабатываются файлы с размером секций в несколько мегабайт.

Список ключей и их значений
Список ключей и их значений

И вот этот список нам все-таки придется парсить самостоятельно. Имя ключа и его значение всегда разделено знаком равенства "=" без пробелов, независимо от того, как эта строчка была записана в исходном файле. Разработчики WinAPI и тут нам очень помогли. Ведь по сути все значения в ini-файле являются строками и то, как будет представлено их значение, зависит исключительно от функции, которой это значение будет запрошено. Это надо будет учитывать при ручной обработке данных из файла. Полностью код для парсинга ini-файла выглядит примерно так:
  1.         ; Получить список секций ini-файла
  2.         invoke  GetPrivateProfileSectionNames,sections,1024,ini_file
  3.  
  4.         ; Файл пустой?
  5.         or      eax,eax
  6.         jz      loc_scan_sections_done
  7.  
  8.         ; Указатель на список секций
  9.         mov     esi,sections
  10.  
  11. loc_scan_sections:
  12.         ; Конец списка секций?
  13.         cmp     byte [esi],0
  14.         je      loc_scan_sections_done
  15.  
  16.         ; Имя обрабатываемой секции
  17.         mov     edi,sname
  18. @@:
  19.         lodsb
  20.         stosb
  21.         or      al,al
  22.         jnz     @b
  23.  
  24.         push    esi
  25.  
  26.         ;----------------------------------------------
  27.         ; sname = имя секции
  28.         ;----------------------------------------------
  29.  
  30.         ; Прочитать содержимое секции
  31.         invoke  GetPrivateProfileSection,sname,keys,1024*32,ini_file
  32.         ; Секция пустая?
  33.         or      eax,eax
  34.         jz      loc_scan_keys_done
  35.  
  36.         ; Указатель на список ключений и значений
  37.         mov     esi,keys
  38.  
  39. loc_scan_keys:
  40.         ; Конец списка ключей?
  41.         cmp     byte [esi],0
  42.         je      loc_scan_keys_done
  43.  
  44.         ; Название ключа
  45.         mov     edi,key
  46. @@:
  47.         lodsb
  48.         cmp     al,'='
  49.         je      @f
  50.         stosb
  51.         jmp     @b
  52. @@:
  53.         xor     eax,eax
  54.         stosb
  55.  
  56.         ; Значение ключа
  57.         mov     edi,value
  58. @@:
  59.         lodsb
  60.         stosb
  61.         or      al,al
  62.         jnz     @b
  63.  
  64.         ;----------------------------------------------
  65.         ; key = название ключа
  66.         ; value = значение ключа
  67.         ;----------------------------------------------
  68.  
  69.         ; Следующий ключ
  70.         jmp     loc_scan_keys
  71.  
  72. loc_scan_keys_done:
  73.  
  74.         ; Следующая секция
  75.         pop     esi
  76.         jmp     loc_scan_sections
  77.  
  78. loc_scan_sections_done:
  79.  
  80.         ; Разбор ini-файла завершен
Для удобства обработки имена секций копируются в переменную sname, а имена и значения ключей, соответственно, в переменные key и value. Места в коде, где эти значения можно обрабатывать, выделены комментариями.

Еще немного полезной информации. Для того, чтобы сбросить кешированные данные на диск и сразу задействовать изменения после записи в ini-файл, надо вызвать функцию WriteProfileString или WritePrivateProfileString, передав им в качестве имени секции, наименования ключа и строки значения NULL:
  1.         ; Сбросить кешированные данные на диск
  2.         invoke  WriteProfileString, NULL, NULL, NULL
  3.         invoke  WritePrivateProfileString, NULL, NULL, NULL, ini_file
Для продвинутых пользователей расскажу еще про один способ работы с ini-файлами. Windows позволяет рассматривать секции ini-файлов с ключами и их значениями как набор свойств и предоставляет COM-интерфейсы IPropertyBag или IPropertyBag2 для работы с ними. Но сперва пачка недостающих данных для работы с COM-объектами.
  1. struct DECIMAL
  2.     wReserved dw ?
  3.     union
  4.         struct
  5.             scale db ?
  6.             sign db ?
  7.         ends
  8.         signscale dw ?
  9.     ends
  10.     Hi32 dd ?
  11.     union
  12.         struct
  13.             Lo32 dd ?
  14.             Mid32 dd ?
  15.         ends
  16.         Lo64 dq ?
  17.     ends
  18. ends
  19.  
  20. struct VARIANT
  21.     union
  22.         struct
  23.             vt dw ?
  24.             wReserved rw 3
  25.             union
  26.                 llVal dq ?
  27.                 lVal  dd ?
  28.                 iVal  dw ?
  29.                 bVal  db ?
  30.             ends
  31.         ends
  32.         decVal DECIMAL
  33.     ends
  34. ends
  35.  
  36. ; IID_IPropertyBag Interface
  37. struct IPropertyBag
  38.     ; IUnknown
  39.     QueryInterface dd ?   ; 000h
  40.     AddRef         dd ?   ; 004h
  41.     Release        dd ?   ; 008h
  42.     ; IPropertyBag
  43.     Read           dd ?   ; 00Ch
  44.     Write          dd ?   ; 010h
  45. ends
  46.  
  47. ; GUID {55272A00-42CB-11CE-8135-00AA004BB851}
  48. IID_IPropertyBag \
  49.     dd 055272A00h
  50.     dw 042CBh
  51.     dw 011CEh
  52.     db 081h, 035h, 000h, 0AAh, 000h, 04Bh, 0B8h, 051h
  53.  
  54. VT_BSTR  = 8
  55. STGM_READWRITE = 0x02
Для связки секции с объектом используется недокументированная функция из библиотеки shlwapi.dll SHCreatePropertyBagOnProfileSection. Она мало того, что она не имеет описания на сайте MSDN, так еще и экспортируется исключительно по ординалу 472. Как и большинство COM-функций, функция работает только с юникодными строками.
  1.         invoke  CoInitialize,0
  2.  
  3.         ; Полный путь до ini-файла
  4.         invoke  GetFullPathName,fname,MAX_PATH,ini_file,NULL
  5.  
  6.         ; Импортировать функции из shlwapi.dll
  7.         invoke  LoadLibrary,szlib
  8.         mov     [lib],eax
  9.         ; SHCreatePropertyBagOnProfileSection
  10.         invoke  GetProcAddress,[lib],472
  11.         mov     [SHCreatePropertyBagOnProfileSection],eax
  12.  
  13.         ; Связать объект с секцией ini-файла
  14.         stdcall [SHCreatePropertyBagOnProfileSection],ini_file,\
  15.                 sname,STGM_READWRITE,IID_IPropertyBag,ppBag
  16.  
  17.         ; Получить значение ключа
  18.         mov     eax,[ppBag]
  19.         mov     eax,[eax]
  20.         ; Ожидаемый тип значения
  21.         mov     [vtVar.vt],VT_BSTR
  22.         stdcall dword [eax+IPropertyBag.Read],[ppBag],\
  23.                 szKeyName1,vtVar,NULL
  24.  
  25.         ...
  26.         ; [vtVar.lVal] -> указатель на строку со значением ключа
  27.         ...
  28.  
  29.         ; Очистить выделенную для строки память
  30.         invoke  SysFreeString,[vtVar.lVal]
  31.  
  32.         ; Установить значение ключа или добавить новый
  33.         mov     eax,[ppBag]
  34.         mov     eax,[eax]
  35.         ; Тип значения - указатель на строку
  36.         mov     [vtVar.vt],VT_BSTR
  37.         ; Указатель на строку
  38.         mov     [vtVar.lVal],szVal
  39.         stdcall dword [eax+IPropertyBag.Write],[ppBag],\
  40.                 szKeyName2,vtVar
  41.  
  42.         ; Удалить объект
  43.         invoke  CoUninitialize
После связывания секции с объектом становятся доступны два метода: Read для чтения значений ключей и Wrtite для их записи. Перед чтением и записью значений ключей обязательно надо заполнять структуру VARIANT, в частности поле vt. Там должен быть тип данных, которые ожидаются при чтении или передаются при записи. Если значение ключа не соответствует указанному типу, то функция завершится с ошибкой. Также при использовании метода Read надо учитывать такую особенность, что значение ключа, обрамленное кавычками, будет получено вместе с этими кавычками, тогда как функция GetPrivateProfileString вернет содержимое строки внутри кавычек. Также не забывайте освобождать выделенную системой память после чтения значений ключей.

В описании интерфейса IPropertyBag2 есть еще два интересных метода, а именно CountProperties для получения количества свойств в списке и GetPropertyInfo для получения информации. Велик соблазн реализовать с их помощью перебор всех ключей секции, но не тут-то было. При попытке вызова этих методов для объекта секции ini-файла, они завершаются с ошибкой, так как объект такого типа не поддерживает в полной мере интерфейс IPropertyBag2. Поэтому читать значения можно только для заведомо известных названий ключей.

Кроме методов интерфейсов в библиотеке shlwapi.dll есть еще несколько функций для работы с наборами параметров, в нашем случае это ключи ini-файлов. Как и основная функция, эти функции доступны только по ординалам. Вот их актуальный список с указанием ординала и протитипа.

493 - SHPropertyBag_ReadType (ppb, pszPropName, VARIANT* pv, VARTYPE vt);
494 - SHPropertyBag_ReadStr (ppb, pwzPropName, LPWSTR psz, int cch);
495 - SHPropertyBag_WriteStr (ppb, pwzPropName, LPCWSTR psz);
496 - SHPropertyBag_ReadLONG (ppb, pwzPropName, LONG* pl);
497 - SHPropertyBag_WriteLONG (ppb, pwzPropName, LONG l);
498 - SHPropertyBag_ReadBOOLOld (ppb, pwzPropName, BOOL* bDefault);
499 - SHPropertyBag_WriteBOOL (ppb, pwzPropName, BOOL fValue);
505 - SHPropertyBag_ReadGUID (ppb, pwzPropName, GUID* pguid);
506 - SHPropertyBag_WriteGUID (ppb, pwzPropName, const GUID* pguid);
507 - SHPropertyBag_ReadDWORD (ppb, pwzPropName, DWORD* pdw);
508 - SHPropertyBag_WriteDWORD (ppb, pwzPropName, DWORD dw);
512 - SHPropertyBag_ReadPIDL (ppb, pwzPropName, LPITEMIDLIST* ppidl);
513 - SHPropertyBag_WritePIDL (ppb, pwzPropName, LPCITEMIDLIST pidl);
520 - SHPropertyBag_ReadBSTR (ppb, pwzPropName, BSTR* pbstr);
521 - SHPropertyBag_ReadPOINTL (ppb, pwzPropName, POINTL* ppt);
522 - SHPropertyBag_WritePOINTL (ppb, pwzPropName, const POINTL* ppt);
523 - SHPropertyBag_ReadRECTL (ppb, pwzPropName, RECTL* prc);
524 - SHPropertyBag_WriteRECTL (ppb, pwzPropName, const RECTL* prc);
525 - SHPropertyBag_ReadPOINTS (ppb, pwzPropName, POINTS* ppt);
526 - SHPropertyBag_WritePOINTS (ppb, pwzPropName, const POINTS* ppt);
527 - SHPropertyBag_ReadSHORT (ppb, pwzPropName, SHORT* psh);
528 - SHPropertyBag_WriteSHORT (ppb, pwzPropName, SHORT sh);
529 - SHPropertyBag_ReadInt (ppb, pwzPropName, INT* piResult);
530 - SHPropertyBag_WriteInt (ppb, pwzPropName, INT iValue);
531 - SHPropertyBag_ReadStream (ppb, pwzPropName, IStream** ppstm);
532 - SHPropertyBag_WriteStream (ppb, pwzPropName, IStream* pstm);
534 - SHPropertyBag_ReadBOOL (ppb, pwzPropName, BOOL* pfResult);
535 - SHPropertyBag_Delete (ppb, pszPropName);

Если внимательно посмотреть, то можно заметить, что некоторые функции, в основном для чтения-записи числовых значений, практически дублируют друг друга. По-моему ситуация, когда надо прочитать значение именно конкретного байта, крайне редкая. Еще стоит обратить внимание на функции, которые работают со структурами, например, с POINT или RECT. Такие наборы данных хранятся в ini-файле в виде следующих записей:

[options]
; Структура POINT
my_pt.x=100
my_pt.y=200

; Структура RECT
my_rc.left=0
my_rc.top=0
my_rc.right=50
my_rc.bottom=95

Это очень удобно, так как достаточно будет запросить значение одного ключа, соответственно, это будут ключи my_pt и my_rc, чтобы загрузить в приложение уже заполненную структуру. GUID'ы записываются в соответствии с принятым шаблоном:

[options]
; GUID
my_guid={55272A00-42CB-11CE-8135-00AA004BB851}

Функции для чтения-записи строк работают с тем же особенностями, что и методы, то есть строковые значения в кавычках читаются вместе с кавычками. Булевые данные хранятся в виде числовых значений, -1 для TRUE и 0 для FALSE.

В списке есть две функции, заслуживающие отдельного внимания: SHPropertyBag_ReadType и SHPropertyBag_Delete. С помощью первой можно получить тип значения ключа до того, как запрашивать это значение. Вторая используется для удаления ключа. Вопреки названию функции, запись о ключе в ini-файле не удаляется, а только очищается его значение.
  1.         invoke  CoInitialize,0
  2.  
  3.         ; Полный путь до ini-файла
  4.         invoke  GetFullPathName,fname,MAX_PATH,ini_file,NULL
  5.  
  6.         ; Импортировать функции из shlwapi.dll
  7.         invoke  LoadLibrary,szlib
  8.         mov     [lib],eax
  9.  
  10.         ; SHCreatePropertyBagOnProfileSection
  11.         invoke  GetProcAddress,[lib],472
  12.         mov     [SHCreatePropertyBagOnProfileSection],eax
  13.  
  14.         ; SHPropertyBag_WriteLONG
  15.         invoke  GetProcAddress,[lib],497
  16.         mov     [SHPropertyBag_WriteLONG],eax
  17.  
  18.         ; SHPropertyBag_ReadStr
  19.         invoke  GetProcAddress,[lib],494
  20.         mov     [SHPropertyBag_ReadStr],eax
  21.  
  22.         ; Связать объект с секцией ini-файла
  23.         stdcall [SHCreatePropertyBagOnProfileSection],ini_file,\
  24.                 sname,STGM_READWRITE,IID_IPropertyBag,ppBag
  25.  
  26.         ; Получить строковое значение
  27.         stdcall [SHPropertyBag_ReadStr],[ppBag],szKeyName1,buff,100h
  28.  
  29.         ; Установить числовое значение
  30.         stdcall [SHPropertyBag_WriteLONG],[ppBag],szKeyName2,100500
  31.  
  32.         ; Удалить объект
  33.         invoke  CoUninitialize
В приложении примеры программ с исходными текстами, одна из которых парсит находящийся рядом с ней ini-файл и выводит его структуру в окно лога, а вторая читает и записывает значения ключей с помощью COM.

Примеры программ с исходными текстами (FASM)Примеры программ с исходными текстами (FASM)

Read.INI.File.Demo.zip (5,571 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 4207 | Комментариев: 5

Внимание! Статья опубликована больше года назад, информация могла устареть!

Комментарии

Отзывы посетителей сайта о статье
ManHunter (19.05.2023 в 16:33):
Добавил описание работы с функциями SHPropertyBag_*
ManHunter (18.05.2023 в 21:16):
Добавил в статью информацию о работе с ini-файлами с помощью COM, архив обновлен.
ManHunter (21.06.2016 в 00:24):
Цитатасуществует популярный стандартный метод работы со своим файлом настроек - его часто размещали в каталоге %WINDIR%

Если запись в папку с программой по какой-то причине закрыта, то можно хранить настройки в пользовательской папке %USERPROFILE% или %LOCALAPPDATA%, не обязательно же сразу лезть в каталог винды. Экспорт-импорт из/в реестр тоже не панацея, при внезапно рухнувшей системе доступ к реестру может оказаться если не невозможен, то по крайней мере сильно затруднен. А если программа хранит настройки у себя в файле, то после переустановки системы или при переносе программы на другой комп достаточно будет только заново создать ярлычки для ее запуска. При хранении настроек в пользовательской папке легко решается вопрос с персональными настройками для разных учетных записей, если программа подразумевает для них какой-то индивидуальный режим работы. Резервное копирование в автоматическом режиме тоже гораздо легче делать поиском и архивированием *.ini, чем прописыванием в бэкапилке и поддержанием в актуальном состоянии списка из 100500 веток реестра. Короче, сплошные плюсы :)
Реестр must die!
user (20.06.2016 в 19:42):
--Добавлено--

.. когда-то сделал собственную библиотечку на Си (для DOS) с реализацией аналогов Get/Wirite-PrivateProfileInt и Get/Write**String, но, как это часто бывало, попользовался ею сам всего пару раз и забросил - слишком громоздкая оказалась штука, хотя работала вполне нормально ..
С WinAPI всё гораздо проще.
user (20.06.2016 в 19:29):
Немного поразглагольствую на тему, с позволенья.

Действительно, использование собственного файла настроек выглядит предпочтительным во всех случаях, кроме, разве что, случая запуска программы с защищённого от записи носителя.

Но и в этом случае существует популярный стандартный метод работы со своим файлом настроек - его часто размещали в каталоге %WINDIR%. Особенно это было популярно во времена WIN16. Там (в %WINDIR%) обычно накапливались эти самые INI-файлы в приличных количествах от разных программ.

Такой способ тоже имеет недостаток - при запуске разных версий софта он будет писать/читать INI-файл не своей версии.
Примером может служить популярный wave-редактор CoolEdit. У этого редактора в INI-файле хранились регистрационные данные, так что головняк был постоянный.
Решалось запуском CoolEdit'а из пакетного файла, который прежде копировал свой INI из каталога программы в %WINDIR%.
Неудобство заключалось в том, что вновь сохранённые настройки перезаписывались старым файлом при следующем запуске.

Еще один неплохой способ хранения настроек в популярном файловом менеджере FAR - настройки хранятся в реестре, но их можно оттуда экспортировать в reg-файл c помощью самой программы. Для сохранения/переноса на другую машину.

В общем, идеального способа нет. Вернее, идеальным можно считать, если в программе предусмотрен выбор способа хранения своих настроек несколькими (двумя) из перечисленных способов.

Симпатичным представляется вариант, когда программа использует свой INI-файл в каталоге запуска, иначе INI-файл в каталоге %WINDIR%, иначе ключи в реестре. И есть возможность выбрать способ при сохренении настроек.

Ну, как-то так.

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2024
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.08 сек. / MySQL: 2 (0.0085 сек.) / Память: 4.5 Mb
Наверх