Программирование в Win32 Оглавление Основные сведения .................................................................................................................................................. 1 Память в Win32 ..................................................................................................................................................... 1 Исполняемые компоненты Windows .................................................................................................................. 2 Системные библиотеки и подсистемы ............................................................................................................... 3 Модель вызова функций в Win32 ....................................................................................................................... 3 Выполнение программ в Win32: общая картина .............................................................................................. 3 Программирование в Win32 .................................................................................................................................... 4 Используемый компилятор ................................................................................................................................. 4 Работа с объектами .............................................................................................................................................. 5 Работа с файлами ................................................................................................................................................. 5 Обработка ошибок АРI-функций.......................................................................................................................... 7 Консольные программы....................................................................................................................................... 7 Основные сведения Как уже было сказано, системы Windows - это операционные системы защищённого режима (вернее, их 32-битные версии). Система безопасности этих операционных систем построена на разделении кода пользователя и системного кода. Код пользователя работает в режиме user mode и на него наложено очень много ограничений. Системный код работает в режиме kernel mode и почти ничем не ограничен. Основанная единица выполнения в Win32 - это поток (по терминологии защищённого режима - задача). Потоки объединяются в процессы. В общем случае одна программа - это один процесс. В процессе может быть сколько угодно потоков. Каждый процесс обособлен от всех остальных. Это достигается за счет того, что у каждого процесса своя собственная виртуальная память. Тем не менее, если программе надо получить доступ в памяти других процессов, она может осуществить это через специальные системные сервисы. Диспетчеризация потоков осуществляется на основе приоритетов. Приоритеты у потоков не являются статическими параметрами - они могут меняться в зависимости от того, что делает поток. Так или иначе, в некоторый момент времени на центральном процессоре всегда выполняется поток с наибольшим приоритетом. Память в Win32 Каждый процесс имеет своё собственное виртуальное адресное пространство. Адресное пространство любого процесса разбито на две равные части: память процесса и память системы. Младшие 2 Гб памяти являются памятью процесса, старшие 2 Гб - памятью системы. Память системы одна для всех процессов, она недоступна из user mode даже для чтения; любое обращение к этой памяти приводит к ошибке доступа к памяти и завершению приложения. В некоторых случаях память системы и процесса делятся не поровну: под память процесса выделяется 3 Гб памяти, а под память системы - 1 Гб. Так делается в тех случаях, когда используются приложения, требовательные к памяти, которым 2 Гб памяти недостаточно. После выхода 64-битных систем такой метод разделения памяти потерял свою актуальность. Адреса в диапазоне 0h-FFFFh никому не доступны - эта память нужна для выявления нулевых указателей. Любой указатель, значение которого меньше 100000h, считается нулевым. Таким образом, каждому процессу в Win32 в общем случае доступно 2 Гб (за вычетом 64 Кб) виртуальной памяти. Исполняемые компоненты Windows В операционных системах Windows имеется несколько типов исполняемых файлов. Все типы исполняемых файлов имеют формат РЕ (Portable Executable). Наиболее часто используемые исполняемые компоненты в Windows: EXE (приложение), DLL (динамическая библиотека), SYS (драйвер). EXE-файлы - это самый распространённый тип исполняемых файлов в Windows, в них находятся наши программы. DLL-файлы - это динамически загружаемые библиотеки, в них хранятся функции и процедуры, которые могут использовать другие исполняемые компоненты. SYS-файлы - это файлы драйверов режима ядра, в них находится код нулевого кольца операционной системы. Файлы формата РЕ состоят из заголовка и секций. Секция в РЕ-файле - это его основная составляющая единица. В заголовке содержатся основные характеристики файла и таблица секций. Рассмотрим базовые характеристики исполняемых файлов . Самые главные характеристики файла - точка входа и база образа, т. е. указание, по какому адресу должен быть загружен данный модуль. Линкер при создании исполняемого файла должен вставить в код программы вместо меток некоторые конкретные адреса и подразумевает, что этот код будет загружен по некоторому базовому адресу. Загрузчик Windows должен знать, по какому адресу надо загрузить данный исполняемый файл, и именно для этого используется поле базы образа в заголовке исполняемого файла. Очень часто при загрузке файлов DLL и SYS адрес, указанный в поле базы образа, является уже занятым или просто-напросто недоступным. Если при загрузке исполняемого файла адрес, указанный в поле базы образа, уже занят, то он грузит файл по другому адресу, и при этом загрузчику надо подправить в коде программы все обращения к данным. Для этого загрузчику будут нужны релокейшены. Релокейшены содержат информацию о командах, в которых есть обращения к памяти, для поправки адресов. Точка входа содержит адрес, с которого начнётся выполнение исполняемого файла. Каждый файл может иметь таблицу импорта и экспорта. С помощью таблицы импорта исполняемый файл может импортировать функции, которые находятся в других модулях, загруженных в текущее адресное пространство, и использовать эти функции как свои. Для того чтобы другие модули могли использовать функции из данного модуля, адреса этих функций должны быть прописаны в таблице экспорта. Как было сказано выше, в заголовке PE-файла содержится таблица секций, она описывает каждую секцию в РЕ-файле: начало данных в секции, размер данных, адрес, куда должна быть спроецирована данная секция, и её характеристики. Наиболее часто в секциях находятся данные, код, импорты, экспорты и релокейшены. Файлы с расширением EXE являются обычными программами; в 99,99% случаев ЕХЕ-файл представляет процесс, в память которого он загружен. Файлы с расширением DLL являются библиотеками, где содержатся функции, которые могут использовать другие программы или другие DLL. Файлы с асширением SYS являются драйверами режима ядра; код, содержащийся в них, выполняется на нулевом уровне привилегий, в режиме ядра. В файлах DLL и SYS точка входа указывает на инициализирующую функцию. Системные библиотеки и подсистемы В коде Win32, выполняемом в режиме пользователя, запрещены любые прямые обращения к устройствам и портам ввода-вывода. Это значит, что любые обращения к портам ввода/вывода, вызов прерываний и выполнение привилегированных инструкций приведут к ошибке и завершению программы; обращение к памяти, на которую спроецированы регистры устройств, обращение к другим важным областям памяти (например, 0B8000h) ничего не даст. Без обращения к внешним устройствам и портам вводавывода польза от программ, работающих в третьем кольце, нулевая. Для того чтобы они могли обратиться к внешним устройствам и наладить взаимодействие с «внешним миром», операционная система предоставляет программам API-функции. Все API-функции содержатся в системных DLL-библиотеках, самые главные из них: kernels 2.dll (взаимодействие с системой), user32.dll (пользовательский интерфейс), gdi32.dll (графика). Функции библиотеки kernel32.dll в основном являются оболочками вокруг функций из ntdll.dll. Функции из библиотеки ntdll.dll являются «переходниками» к функциям ядра Windows: эти функции просто принимают параметры, подготавливают их к вызову шлюза ядра ( Win2000) или вызову команды SYSENTER (WinXP и позже). Библиотека ntdll.dll - важнейший компонент пользовательской подсистемы Windows и является основополагающей для всех подсистем, т. к. именно через неё пользовательский код может взаимодействовать с кодом ядра. Эта библиотека загружается в память любого процесса одной из первых и всегда по одному и тоже адресу. Библиотека kernel32.dll является основополагающей подсистемы Win32 - она загружается во все процессы Win32 одной из первых (разумеется, после ntdll) и всегда по одному и тому же адресу. Модель вызова функций в Win32 В системах Win32 при вызове всех системных функций используется модель вызова stdcall. Согласно этой модели параметры функций передаются через стек в обратном порядке, при этом за очистку стека от параметров ответственна вызываемая функция. Например, если у функции есть три параметра, то вызов согласно модели stdcall будет выглядеть так: Push Push Push Call paramЗ param2 param1 FunctionAddr Результат выполнения функции будет содержаться в регистре ЕАХ. Также при использовании API-функций следует помнить, что они сохраняют значение не всех регистров общего назначения. Соглашение stdcall предусматривает сохранение содержимое регистров ЕВХ, ESI, EDI и ЕВР. Также при написании функций обратного вызова (подробнее о них пойдет речь в разделе 3.2) надо обязательно сохранять содержимое этих регистров, поскольку код системных функций не ожидает их изменения. Выполнение программ в Win32: общая картина Когда загрузчик Windows загружает наш исполняемый файл, то сначала создаёт для него виртуальное адресное пространство размером 4 Гб, причём нижние 2 Гб из них доступны приложению. Потом он загружает системные библиотеки ntdll.dll и kernel32.dll; если в таблице импорта нашего файла указаны ещё какие-нибудь библиотеки, то они загружаются тоже. После того как библиотеки загружены, создаётся первичный поток процесса, который начинает своё выполнение с точки входа программы. Во время выполнения процесса, вернее его потоков, ему запрещены какие-либо обращения к портам ввода/вывода и вызов каких-либо прерываний. Также запрещены работа с привилегированными регистрами и выполнение привилегированных команд. Чтобы программы могли работать с внешними устройствами «приносить пользу», Windows предоставляет им API-функции, позволяющие программам работать с внешними устройствами, взаимодействовать с системой, а также друг с другом. API-функции находятся в системных библиотеках; каждая функция, которая работает с ресурсом, охраняемым системой (файлы, процессы, устройства и т. д.), вызывает соответствующую функцию ядра системы. Вызов подавляющего большинства функций, экспортируемых системными библиотеками функций, происходит по соглашению stdcall. Резюмируем всё вышесказанное: операционная система помещает программу в некоторое изолированное адресное пространство, разрешая ей взаимодействовать с «внешним миром» посредством функций (системных сервисов), которые сама же и предоставляет. Операционная система Windows избавляет программиста, который пишет программы на ассемблере, от множества забот, которые вообще-то не должны его касаться, например: от работы с системными регистрами и структурами, взаимодействия с внешними устройствами, реализации работы с файловой системой и т. д. В связи с этим программирование на ассемблере под Win32 намного легче, чем многие думают. При работе с файлами программисту не нужно заботиться о том, какая же модель жёсткого диска установлена на компьютере: достаточно просто вызывать функции, которые предоставляет операционная система, а она уже сама разберётся со всеми проблемами. Программирование в Win32 Используемый компилятор Для компиляции программ будем использовать FASМ, которые имеет следующие достоинства: 1. Максимальный набор поддерживаемых команд. FASM поддерживает весь (или почти весь) набор команд х86-64. 2. Наличие Linux- и Windows-версий, а также поддержка широкого списка выходных файлов. Можно компилировать программы для Windows (формат РЕ) и для Linux (формат ELF). 3. Гибкий синтаксис и отсутствие совершенно бесполезных, но обязательных директив (например, .386, .486 и т. п.). 4. Полный контроль над размещением данных в исполняемом файле. 5. Мощнейший макросный движок, с помощью которого можно создать практически любой макрос и изменить текст программы и сам язык до неузнаваемости. 6. При компиляции через командную строку нет необходимости указывать множество опций компиляции. Для того чтобы получить исполняемый файл нужного типа, достаточно задать все необходимые параметры в самом исходнике. 7. Windows-версия FASМ поставляется вместе с IDE, благодаря которой можно скомпилировать программу нажатием одной кнопки (или пункта меню). FASM предоставляет два макроса для вызова API функций: stdcall и invoke. Оба макроса принимают указатель на функцию и параметры, передаваемые ей. Различие между ними только одно - способ передачи адреса вызываемой функции. Макросу stdcall необходимо передавать адрес функции, а макросу invoke - адрес, где хранится указатель на функцию. Эта особенность иногда оказывается довольно-таки удобной, когда адрес (или указатель на адрес) функции содержится в регистре. Также вместе с компилятором FASM поставляется набор заголовочных файлов со значениями большинства констант, используемых при работе с API-функциями. Чтобы подключить полный набор заголовочных файлов с определениями всех констант, макросов и структур, достаточно подключить в программе файл win32a.inc для файлов с определениями для ANSI-версий функций - или win32w.inc для подключения файлов с определениями для UNICODE-версий функций. Программы, работающие в подсистеме Win32, бывают следующих типов: 1. Консольные программы. 2. GUI программы. Главное отличие этих двух видов состоит в способе ведения диалога с пользователем. Консольные программы в этих целях пользуются консолью. GUI-программы ведут диалог с пользователем посредством графического интерфейса. Тем не менее большой разницы между этими двумя видами программ нет - отличия только формальные. Консольная программа может создать окно и посредством окна взаимодействовать с пользователем, а GUI-программа может создать консоль и взаимодействовать с пользователем через нее. Работа с объектами В Windows для работы с каким-либо объектом нужно получить его описатель, или хендл. Описатель объекта (далее - хендл) - это обычное число, которое задаёт номер элемента в некоторой таблице, описывающей объекты. В Windows есть два типа объектов: объекты ядра и пользовательские объекты. Объектами ядра, или системными объектами являются файлы, процессы, потоки, объекты синхронизации и т. д., т. е. все объекты, от которых прямо или косвенно зависит нормальная работа системы. Объектами пользователя в большинстве своём являются объекты графической подсистемы, а именно окна и элементы управления. Ядро системы не оперирует пользовательскими объектами и, по сути, не имеет о них никакого понятия. У каждого процесса в Windows есть таблица хендлов, где описаны системные объекты, к которым процесс получил какой-либо доступ (как минимум на чтение). В этой таблице в каждом элементе содержатся имя объекта и уровень доступа к нему. В зависимости от уровня доступа к объекту возникает понятие уровня доступа хендла. К примеру, если хендл указывает на элемент таблицы, в котором описан файл с полным уровнем доступа, то его называют хендлом с полным доступом к файлу. Поскольку у каждого процесса своя таблица хендлов, то хендл, используемый в одном процессе (не забывайте, что хендл - это обычное число), в другом процессе может указывать на другой объект либо вовсе быть недействительным. С пользовательскими объектами всё намного проще. Пользовательские объекты доступны всем процессам системы - хендлы являются общими для всех процессов системы. Работа с файлами Работа с файлами - один из ключевых аспектов при программировании в Windows. Большинство устройств в Windows имеют файловую структуру, а именно: файлы, каталоги, СОМ- и LPT-порты, многие периферийные устройства, все дисковые и недисковые устройства хранения информации, некоторые системные объекты. Даже если устройство не имеет файловую структуру, операционная система абстрагирует запросы к нему так, как будто запросы идут к файлам. Всю основную работу с файлами осуществляют следующие API-функции, находящиеся в библиотеке KERNERL32.DLL: 1. CreateFile. 2. ReadFile. 3. WriteFile. Функция CreateFile позволяет не только создавать файлы, но и открывать файловые объекты. Функция может открыть следующие объекты: файлы, пайпы, почтовые слоты, дисковые устройства, консоли, каталоги, а также любые устройства, имеющие имя и позволяющие работать с ними как с файлами. Функция CreateFile принимает следующие параметры: 1. Имя файла (LPCTSTR). 2. Запрашиваемый доступ (DWORD). 3. 4. 5. 6. 7. Параметры общего доступа (DWORD). Параметры безопасности (LPSECURITY_ATTRIBUTES). Производимое действие (DWORD). Дополнительные флаги и атрибуты (DWORD). Файл-шаблон (HANDLE). В скобках указаны названия типов параметров - именно те названия, которые были придуманы разработчиками операционной системы Windows. По сути, параметры, принимаемые функцией, являются обычными числовыми константами или указателями. Первый параметр задаёт имя объекта (далее - файл), с которым будет идти работа. Строка может быть как ANSI, так и UNICODE. Второй параметр задаёт тип доступа к объекту; он может быть равен следующим значениям: GENERIC_READ (чтение) и GENERIC_WRITE (запись) либо их комбинацией. Третий параметр задаёт параметры общего доступа к файлу, а именно - какой доступ к открываемому файлу смогут получить другие программы. Если указана константа FILE_SHARE_READ, то другие процессы смогут только читать из файла; если указать FILE_SHARE_WRITE, то другие процессы смогут производить запись в файл. Четвёртый параметр задаёт атрибуты безопасности, которые мы не будем рассматривать. Можно указать ноль, и будут применены атрибуты безопасности, задаваемые системой по умолчанию. Пятый параметр задаёт операцию, производимую с файловым объектом. Можно передать следующие константы: CREATE_NEW - будет создан новый файл; если файл уже существует, функция вернёт ошибку. CREATE_ALWAYS - будет создан новый файл; если файл уже существует, то новый файл запишется поверх него (т. е. содержимое предыдущего файла будет потеряно). OPEN_EXISTING - файл будет открыт; если файла не существует, то функция вернёт ошибку. ОРEN_ALWAYS - файл будет открыт в любом случае; если он не существует, то будет создан новый. При открытии устройств или любых объектов, которые нельзя удалить или создать, необходимо указать ОPEN_EXISTING в качестве пятого параметра - любая другая константа вызовет ошибку функции. Шестой параметр задаёт дополнительные атрибуты открытия. Седьмой параметр позволяет указать хендл другого файла, на основе которого будет создан новый (используется совместно с действиями CREATE_NEW, CREATE_ALWAYS и OPEN_ALWAYS). Шестой и седьмой параметры рассматривать не будем. В случае удачи функция CreateFile вернёт описатель (хендл) объекта, имя которого было указано в первом параметре, в случае неудачи вернёт значение INVALID_HANDLE_VALUE (а именно -1 или 0ЕЕЕЕЕЕЕЕh). Поскольку операционные системы семейства WinNT (а речь идёт именно о них) являются сетевыми операционными системами, программа имеет возможность открывать файловые объекты на других системах, доступных через сеть. Для доступа к объектам на других системах необходимо указать имя объекта следующего формата: \\имя удалённого компьютера\имя объекта на удалённой системе. Указанный формат является форматом полного имени файла. Для открытия объектов в текущей системе можно указывать как полное имя объекта, так и неполное. Неполное имя объекта представляет собой привычное всем имя, например: C:\folder\file.ext. При использовании полного имени объекта можно указать имя текущего компьютера либо просто точку вместо имени, например: \\.\D:\File.dat. Можно получать доступ и к дисковым устройствам; например, если указать \\.\PHYSICALDRIVE0, то мы получим хендл, с помощью которого сможем работать с первым жёстким диском как с одним большим файлом. Кроме того, можно открывать логические диски и работать с ними как с одним большим файлом, например \\.\D:. Помимо функции CreateFile есть функция OpenFile, которая позволяет только открывать файлы или устройства. Функция OpenFile является оболочкой вокруг функции CreateFile, к которой в результате всё и сводится так или иначе. После открытия файла можно производить запись и чтение из него. Чтение из файла осуществляет функция ReadFile. Функция ReadFile принимает следующие параметры: 1. 2. 3. 4. 5. Хендл файла (HANDLE). Указатель на буфер (LPVOID). Количество читаемых байтов ( DWO R D). Количество реально считанных байтов (LPDWORD). Указатель на структуру, используемую при асинхронных операциях (LPOVERLAPPED). Первый параметр задаёт хендл файла, с которым будет производиться операция чтения. Второй параметр задаёт указатель на буфер, в который будут сохранены считанные данные. Третий параметр задаёт указатель на переменную типа DWORD (двойное слово), в которую будет сохранено количество реально считанных байтов. Пятый параметр задаёт указатель на специальную структуру, используемую при асинхронных операциях (они в этом разделе рассматриваться не будут). Запись в файл производит функция WriteFile. Она принимает следующие параметры: 1. 2. 3. 4. 5. Хендл файла (HANDLE). Указатель на буфер (LPVOID). Количество записываемых байтов (DWORD). Количество реально записанных байтов (LPDWORD). Указатель на структуру, используемую при асинхронных операциях (LPOVERLAPPED). Назначение параметров идентично тому, которое приводилось для функции ReadFile. После завершения работы с файлом необходимо закрыть его хендл, чтобы другие программы могли работать с этим файлом. Функция closeHandle производит закрытие хендла файла; она принимает один параметр - хендл файла. Также помимо функций WriteFile и ReadFile есть более универсальная и гибкая функция - DeviceioControl, которая позволяет работать не только с файлами, но и с устройствами. Функция CioseHandle позволяет закрывать не только хендл файла, но и хендл любого системного объекта. Обработка ошибок АРI-функций При работе с API-функциями зачастую функции завершаются неудачей, и возникает необходимость узнать причину ошибки. Узнать код последней ошибки можно с помощью функции GetLastError. Функция не принимает никаких параметров и возвращает в регистре ЕАХ код последней ошибки. Операционная система Windows позволяет программе установить код последней ошибки. Функция SetLastError устанавливает код последней ошибки и принимает только один параметр - числовой код ошибки. Консольные программы Консольные программы - это полноценные 32- битные программы, которые могут использовать все возможности операционной системы Windows. Главная особенность консольных программ в том, что при запуске к ним автоматически привязывается консоль, т. е. никаких действий со стороны программы не требуется - консоль создаётся автоматически. Если же GUI-приложению понадобится взаимодействовать с пользователем через консоль, оно должно будет создать консоль с помощью функции AllocConsole. По сути, это единственное отличие консольных программ от оконных. Чтобы работать с консолью, необходимо получить её хендл. Есть несколько способов получения хендл а консоли: 1. Через функцию GetStdHandle. 2. Через функцию CreateFile. Исторически сложилось так: чтобы работать с консолью, надо иметь два хендла - один на чтение и один на запись. Функция GetStdHandle принимает один параметр; если он равен STD_INPUT_HANDLE, то функция возвращает хендл для чтения, а если STD_OUTPUT_HANDLE - хендл для записи. При использовании функции CreateFile необходимо задать только два параметра: имя и запрашиваемый доступ. Имя должно быть равно ‘CON’, а запрашиваемый доступ - GENERIC_READ (если надо получить хендл для чтения) или GENERIC_WRITE (если надо получить хендл для записи). Пятый параметр, который задаёт код действия, должен быть равен ОPEN_EXISTING: любой другой код приведёт к ошибке. Производить запись или чтение из консоли тоже можно двумя способами: через функции ReadFile/WriteFile или ReadConsole/WriteConsole. Между ними есть только одно отличие у функций ReadConsole/writeConsole есть «функции-близнецы» ReadConsolew/writeConsolew, которые позволяют работать с UNICODE-строками. Набор параметров функций ReadConsole/WriteConsole полностью идентичен параметрам функций ReadFile/WriteFile: хендл, указатель на буфер, количество записываемых байтов, указатель на переменную, в которую будет сохранено количество реально записанных/прочитанных байтов. Пятый параметр у функций ReadConsole/WriteConsole зарезервирован и должен быть равен нулю. Функции записи в консоль WriteConsole/writeFile производят немедленный вывод строки на консоль. Функции чтения из консоли ReadConsole/ReadFile ожидают нажатия пользователем клавиши Enter и заносят введённую строку в указанный программой буфер (символы новой строки и перевода каретки тоже заносятся в буфер).