Некоммерческое акционерное общество

АЛМАТИНСКИЙ УНИВЕРСИТЕТ ЭНЕРГЕТИКИ И СВЯЗИ

Кафедра компьютерных технологий 

 

 

СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ 

Методические указания к выполнению лабораторных работ для студентов

всех форм обучения специальности 5В070400 – Вычислительная техника и программное обеспечение (часть 1)

 

 

Алматы 2011

СОСТАВИТЕЛИ: Мусатаева Г.Т., Конуспаева А.Т., Байжанова Д.О. Системное программирование. Методические указания к выполнению лабораторных работ для студентов всех форм обучения специальности 5В070400 – Вычислительная техника и программное обеспечение (часть 1). - Алматы: АУЭС, 2011. - 24 с. 

 

Методические указания составлены в соответствии с требованиями  квалификационной характеристики специалиста, Государственных стандартов, типовой программы курса. Они составлены с учетом активизации процесса изучения основ курса, закреплению лекционного курса и являются подготовкой к проведению лабораторных работ.

Методические указания предназначены для студентов всех форм обучения специальности 5В070400 – Вычислительная техника и программное обеспечение.

Библиография – 6 названий. 

 

Рецензент: канд. физ.-мат. наук, доцент Б. М. Шайхин.

 

Печатается по плану издания некоммерческого акционерного общества «Алматинский университет энергетики и связи» на 2011 г.

 

© НАО «Алматинский университет энергетики и связи», 2011 г.

 

1 Лабораторная работа. Разработка консольного приложения

 

Цель работы: изучение основ применения API функции для консольных приложений.

Задания:

1) Изменить размер окна консоли.

2) Поменять заголовок окна консоли.

3) Изменить позиции курсора.

4) Поменять цветовые атрибуты текста.

5) Разобрать алгоритм работы процедур NUMPAR и GETPAR.

Методические указания к выполнению

Вызов функций API

Начнем с того, как можно вызвать функции API. Выберем любую функцию API, например, MessageBox:

int MessageBox (HWND hwnd, LPCTSTR 1pText, LPCTSTR 1pCaption, UINT uType);

Данная функция выводит на экран окно с сообщением и кнопкой (или кнопками) выхода. Смысл параметров:

hWnd – дескриптор окна, в котором будет появляться окно-сообщение,

lpText – текст, который будет появляться в окне,

lpCaption – текст в заголовке окна,

uType – тип окна, в частности можно определить количество кнопок выхода.

Теперь о типах параметров. Все они в действительности 32-битные целые числа:

HWND – 32-битное целое,

LPCTSTR – 32-битный указатель на строку,

UINT – 32-битное целое.

Структура программы

Рассмотрим классическую структуру программы под Windows. В такой программе имеется главное окно, а следовательно, и процедура главного окна. В целом, в коде программы можно выделить следующие секции:

- регистрация класса окон;

- создание главного окна;

- цикл обработки очереди сообщений;

- процедура главного окна.

Конечно, в программе могут быть и другие разделы, но данные разделы образуют основной скелет программы. Разберем эти разделы по порядку.

Регистрация класса окон

Регистрация класса окон осуществляется с помощью функции RegisterClassA, единственным параметром которой является указатель на структуру WNDCLASS, содержащую информацию об окне (см. пример ниже).

 

Создание окна

На основе зарегистрированного класса с помощью функции Create WindowExA (или Create WindowA) можно создать экземпляр окна. Как можно заметить, это весьма напоминает объектную модель программирования.

Цикл обработки очереди сообщений

Вот как выглядит этот цикл на языке Си:

while (GetMessage (&msq, NULL, 0, 0))

{

/ / разрешить использование клавиатуры,

/ / путем трансляции сообщений о виртуальных клавишах

/ / в сообщения о алфавитно-цифровых клавишах

TranslateMessage (&msq);

/ /  вернуть управление Windows и передать сообщение дальше

/ / процедуре окна

DispatchMessage (&msg);

}

Функция GetMessage() «отлавливает» очередное сообщение из ряда сообщений данного приложения и помещает его в структуру MSG.

Что касается функции TranslateMessage, то ее компетенция касается сообщений WM_KEYDOWN и WM_KEYUP, которые транслируются в WM_CHAR и WM_DEDCHAR, а также WM_SYSKEYDOWN и WM_SYSKEYUP, преобразующиеся в WM_SYSCHAR и WM_SYSDEADCHAR. Смысл трансляции заключается не в замене, а в отправке дополнительных сообщений. Так, например, при нажатии и отпускании алфавитно-цифровой клавиши в окно сначала придет сообщение WM_KEYDOWN, затем WM_KEYUP, а затем уже WM_CHAR.

Как можно видеть, выход из цикла ожиданий имеет место только в том случае, если функция GetMessage возвращает 0. Это происходит только при получении сообщения о выходе (сообщение WM_QUIT). Таким образом, цикл ожидания играет двоякую роль: определенным образом преобразуются сообщения, предназначенные для какого-либо окна, и ожидается сообщение о выходе из программы.

Процедура главного окна

Вот прототип функции окна на языке С:

LRESULT CALLBACK WindowFunc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

         Смысл параметров:

hwnd – идентификатор окна,

message – идентификатор сообщения,

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

Все четыре параметра имеют тип DWORD.

 

Основная литература: [2] – 164- 192 c.

 

Контрольные вопросы

1) Какие аргументы использует АРІ функция GetStdHandle?

2) Дайте названия цветов букв и символов используемых функцией SetConsoleTextAttribute?

3) Назовите параметры функции CharToOem?

4) Какие типы событий зарезервированы  операционной системой?

 

2 Лабораторная работа. Разработка пользовательского интерфейса

 

Цель работы: изучение основ программирования в среде Win32 и вопросов проектирования интерфейса пользователя.

Задания:

1) Изучить процедуру главного окна.

2) Изучить класс и функции создания окон.

3) Изучить используемые в программе API-функции.

4) Изменить параметры функции GetMessageA.

5) Изменить параметры функции CreateWindowExA.

6) Изменить главную процедуру окна WNDPROC.

7) Изучить общие элементы управления – кнопки, флажки, переключатели, полосы прокрутки, редакторы текста, списки строк, комбинированные списки.

Методические указания к выполнению

Директива INCLUDELIB

В пакете Win32 довольно много разных библиотек. Для данного примера необходима: import32.lib. При наборе текста программы  нужно изменить путь к библиотеке.

Сначала определяются константы и внешние библиотечные процедуры. В действительности все эти определения можно найти в include-файлах, прилагаемых к пакету Win32. Мы не будем использовать стандартные include-файлы, т.к. так удобнее понять технологию программирования.

Необходимо четко понимать способ функционирования процедуры окна, так как именно это определяет суть программирования под Windows. Задача данной процедуры – правильная реакция на все приходящие сообщения. Нужно детально разобратся в работе нашего приложения. Сначала обратите внимание на то, что необработанные сообщения должны возвращаться в систему при помощи функции DefWindowProcA.

Программа отслеживает четыре сообщения: WM_CREATE, WM_DESTROY, WM_LBUTTONDOWN, WM_RBUTTONDOWN. Сообщения WM_CREATE и WM_DESTROY в терминах объектного программирования играют роль конструктора и деструктора: они приходят в функцию окна при создании окна и при уничтожении окна. Если щелкнуть по крестику в правом углу окна, то в функцию окна придет сообщение WM_DESTROY. Далее будет выполнена функция PostQuitMessage и приложению будет послано сообщение WM_QUIT, которое вызовет выход из цикла ожидания и выполнение функции ExitProcess, что, в свою очередь, приведет к удалению приложения из памяти.

Обратите внимание на метку _ERR – переход на нее происходит при возникновении ошибки, и здесь можно поместить соответствующее сообщение.

Для создания консоли используем функцию AllocConsole. По завершении программы консоль освобождается автоматически или с помощью функций FreeConsole.

Следует отметить, что один процесс может иметь только одну консоль, поэтому выполнение в начале программы FreeConsole обязательно.

Для чтения из буфера консоли используется функция ReadConsole.

Установить позицию курсора в консоли можно при помощи функции SetConsoleCursorPosition.

Установить цвет выводимых букв можно с помощью функции SetConsoleTextAttribute.

Для определения заголовка окна консоли используется функция SetConsoleTitle.

Функция CharToOem используется для перевода DOS-кодировки в Windows-кодировку.

Большинство консольных функций при правильном их завершении возвращает ненулевое значение. В случае ошибки в EAX помещается ноль.

 

Основная литература: [4] – 31- 92 c., [5] – 5-86 c.

 

Контрольные вопросы

1) Какие API-функции используются в программе?

2) Какие структуры используются в программе?

3) Структура графического приложения.

4) Какие API-функции включает цикл обработки сообщений?

 

3 Лабораторная работа. Использование ресурсов

 

Цель работы: изучить использование наиболее употребляемых ресурсов.

Задания:

1) Изучить наиболее употребляемые ресурсы.

2) Написать программу с использованием ресурса - Битовая картинка.

3) Написать программу с использованием ресурса - Строка.

4) Написать программу с использованием ресурса - Диалоговое окно.

5) Написать программу с использованием ресурса - Меню.

6) Написать программу с использованием ресурса - Акселераторы.

Методические указания к выполнению

События

Последний объект ядра для синхронизации — события. Этот объект используется для информирования других потоков о том, что произошло некоторое событие, например стало доступно новое сообщение.

Важная дополнительная возможность, предоставляемая событиями, — освобождение нескольких потоков от совместного ожидания, когда в сигнальное состояние перешел один дескриптор. События делятся на сбрасываемые автоматически и вручную, это свойство события устанавливается при вызове функции CreateEvent.

- Событие,  сбрасываемое  вручную,  может сигнализировать одновременно  нескольким потокам, выполняющим ожидание события.

- События с автоматическим сбросом посылают сигналы единственному потоку, выполняющему ожидание события, и сбрасываются автоматически.

Событиями используются функции CreateEvent, OpenEvent, SetEvent, ResetEvent и PulseEvent.

HANDLE CreateEvent (LPSECURITY_ATTRIBUTES lpsa, BOOL fManualReset, BOOL fInitialState, LPCTSTR lpszEventName)

При установленном в значение TRUE параметре fManualReset создается событие с ручным сбросом. Аналогично, если событие первоначально должно находиться в сигнальном состоянии, параметр fInitialState необходимо установить в значение TRUE. Чтобы открыть существующее событие, возможно, из другого процесса, следует воспользоваться функцией OpenEvent.

Для управления событиями используются следующие три функции:

BOOL SetEvent (HANDLE hEvent)

BOOL ResetEvent (HANDLE hEvent;

BOOL PulseEvent (HANDLE hEvent)

Поток может перевести событие в сигнальное состояние, использовав функцию SetEvent. Если событие сбрасывается автоматически, единственный (возможно, из многих) ожидающий поток освобождается, а событие автоматически переводится в несигнальное состояние. Если для данного события не выполняет ожидание ни один из потоков, оно остается в сигнальном состоянии до момента, когда поток начнет ожидание, после чего он немедленно освобождается. Отметим, что тот же результат можно получить, использовав семафор с максимальным значением счетчика 1.

Если же, с другой стороны, событие сбрасывается вручную, оно остается в сигнальном состоянии до тех пор, пока поток не вызовет функцию ResetEvent для данного события. В этот промежуток времени все ожидающие потоки освобождаются и возможно, что поток начнет ожиание и будет освобожден до сброса события.

Функция PulseEvent освобождает все события, выполняющие ожидание для дан­ного события с ручным сбросом, которое затем сбрасывается автоматически. В случае события с автосбросом функция PulseEvent освобождает единственное ожидающее событие, если таковое существует.

Отметим, что функция ResetEvent используется только после того, как событие с ручным сбросом будет переведено в сигнальное состояние функцией SetEvent. Будь­те осторожны при использовании функции WaitForMultipleObjects для ожидание всех событий, которые должны перейти в сигнальное состояние. Ожидающий поток будет освобожден, только когда все события одновременно окажутся в сигнальном со­стоянии, а некоторые из событий могут быть сброшены из сигнального состояния до освобождения ожидающего потока.

Обзор: четыре модели использования событий

Комбинирование устанавливаемых вручную и автоматически событий с функциями SetEvent и PulseEvent дает четыре различных пути использования событий.

Предупреждение: при неверном использовании события могут привести к возникновению состояния гонок, зависаниям и другим незаметным и трудно выявляемым ошибкам. В таблице описаны четыре возможные ситуации.

Событие с автоматическим сбросом похоже на дверь с пружиной, которая ее захлопывает, тогда как событие с ручным сбросом не имеет пружины, и поэтому так и остается открытым. Если продолжить аналогию, то функция PulseEvent открывает дверь и немедленно закрывает ее после того, как один (для автосброса) или все (для сброса вручную) ожидающие потоки пройдут через дверь. Функция SetEvent откры­вает дверь и оставляет ее открытой.

 

Основная литература: [4] –193 - 260 c., [6] –145 - 190 c.

 

Контрольные вопросы

1) Какие API-функции используются в программе?

2) Какие структуры используются в программе?

3) Выгоды использования ресурсов.

4) Отличия в поведении диалогового окна от обычного.

5) Отличия в описании диалогового окна и меню от других ресурсов.

 

4 Лабораторная работа. Управление файлами

 

Цель работы: изучение основ работы с файлами в Win32.

Задания:

1) Изучить API-функции для работы с файлами и каталогами.

2) Написать программу получения информации о дисках, установленных в компьютере.

3) Написать программу работы с каталогами.

4) Написать программу записи информации в файл и чтения информации из файла.

5) Написать программу поиска файлов.

Методические указания к выполнению

Создание и открытие файлов

Для создания и открытия файлов всех типов служит единственная функция API Win32 – CreateFile. В приложении можно указать, будет ли выполняться чтение из файла, запись в файл или и то, и другое. Можно также указать, предусматривается ли совместное использование этого файла для чтения, записи либо для того и другого.

HANDLE CreateFile (LPCTSTR lpszName, DWORD fdwAccess, DWORD fdwShareMode, LPSECURITY_ATTRIBUTES lpsa, DWORD fdwCreate, DWORD fdwAttrsAndFlags, HANDLE hTemplateFile)

Возвращаемое значение: дескриптор объекта открытого файла или INVALID_HANDLE_FAILURE в случае неудачи.

Параметры

lpszName – указатель на строку, содержащую имя файла, канала или другого именованного объекта, который требуется открыть или создать

fdwAccess определяет режим доступа для чтения или записи значениями GENERIC_READ  и GENERIC_WRITE соответственно. Могут использоваться другие имена констант. Эти значения можно объединить с помощью поразрядного “или”.

fdwShareMode – это объединение поразрядным “или” сочетание приведенных ниже значений:

0 – совместный доступ к файлу не допускается.

FILE_SHARE_READ – другие процессы, включая тот, что сделал данный вызов, могут открывать этот файл для параллельного чтения.

FILE_SHARE_WRITE – разрешается параллельная запись в файл.

Используя блокировку или другие механизмы, программист должен не допустить параллельных модификаций одного и того же файла.

lpsa указывает на структуру SECURITY_ATTRIBUTES.

fdwCreate определяет, надо ли создать новый файл, заменить существующий файл и т.д. Отдельные значения можно объединить оператором поразрядное “или”.

CREATE_NEW – завершается неудачей, если указанный файл уже существует; иначе создается новый файл.

CREATE_ALWAYS – существующий файл заменяется новым.

OPEN_EXISTING – неудача, если файл не существует.

OPEN_ALWAYS – файл открывается и создается, если он не существует.

TRUNCATE_ EXISTING – устанавливается нулевая длина файла. В fdwAccess должен быть задан по крайней мере доступ GENERIC_WRITE.

fdwAttrsAndFlags определяет атрибуты и флаги. Существует 16 флагов и атрибутов. Атрибуты – это характеристики файла, а не открытого дескриптора HANDLE; они игнорируются, когда открывается существующий флаг. Ниже перечислены некоторые важные атрибуты и флаги.

FILE_ATTRIBUTE_NORMAL – может использоваться, только если не установлены никакие другие атрибуты (однако флаги могут быть установлены).

FILE_ATTRIBUTE_ READONLY – приложения не могут не писать в файл, ни удалять его.

FILE_FLAG_DELITE_ON_CLOSE – полезен для временных файлов. Файл удаляется, когда закрывается его последний открытый дескриптор.

FILE_FLAG_OVERLAPPED – важен для асинхронного ввода-вывода. В Windows он имеет значение NULL всегда, кроме устройств последовательного ввода-вывода.

Еще несколько дополнительных флагов определяют способ обработки файла и позволяют Win32 оптимизировать быстродействие и целостность файла.

FILE_FLAG_WRITE_THROUGH – промежуточные кэши немедленно записываются в файл на диске.

FILE_FLAG_NO_BUFFERING – в пространстве пользователя не выполняется буферизация и кэширование, и данные передаются непосредственно в буфера программы и из них.

FILE_FLAG_RANDOM_ACCESS – файл предназначен для произвольного доступа, и Windows будет пытаться оптимизировать кэширование файла.

FILE_FLAG_SEQUENTIAL_SCAN – файл предназначен для последовательного доступа, и Windows соответственно оптимизирует кэширование. Эти два режима доступа не обязательны.

hTemplateFile – дескриптор файла, открытого с GENERIC_READ, определяющий расширенные атрибуты для вновь создаваемого файла, причем значения fdwAttrsAndFlags игнорируются. Обычно этот параметр равен NULL. Параметр hTemplateFile игнорируется, когда открывается существующий файл. С помощью этого параметра можно сделать так, чтобы атрибуты нового файла были такими же, как у существующего.

Закрытие файлов

Дескрипторы закрываются и системные ресурсы освобождаются почти для всех объектов одной универсальной функцией. При закрытии дескриптора также уменьшается на единицу счетчик ссылок объекта.

BOOL CloseHandle (HANDLE hObject)

Возвращаемое значение: TRUE, если функция выполняется успешно; иначе FALSE.

Чтение файлов

BOOL ReadFile ( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped)

Возвращаемое значение: TRUE, если чтение завершается успешно.

Параметры

hfile – дескриптор файла с доступом GENERIC_READ

lpBuffer указывает на буфер памяти для получения входных данных.

nNumberOfBytesToRead – количество байтов, которые нужно прочитать из файла.

lpNumberOfBytesRead – указывает на фактическое число байтов, прочитанное функцией  ReadFile.

lpOverlapped указывает на структуру OVERLAPPED.

Запись в файл

BOOL WriteFile (HANDLE hFile, CONST VOID *lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped)

Возвращаемое значение: TRUE, если функция завершается успешно, иначе FALSE.

Win32 содержит множество функций управления файлами.

Для удаления файла

BOOL DeleteFile (LPCTSTR  lpszFileName)

Копирование файла

BOOL CopyFile (LPCTSTR lpszExistingFile, LPCTSTR lpszNewFile, BOOL fFaillfExists)

CopyFile копирует определенный по имени существующий файл и присваивает копии указанное новое имя. Если файл с новым именем уже существует, он будет заменен, только если fFailIfExists равно FALSE.

Еще две функции служат для переименования, или "перемещения", файла. Эти функции также могут работать с каталогами

BOOL MoveFile ( LPCTSTR  lpszExisting, LPCTSTR lpszNew);

BOOL MoveFileEx (LPCTSTR lpszExisting, LPCTSTR lpSzNew, DWORD fdwFlags)

MoveFile завершается неудачно, если новый файл уже существует; для существующих файлов следует применять MoveFileEx.

Параметры

lpszExisting определяет имя существующего файла или каталога.

lpszNew определяет имя нового файла или каталога, который в MoveFile не должен существовать. Новый файл может находиться в другой файловой системе или на другом диске, но новые каталоги должны быть на том же диске. Если этот параметр имеет значение NULL, существующий файл удаляется.

fdwFlags определяет следующие опции:

MOVEFILE_REPLACE_EXISTING — используется для замены существующего файла;

MOVEFILE_WRITETHROUGH — гарантирует, что функция не возвращает управление, пока скопированный файл не будет переписан из промежуточного буфера на диск;

MOVEFILE_COPY_ALLOWED — когда новый файл находится на другом томе, перемещение осуществляется путем выполнения CopyFile и DeleteFile;

MOVEFILE_DELAY_UNTIL_REBOOT — этот флаг, который не может применяться вместе с movefile_copy_allowed, разрешен только для администраторов и за­держивает фактическое перемещение файла до перезапуска системы.

Перемещение (переименование) файлов связано с несколькими важными ограничениями.

Поскольку в Windows не реализована функция MoveFileEx, ее необходимо заменять последовательностью CopyFile и DeleteFile. Это означает, что в какой-то момент времени будут существовать две копии, что может вызвать проблемы при почти заполненном диске или большом файле. Этот способ влияет на атрибуты файла иначе, чем "настоящее" перемещение.

В именах файлов или каталогов не допускаются подстановочные знаки. Следует указывать конкретное имя.

 

Основная литература: [4] –261 - 351 c.

 

Контрольные вопросы

1) Какие API-функции используются для определения и изменения текущей директории?

2) Какие API-функции используются для создания, копирования, перемещения, удаления файлов?

3) Алгоритм чтения и записи в файл

4) Какие API-функции используются для работы с характеристиками файла?

 

5 Лабораторная работа. Создание динамических библиотек

 

Цель работы: освоить создание динамических библиотек.

Задания:

1) Создать DLL.

2) Написать программу вызова динамической библиотеки, используя явное связывание.

3) Написать программу вызова динамической библиотеки, используя неявное связывание.

4) Написать программу и DLL. Организовать передачу параметров между ними.

5) Написать программу загрузки ресурса из динамической библиотеки.

Методические указания к выполнению

DLL и адресное пространство процесса

DLL представляет собой набор модулей исходного кода, в каждом и которых содержится определенное число функций, вызываемых приложением или другим DLL. Причем  в DLL обычно нет кода, предназначенного для обработки циклов выборки сообщений и создания окон.

Файлы с исходным кодом компилируются и компонуются также, как и при создании EXE-файла, но при компоновке нужно указать ключ /DLL.

Чтобы приложение (или другая DLL) могло вызывать функции, содержащиеся в DLL, образ ее файла нужно сначала спроецировать на адресное пространство вызывающего процесса. Это выполняется за счет неявного связывания при загрузке, либо за счет явного – в период выполнения. Теперь все функции DLL доступны всем потокам этого процесса. Когда поток вызывает из DLL какую-то функцию, то он считывает свои параметры из списка потока и размещает в этом стеке собственные локальные переменные. Кроме того, любые созданные кодом  объекты принадлежат вызывающему потоку или процессу DLL ничем не владеют.

При проецировании образа DLL-файла на адресное пространство процесса система создает также экземпляры глобальных и статических переменных.

Неявное связывание EXE – и DLL – модулей

Неявное связывание (implicit linking) - самый распространенный метод.

Исполняемый модуль (EXE) импортирует функции и переменные из DLL, а DLL– модули экспортирует их в исполняемый модуль. DLL – также может импортировать функции и переменные их других DLL.

Создание DLL-модуля

DLL может экспортировать переменные, функции или С++ классы в другие модули.

При разработке DLL сначала создается заголовочный файл, который включается во все модули исходного кода нашей DLL. Кроме того, его нужно поставлять вместе со своей DLL, чтобы другие разработчики могли включать его в свои модули исходного кода, которые импортируют наши функции или переменные. Единый заголовочный файл, используемый при сборке DLL и любых исполняемых модулей, существенно облегчает поддержку приложения.

При компиляции исходного файла DLL MYLIBAPI определяется как __declspec (dllexport) до включения заголовочного файла MyLib.h. Такой модификатор означает, что данная переменная, функция или C++ класс экспортируется  из DLL.

Также следует обратить внимание, что в файле MyLibFile1.cpp перед экспортируемой переменной или функцией не ставится идентификатор MYLIBAPI. Он здесь не нужен: проанализировав заголовочный файл, компилятор запоминает, какие переменные и функции являются экспортируемыми.

Идентификатор MYLIBAPI включает extern. Модификатор extern не даёт компилятору искажать имена переменных или функции, и они становятся доступными исполняемым модулям, написанным на С, С++ или любом другом языке программирования. Этим модификатором можно пользоваться только в коде С++, но ни в коем случае ни в коде на стандартном С.

Мы рассмотрели, как используется заголовочный файл в исходных файлах DLL, а в исходных файлах ЕХЕ-модуля MYLIBAPI определять не надо: включая заголовочный файл, вы определяете этот идентификатор как – DLLSPEC(DLLIMPORT), и при помещении исходного кода ЕХЕ-модуля компилятор поймёт, что переменные и функции импортируются из DLL.

Что такое экспорт

Если перед переменной, прототипом функции или С++ классом указан модификатор – _dlspec(dllexport), компилятор Microsoft C/C++ встраивает в конечный obj-файл дополнительную информацию. Она понадобится при сборке DLL из OBJ-файлов.

Обнаружив такую информацию, компоновщик создает LIB-файл со списком идентификаторов, экспортируемых из DLL. Этот LIB-файл нужен при сборке любого EXE-модуля, ссылающегося на такие идентификаторы. Компоновщик также вставляет в конечный DLL-файл таблицу экспортируемых идентификаторов – раздел экспорта, в котором содержится список (в алфавитном порядке) идентификаторов экспортируемых функций, переменных и классов. Туда же помещается относительный виртуальный адрес (relative virtual address, RVA) каждого идентификатора внутри DLL-модуля.

Создание EXE-модуля

Вот пример исходного кода EXE-модуля, который импортирует идентификаторы, экспортируемые DLL, и ссылается на них внутри в процессе выполнения.

//Модуль: MyExeFilel.cpp

//Сюда включаются стандартные заголовочные файлы Windows и библиотеки С

#include <windows.h>

//включаем экспортируемые структуры данных, идентификаторы, функции и переменные

#include “MyLib\MyLib.h”

….

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, LPTSTR pszCmdLine, int){int nLeft=10, nRight=25; TCHAR sz[100]; wsprintf(sz, TEXT(“%d + %d = %d”), nLeft, nRight, Add(nLeft, nRight)); MessageBox(NULL, sz, TEXT(“Calculation”), MB_OK); wsprintf(sz, TEXT(“The result from the last Add is: %d”), g_nResult); MessageBox(NULL, sz, TEXT(“Last Result”), MB_OK); return (0);}

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

MYLIBAPI  в исходных файлах EXE-модуля до заголовочного файла DLL не определяется. Поэтому при компиляции приведенного выше кода MYLIBAPI за счет заголовочного файла MyLib.h будет определен как __declspec(dllimport). Встречая такой модификатор перед именем переменной, функции или С++ класса, компилятор понимает, что данный идентификатор импортируется из какого-то DLL –модуля.

Далее компоновщик собирает все OBJ-модули в конечный EXE-модуль. Для этого он должен знать, в каких DLL содержаться импортируемые идентификаторы, на которые есть ссылки в коде. Информацию об этом он получает из передаваемого ему LIB-файла (в котором указан список идентификаторов, экспортируемых DLL)

Что такое импорт

Импортируя идентификатор, необязательно прибегать к __declspec(dllimport) – можно использовать стандартное ключевое слово extern языка С. Но компилятор создаст чуть более эффективный код, если ему будет заранее известно, что идентификатор, на который мы ссылаемся, импортируется из LIB-файла DLL –модуля.

Разрешая ссылки на импортируемые идентификаторы, компоновщик создаст в конечном EXE-модуле раздел импорта (import section).

