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

ЖАНРЫ

Программирование на Visual C++. Архив рассылки

Jenter Алекс

Шрифт:

ПРИМЕЧАНИЕ

Я не стал рассматривать еще два возможных варианта стартовой функции: wmain или wWinMain, предназначенных для проектов, компилируемых в Unicode. Кроме того, при создании DLL имеется еще один вариант стартовой функции – DllMain.

Точка входа в программу

Функция [w]main или [w]WinMain, с которой начинается выполнение программы, вовсе не является точкой входа исполняемого модуля! На самом деле, программа на C++ начинает работу с выполнения специальной процедуры инициализации. Что касается Win32, то адрес этой процедуры и содержится в поле AddressOfEntryPoint

заголовка portable executable (pe) выполняемого файла. Она представляет собой обычную функцию C, описанную с соглашением о вызовах __stdcall. В зависимости от настроек проекта, в Visual C++ эта функция может называться [w]mainCRTStartup, [w]WinMainCRTStartup или _DllMainCRTStartup (символ 'w' добавляется к имени для Unicode-проектов). Конкретно же для сборки приложения имя функции-точки входа можно задать опцией линкера /entry. Умолчанием для visual c++ является "maincrtstartup". Все сказанное справедливо и для некоторых других компиляторов C++ для Win32.

Что же происходит во время ее выполнения? Вот типичный сценарий работы такой функции (случай DLL здесь не рассматривается).

• Инициализируются переменные CRT (такие, как errno и osver). Многопоточная библиотека требует особой инициализации.

• Происходит инициализация динамической памяти (кучи).

• Инициализируется среда обработки ошибок в вычислениях с плавающей точкой. Это необходимо не только для библиотечных функций (таких, как sqrt), но и для преобразований между целочисленными и плавающими типами данных.

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

• В случае необходимости, происходит инициализация консоли и привязка стандартного вывода к файловым дескрипторам C. При старте исполняемого файла, у которого в уже упомянутом заголовке PE значение поля Subsystem равно 3 (Windows character-mode executable), создается консоль. Это значение можно задать опцией линкера /subsystem. Выбор подсистемы выполнения также влияет на выбор стартовой функции (если ее имя не задано явно). Умолчанием является "console".

• Происходит вызов цепочки функций инициализации CRT и конструкторов глобальных переменных (подробнее об этом – в следующем разделе).

• И лишь после этого вызывается функция [w]main или [w]WinMain. Коротко можно сказать, что функция xxxCRTStartup вызывает соответствующую функцию xxx.

• Программа работает.

• Выполняется последовательность действий по очистке, к которой мы еще вернемся.

• И, наконец, происходит завершение процесса.

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

Так, например, при вызове компилятора в командной строке таким образом:

cl test.cpp user32.lib

мы получим консольную программу и сообщение "Hello from main" (вспомните, что говорилось об умолчаниях).

А вызвав компилятор вот так:

cl test.cpp user32.lib /link /entry:WinMainCRTStartup /subsystem:console

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

Код инициализации глобальных переменных

Как в VC++ реализован вызов цепочки функций инициализации/завершения?

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

сегменты такого типа будут при сборке объединены в алфавитном порядке в один сегмент. Таким образом, в момент старта программы в памяти будет находиться массив указателей на функции, при вызове которых и произойдут необходимые действия. В стартовом коде CRT VC этим занимается функция _initterm.

А почему здесь используется термин "функции инициализации/завершения " вместо терминов "конструкторы/деструкторы"?

Напомню, что стандарт языка C++ разрешает инициализацию переменных с помощью неконстантных выражений. Если переменная (даже простого типа) описана в глобальной области, то ее инициализатор должен быть выполнен до вызова функции main/WinMain:

int len = strlen("Hello, world!");

Обработка в этом случае ничем не отличается от инициализации экземпляра класса имеющего конструктор.

Код завершения

Упомянув инициализацию CRT, нельзя умолчать о коде очистки, или завершения. В нем выполняются действия обратного характера (и, в том числе, деструкторы глобальных переменных). Что действительно заслуживает описания, так это то, что код очистки можно вызвать собственноручно. Да-да, он содержится в функции exit. Если же не вызвать ее явно, то она вызовется после возврата из main/WinMain. Наиболее выразительную реализацию вышесказанного я встретил однажды в исходных файлах CRT компилятора WATCOM C++:

exit(main(__argv, __argc, __envp));

То есть, можно сказать, что все выполнение программы имеет целью получение параметра для функции exit. :)

ПРИМЕЧАНИЕ

Вообще-то, exit (вернее, возможность ее прямого вызова) является, скорее, "пережитком" со времен программирования на C. При вызове этой функции из программы на C++ не выполнятся деструкторы для локальных переменных (что естественно, поскольку, в отличие от глобальных объектов, их деструкторы нигде не зарегистрированы). Кроме того, вызов exit из деструктора может привести к входу программы в бесконечный цикл, так что не злоупотребляйте этой функцией.

Со времен создания библиотеки языка C осталась и такая возможность, как регистрация цепочки обработчиков завершения с помощью функций atexit/_onexit. Функции, зарегистрированные вызовом atexit/_onexit, будут вызваны в ходе завершения программы в порядке, обратном порядку их регистрации. Для программы на C++ с этой целью лучше воспользоваться глобальными деструкторами.

На самом деле, в программе на VC регистрация деструкторов глобальных объектов также выполняется с помощью внутреннего вызова atexit после вызова конструктора. Это имеет довольно веские основания: если конструктор объекта вызван не был, то не будет вызван и его деструктор. Но, в любом случае, это – деталь реализации, на которую полагаться не стоит.

Внутри exit содержится вызов функции более низкого уровня – _exit. Ее вызов не приведет к вызову деструкторов и exit-обработчиков, а только выполнит самую необходимую очистку (не буду вдаваться в подробности, замечу только, что при этом вызываются C-терминаторы (функции из таблицы в сегментах "CRT$XT[A-Z]"), в частности, подчищается low-level i/o) и завершит программу вызовом функции Windows API ExitProcess.

И, наконец, функция abort является способом "пожарного" завершения программы. Она выводит диагностическое сообщение и также вызывает _exit для завершения процесса.

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

Уменьшаем размер выполняемого модуля

Но в нашем примере нет ничего, что потребовало бы использовать CRT. Более того, включив оптимизацию по размеру (/O1) и генерацию карты исполняемого файла (Generate Link Map, /Fm), можно заметить, что размер функции main – всего 23 байта. А размер выполняемого модуля составляет около 36 килобайт. Неужели нельзя его немного уменьшить?

Поделиться с друзьями: