Статьи по Assembler

       

Старт и завершение приложений


Поэт утверждал, что-де "все начинается с любви". Поэтам вообще свойственно сказануть что-нибудь этакое не подумав, получить гонорар - и в ресторан, а вы потом разбирайтесь, чего он имел ввиду. Например, предположим, имеется пара гомо сапиенс противоположных полов, выращенная до репродуктивного возраста в условиях, гарантированно исключающих ознакомление с процессом размножения двуполых животных вообще и вида гомо сапиенс в частности. Вопрос: сможет ли указанная пара, руководствуясь исключительно собственными наблюдениями разницы физиологического строения своих организмов и инстинктом, реализовать репродуктивную функцию? Низшие существа, как-то: хомячки - могут, проверено. А мы, цари природы, сможем? Разум не помешает? Ой, что-то сомнительно. Похоже, в процессе эволюции мы прикупили себе одно средство выживания - разум, заплатив за него другим - способностью существовать вне информационного поля социума.

Впрочем, что касается приложений win32, то здесь поэт точно был неправ. В этом случае все начинается с того, что вызывающая программа (например, Проводник) подготавливает и вызывает функцию API CreateProcess.

Ту же задачу, в принципе, решают и устаревшие функции WinExec и LoadModule. На самом деле на сегодняшний день они представляют собой всего лишь реализации той же функции CreateProcess.

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

Функция CreateProcess:

  • подготавливает для вновь создаваемого процесса рабочую среду (контекст, context), в которую входят: виртуальное адресное пространство, исполняемый код и данные, набор дескрипторов объектов, набор переменных окружения, базовый приоритет, минимальный и максимальный рабочие наборы страниц памяти
  • в процессе подготовки контекста загружает PE-файл приложения. (Большинство из перечисленных выше элементов контекста определяются содержимым этого файла.)
  • создает в рамках контекста процесса первичный поток (нить, thread) приложения и запускает его исполнение через точку входа приложения. Обычно точкой входа служит скрытая от прикладного программиста функция _WinMainCRTStartup, содержащаяся в runtime-библиотеке C/C++, подключаемой к приложению на этапе его сборки. В приложениях, написанных на ассемблере, без подключения runtime-библиотеки, точкой входа объявляется функция, подготовленная пользователем, обычно WinMain. (Подробнее...)

  • Функция _WinMainCRTStartup, если таковая имеется, подготавливает рабочую среду для runtime-библиотеки, а также значения параметров для функции WinMain, после чего передает управление функции WinMain.

    И, наконец, функция WinMain делает все, что придумает прикладной программист.

    Несколько пояснений про элементы контекста. Поскольку assembler.ru не ставит своей целью продублировать всю имеющуюся в природе документацию на Windows, то эти пояснения будут самыми общими.


    • виртуальное адресное пространство (virtual address space) - это набор адресов памяти от 0 до 0ffffffffh, доступных данному процессу. Каждый процесс имеет свое собственное виртуальное адресное пространство, одно для всех потоков процесса. Физически в каждый момент времени в оперативной памяти присутствует только небольшая часть из 4 Гбайт, доступных процессу, как правило, только то, что ему необходимо для работы. Для этого используется механизм страничной организации. Виртуальное адресное пространство разбито на страницы по 4 Кбайта. Страницы по мере необходимости загружаются в оперативную память или удаляются из нее. Это происходит автоматически, прозрачно для прикладного программиста
    • исполняемый код и данные (executable code and data) - это совокупность кода и данных, загружаемых из соответствующих секций PE-файла приложения. Одни и те же фрагменты кода могут одновременно использоваться несколькими нитями процесса
    • дескрипторы объектов (object handles) - это уникальный для данного процесса набор 32-битных идентификаторов, обеспечивающих обращение к самым разнообразным объектам рабочей среды: файлам, элементам графического интерфейса, процессам, потокам и пр.
    • переменные окружения (environment variables) - это пришедший из DOS и UNIX набор текстовых пар имя=значение. Каждый процесс может иметь собственное окружение, либо пользоваться окружением вызвавшего его процесса
    • базовый приоритет (base priority) - это значение в интервале от 0 до 31, определяющее приоритет каждого потока в многозадачной среде. В зависимости от базового приоритета распределяется процессорное время между всеми активными в данный момент потоками. Базовый приоритет формируется из класса приоритета, который присваивается каждому процессу и уровня приоритета, присваиваемого каждому потоку внутри процесса
    • минимальный рабочий набор (minimum working set) - минимально необходимый для существования активного процесса набор страниц памяти
    • максимальный рабочий набор (maximum working set) - наибольший набор страниц памяти, который может потребоваться активному процессу единовременно




    В ОС Windows существует множество способов закончить работу приложения. В MSDN сказано, что это произойдет, если:


    • любой из потоков приложения вызовет функцию ExitProcess
    • первичный поток приложения завершится командой ret Завершение первичного потока вызовом функции ExitThread в случае наличия других потоков не приведет к завершению приложения.


    • завершится (любым способом) последний из потоков приложения
    • любой из потоков приложения вызовет функцию TerminateProcess
    • консольное приложение получит от пользователя сигнал CTRL+C (CTRI+BREAK)
    • завершится работа системы (shut down) или работа пользователя в системе (log off).


    Надо сказать, что этот список не окончателен. Николай Критский (nkritsky@mail.ru) подсказал еще и такой экзотический, но вполне работоспособный вариант: функцией SetErrorMode отключить выдачу системных сообщений об ошибках, а затем искусственно создать такую ошибку (например, выполнив деление на 0). Возникшая при этом исключительная ситуация приведет к завершению работы приложения.

    На самом деле перечисленное разнообразие - только кажущееся, так как некоторые из указанных вариантов - это всего лишь скрытый вызов все той же функции ExitProcess. Это касается и ret в первичном потоке, и CTRL+C для консольного приложения, и даже варианта Николая Критского. Фактически в этих вариантах управление передается в существующие по умолчанию runtime-модуль, процедуру поддержки консоли или обработчик исключительной ситуации, которые и вызовут функцию ExitProcess.

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


    1. Все занятые приложением dll-библиотеки будут освобождены, то есть для каждой из них будет вызвана входная функция со параметрами отключения. При этом внутренние счетчики занятости библиотек будут декрементированы, и те библиотеки, счетчики которых достигнут 0, будут выгружены из системы.
    2. Будут закрыты все дескрипторы объектов, существовавшие в приложении
    3. Будет завершена работа всех потоков приложения
    4. Объект приложения перейдет в состояние "установлен", так что если кто-то в системе ждал завершения работы нашего приложения, сразу узнает об этом
    5. Все объекты потоков приложения также перейдут в состояние "установлен"
    6. Статус завершения процесса изменится со значения STILL_ACTIVE на значение, переданное функции ExitProcess


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

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

    См. также статьи минимальное приложение и параметры функции WinMain.


    Содержание раздела