В нем перечисляются DLL, необходимые этому модулю, и идентификаторы, на которые есть ссылки из всех используемых DLL.

Выполнение EXE-модуля

При запуске EXE-файла загрузчик операционной системы создает для его процесса виртуальное пространство и проецирует на него исполняемый модуль. Далее загрузчик анализирует раздел импорта и пытается спроецировать все необходимые DLL на адресное пространство процесса.

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

1) Каталог, содержащий EXE-файл

2) Текущий каталог процесса

3) Системный каталог Windows

4) Основной каталог Windows

5) Каталоги, указанные в переменной окружения PATH

Проецируя DLL-модули на адресное пространство, загрузчик проверяет в каждом из них раздел импорта. Если у DLL есть раздел импорта (что обычно и бывает), загрузчик проецирует следующий DLL-модуль.  При этом загрузчик ведет учет загружаемых DLL, и проецирует их только один раз, даже если загрузки этих DLL требуют и другие модули.

Найдя и спроецировав на адресное пространство процесса все необходимые DLL-модули, загрузчик настраивает ссылки на импортируемые идентификаторы. Для этого вновь просматривает разделы импорта в каждом модуле, проверяя наличие указанного идентификатора в соответствующей DLL.

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

Загрузчик всех этих DLL и настройка ссылок занимает какое-то время. Чтобы сократить время загрузки приложения, нужно модифицировать базовые адреса EXE- и DLL-модулей и провести их связывание.

Явная загрузка DLL и связывание идентификаторов

Чтобы поток мог вызвать функцию из DLL-модуля, DLL надо спроецировать на адресное пространство процесса, которому принадлежит этот поток. Делается это двумя способами:

1) Код приложения просто ссылается на идентификаторы, содержащиеся в DLL, и тем самым заставляет загрузчик неявно загружать (и связывать) нужную DLL при запуске приложения.

2) Явная загрузка и связывание требуемой DLL в период выполнения приложения. Иначе говоря, поток явно загружает DLL в адресное пространство процесса, получает виртуальный адрес необходимой DLL-функции и взывает ее по этому адресу. Изящество этого подхода в том, что все происходит в уже выполняемом приложении.

 

Основная литература: [4] – 430 - 457 c.

 

Контрольные вопросы

1) Виды связывания.

2) Параметры процедуры входа

3) Какие API-функции используются при работе с DLL?

 

6 Лабораторная работа. Управление процессами, потоками

 

Цель работы: изучение основных принципов управления процессами, потоками и объектов синхронизации.

Задания:

1) Написать программу с использованием процессов.

2) Написать программу с использованием потоков.

3) Написать программу с использованием приоритетов потоков.

4) Написать программу с использованием событий.

5) Написать программу с использованием критических секций.

6) Написать программу с использованием семафоров.

7) Написать программу с использованием мьютексов.

Методические указания к выполнению

Управление процессами

Процесс содержит собственное независимое виртуальное адресное пространство с кодом и данными, защищенными от других процессов. Каждый процесс, в свою очередь, включает в себя один или более независимо выполняющихся потоков. Процесс может создавать внутри себя новые потоки и новые независимые процессы, а также управлять сообщением и синхронизацией объектов.

Создавая процессы и управляя ими, приложения могут одновременно выполнять задачи по обработке файлов, производить вычисления и взаимодействовать с другими системами в сети. Возможно даже использование нескольких процессоров для ускорения обработки.

Процессы и потоки в Windows

Каждый процесс содержит один или более потоков. Поток в Windows — основная единица выполнения. Планирование потоков проводится на основе обычных факто­ров: доступности ресурсов, таких как процессоры и физическая память, приоритетов, справедливости распределения ресурсов и т.д. Windows поддерживает симметричную многопроцессорную обработку, поэтому потоки могут распределяться по отдельным процессорам.

С точки зрения программиста, каждый процесс в Win32 включает компоненты, перечисленные ниже.

- Один или несколько потоков.

- Виртуальное адресное пространство, отличное от адресных пространств других процессов, за исключением случаев явного разделения памяти. Отметим, что отображаемые в память файлы разделяют физическую память, но разные процессы для доступа к отображенному файлу будут использовать разные виртуаль­ные адреса.

- Один или более сегментов кода, включая код DLL.

- Один или более сегментов данных, содержащих глобальные переменные.

- Строки окружения с информацией о переменных окружения, таких как текущий путь поиска.

- Память кучи процесса.

- Такие ресурсы, как открытые дескрипторы и другие кучи.

- Все потоки процесса совместно используют код, глобальные переменные, строки окружения и ресурсы. Каждый поток планируется независимо. Поток включает описанные ниже элементы.

- Стек вызовов процедур, прерываний, обработчиков исключений и автоматических данных.

- Локальная память потока (Thread Local Storage — TLS) — массивы указателей, которые дают процессу возможность выделять память для создания собственного уникального окружения данных.

- Параметр стека, полученный от потока, создавшего данный, и обычно уникаль­ный для каждого потока.

- Структура контекста, управляемая ядром, со значениями аппаратных регистров.

Создание процессов

Функция CreateProcess — основная функция для управления процессами в Win32. Она создает процесс с одним потоком. Так как процесс требует наличия кода, в вызове функции CreateProcess необходимо указывать имя исполняемого файла программы.

Часто принято говорить о родительских и дочерних процессах, но такие отношения в действительности не поддерживаются в Win32. Просто удобно называть процесс, ко­торый порождает дочерний процесс, родительским.

Функция CreateProcess для обеспечения гибкости и мощности имеет десять па­раметров. На первый случай можно просто использовать их значения по умолчанию.

В указанную при вызове структуру возвращаются два дескриптора: для процесса и для потока. Функция CreateProcess создаст новый процесс с первичным потоком. В примерах программ оба дескриптора, когда потребность в них исчезает, всегда аккуратно закрываются во избежание утечки ресурсов. На­пример, если перед закрытием дескриптора потока не завершить этот поток, функция CloseHandle лишь удалит ссылку на поток.

BOOL CreateProcess (LPCTSTR lpszImageName, LPTSTR lpszCommandLine,  LPSECURITY_ATTRIBUTES lpsaProcess,LPSECURITY_ATTRIBUTES lpsaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID lpvEnvironment, LPCTSTR lpszCurDir, LPSTARTUPINFO lpsiStartInfo, LPPROCESS_INFORMATION lppiProcInfo);

Возвращаемое значение: TRUE только в случае, если процесс и поток были успешно созданы.

Выход из процесса и его завершение

После того как процесс закончен, он вызывает функцию ExitProcess с кодом завершения.

Эта функция ничего не возвращает, вызвавший ее процесс и все его потоки завершаются.

Для определения кода завершения другой процесс может использовать функцию GetExitCodeProcess.

BOOL GetExitCodeProcess ( HANDLE hProcess, LPDWORD lpdwExitCode);

Процесс, определенный переменной hProcess, должен иметь права доступа PROCESS_QUERY_INFORMATION (см. функцию OpenProcess). Параметр lpdwExitCode указывает на переменную типа dword, в которую будет помещено значение. Одно из возможных значений — STILL_ACTIVE; оно указывает, что процесс не был завершен.

Один процесс может завершить другой, если его дескриптор имеет флаг доступа PROCESS_TERMINATE. Функция, завершающая процесс, также определяет код выхода из процесса.

BOOL TerminateProcess ( HANDLE hProcess, UINT uExitCode)

Ожидание завершения процесса

Простейший и наиболее ограниченный способ синхронизации с другим процессом состоит в ожидании его полного завершения. Здесь представлены функции общего назначения Win 32 для ожидания, обладающие некоторыми интересными свойствами.

- Эти функции могут быть использованы для большого числа различных типов объектов; дескрипторы процессов — это лишь первое их применение.

- Функции могут ожидать завершения одного процесса, первого из нескольких указанных или всех процессов в группе.

- Для них может быть использована рекомендательная блокировка по времени.

Ниже показаны две функции ожидания общего назначения, которые наиболее часто будут использоваться далее.

Параметр lphObjects указывает на дескриптор одного процесса (типа hObject) или на массив различных объектов. Значение параметра cObjects (размер массива) не должно превышать константу maximum_wait_objects (в настоящее время ее значение 64 определено в заголовочном файле winnt.h).

 

Основная литература: [1] – 167-260 c.

 

Контрольные вопросы

1) На чем основана вытесняющая многозадачность?

2) Назначение объектов синхронизации.

3) Виды объектов синхронизации.

 

7 Лабораторная работа. Исследование структуры PE-формата

 

Цель работы: исследование формата исполняемых файлов Windows с использованием отладчиков, дизассемблеров.

Задания:

1) Запуск программы под отладчиком и анализ ее работы.

2) Исследование программы с помощью дизассемблера.

3) Изучение общей структуры файлов.

4) Изучение заголовков исполняемого файла.

5) Изучение таблицы объектов.

6) Изучение разделов в исполняемом файле.

7) Изучить экспорт функций и механизм экспорта.

8) Изучить импорт функций и механизм импорта.

Методические указания к выполнению

Исключения и их обработчики

Если в программе не будет какой-либо обработки исключений, непреднамеренная исключительная ситуация, например, разыменование нулевого указателя или деление на нуль, приведет к немедленной остановке программы. Это может вызвать проблему, если, например, программа создала временный файл, который должен быть удален перед завершением работы. SEH позволяет задать блок кода — обработчик исключения, который сможет удалить временный файл, когда произойдет исключение.

Блоки Try и Except

Сначала определим, какие блоки кода нужно контролировать, и снабдим их обработчиками исключений. Можно контролировать функцию целиком или иметь отдельные обработчики исключений для разных блоков кода и функций.

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

- Возникают обнаруживаемые ошибки, включая ошибки системных вызовов, и нужно исправить ошибку, а не завершать программу.

- Широко применяются указатели, так что существует вероятность разыменования указателей, неинициализированных должным образом.

- Выполняется много действий с массивами, и индексы массивов могут выйти за доступные пределы.

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

- В коде вызывается функция, которая может породить исключительную ситуацию либо преднамеренно, либо из-за недостаточной проверки.

В примерах как этой главы, так и всей книги для установления контроля за блоком кода создаются следующие блоки try и except:

try   {  /*  Блок контролируемого  кода  */ }

except   {выражение_фильтра) { /*  Блок обработки исключения  */  }

Заметьте, что try и except — ключевые слова, распознаваемые компилятором.

Блок try является частью обычного кода приложения. Если в блоке происходит исключение, операционная система передает управление обработчику исключения — блоку кода, связанному с ключевым словом except. Последующие действия определяются значением выражение_фильтра.

Следует заметить, что исключение может произойти и в блоке, который вложен в блок try; в этом случае поддержка времени выполнения "разворачивает" стек, находит обработчик исключения и передает ему управление. То же самое происходит, когда исключение возникает внутри функции, вызываемой в блоке try.

Выражения фильтра и их значения

Выражение_фильтра в операторе except вычисляется немедленно после того, как происходит исключение. Обычно это литеральная константа, вызов функции фильтра или условное выражение. Во всех случаях выражение должно возвратить од­но из трех значений.

1. EXCEPTION_EXECUTE_HANDLER система выполняет блок except. Это обычная ситуация.

2. EXCEPTION_CONTINUE_SEARCH — система игнорирует обработчик исключения последовательно ищет его во вложенных блоках, пока не находит.

3. EXCEPTION_CONTINUE_EXECUTION — система немедленно возвращает управление в точку, в которой произошло исключение. После некоторых исключений продолжение невозможно, и, если программа пытается продолжить работу, немедленно генерируется другое исключение.

Коды исключений

Блок except или выражение фильтра может определить конкретное исключение с помощью функции

DWORD GetExceptionCode (VOID)

Код исключения должен быть получен немедленно после исключения. Поэтому сама функция фильтра не может вызывать GetExceptionCode (это ограничение установлено в компиляторе). Обычно эта функция вызывается в выражении фильтра, где кодом исключения служит параметр заданной пользовате­лем функции фильтра.

except (MyFilter (GetExceptionCode ())) { }

В этой ситуации функция фильтра определяет и возвращает значение выражения фильтра, которое должно быть одним из трех значений, указанных выше. Функция может определить на основе кода исключения свое возвращаемое значение; напри­мер, фильтр может передавать исключения при операциях с плавающей запятой внешнему обработчику (возвращая EXCEPTION_CONTINUE_SEARCH), а нарушение доступа к  памяти обрабатывать в текущем обработчике (возвращая EXCEPTION_EXECUTE_HANDLER).

GetExceptionCode может возвращать множество различных значений кодов исключений. Все эти коды подразделяются на несколько категорий.

Программные нарушения, например:

- EXCEPTION_ACCESS_VIOLATION — попытка читать или записывать по виртуальному адресу, к которому процесс не имеет доступа;

- EXCEPTION_DATATYPE_MISALIGNMENT — многие типы процессоров требуют, например, чтобы данные типа DWORD были выровнены по 4-байтовым границам;

- EXCEPTION_NONCONTINUABLE — значение выражения фильтра было exception_continue_execution, но после данного исключения продолжение невозможно.

- Исключения, вызываемые функциями  распределения  памяти, HeapAlloc  и HeapCreate, если в них используется флаг HEAP_GENERATE_EXCEPTIONS. Значение  кода будет STATUS_NO_MEMORY либо EXCEPTION_ACCESS_VIOLATION.

- Определяемый пользователем код исключения, генерируемый функцией RaiseException, которая рассматривается ниже.

- Множество разнообразных кодов по арифметическим операциям (особенно с плавающей запятой), например:

EXCEPTION_INT_DIVIDE_BY_ZERO и EXCEPTION_FLT_OVERFLOW

- Исключения, используемые отладчиками, например EXCEPTION_BREAKPOINT и EXCEPTION_SINGLE_STEP.

Существует также альтернативная функция, вызываемая только из выражения фильтра, которая возвращает дополнительную (в том числе зависящую от типа процессора) информацию:

LPEXCEPTION_POINTERS GetExceptionInformation (VOID)

Структура EXCEPTION_POINTERS содержит как зависимую от процессора, так и независимую информацию, сгруппированную в двух других структурах:

typedef struct _EXCEPTION_POINTERS {PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord;} EXCEPTION_POINTERS;

EXCEPTION_RECORD содержит элемент для ExceptionCode, с тем же набором значений, которые возвращаются функцией GetExceptionCode. Еще один элемент EXCEPTION_RECORD —  ExceptionFlags —  имеет  значение 0 или EXCEPTION_NONCONTINUABLE, что позволяет функции фильтра определить, следует ли пытаться продолжать выполнение. Другие элементы включают адрес виртуальной памяти ExceptionAddress и массив параметров ExceptionInformation. В случае EXCEPTION_ACCESS_VIOLATION первый элемент указывает, было ли это нарушение при записи (1) или при чтении (0), а второй элемент — адрес виртуальной памяти.

Второй элемент EXCEPTION_POINTERS ContextRecord — содержит зависимую от процессора информацию. Есть разные структуры для каждого типа процессора; информацию об этом можно найти в <winnt.h>.

 

Основная литература: [5] – 95 - 184 c.

 

Контрольные вопросы

1) Cигнатуры исполняемых файлов.

2) Способы исследования программ.

3) Назначение заголовков исполняетого файла.

4) Назначение разделов в исполняемом файле.

 

Список литературы 

1.     Джеффри Рихтер. Windows. Создание эффективных Win32- приложений с учетом специфики 64-разрядной версии Windows.-СПб., М., Харьков, Минск: “Русская редакция”, “Питер”, 2008 (Серия: для профессионалов).

2.     Ал Вильямс. Системное программирование в Windows. – СПб.: Питер, 2007.

3.     Джонсон М. Харт. Системное программирование в среде Win32. – М.: Издательский дом “Вильямс”, 2007.

4.     Румянцев П.В. Азбука программирования в Win32 API. – М.: Горячая линия – Телеком, 2006.

5.     Румянцев П.В. Работа с файлами в Win32. – М.: Горячая линия – телеком, 2005.

6.     Ганеев Р.М. Проектирование интерфейса пользователя средствами Win32 API. – М.: Горячая линия – Телеком, 2004.

 

Содержание

 

1 Лабораторная работа. Разработка консольного приложения

3

2 Лабораторная работа. Разработка пользовательского интерфейса

5

3 Лабораторная работа. Использование ресурсов

6

4 Лабораторная работа. Управление файлами

8

5 Лабораторная работа. Создание динамических библиотек

12

6 Лабораторная работа. Управление процессами, потоками

16

7 Лабораторная работа. Исследование структуры PE-формата

19

Список литературы

23

 

 Сводный план 2011 г. поз. 248