Программирование на Visual C++. Архив рассылки - Алекс Jenter
Шрифт:
Интервал:
Закладка:
Внутри 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 килобайт. Неужели нельзя его немного уменьшить?
Конечно, такие способы существуют, и некоторые из них я опишу ниже. Но важно понимать, что каждый из них не является общим решением (иначе именно он использовался бы по умолчанию), и имеет свои недостатки.
Использование внешней библиотеки CRTОткомпилируем нашу программу следующей командой:
cl /MD test.cpp user32.lib
Размер полученного в результате EXE-файла составляет около 16 килобайт. Что за чудеса? Куда делась половина исполняемого модуля? Неужели он "похудел" за счет исключения CRT?
И да, и нет. Опция компилятора /MD указывает использовать для сборки библиотеку MSVCRT.LIB. В ней содержится только тот набор кода, который позволяет линкеру разрешить внешние связи. А сам код CRT находится в динамической библиотеке MSVCRT.DLL в системном каталоге windows. Эта многопоточная библиотека используется и некоторыми бесплатными компиляторами C/C++ для Windows, например, MinGW.
Такое решение достаточно удобно, если проект состоит из нескольких модулей – каждый из них станет меньше на объем рантайма. Кроме того, оно позволяет Microsoft исправлять ошибки в уже выпущенных программах простой заменой старой DLL на исправленную версию. Этот подход активно используется многими разработчиками, использующими библиотеку MFC: если в опциях проекта выбрать "Use MFC in a shared DLL", то придется использовать динамическую версию CRT, иначе проект попросту не соберется. В интегрированной среде версия CRT выбирается в свойствах проекта: на закладке C/C++ в категории Code Generation.
Плохая новость заключается в том, что MSVCRT.DLL существует не на всех версиях Windows. Она начала поставляться в составе ОС, начиная с Windows 95 OSR2. Приложение, запущенное в системе без этой библиотеки, выполняться не будет. Правда, таких систем становится все меньше и меньше.
Уменьшение выравнивания файловых секцийВозможно, владельцы Visual C++ 5.0 заметили, что у них в результате получаются EXE-файлы куда меньшего размера, чем сказано здесь. Дело в том, что компоновщик версии 5.0 использовал выравнивание секций исполняемого файла на величину 512 байт. Начиная же с версии 6.0, при сборке приложения используется другая величина выравнивания – 4К. Это позволяет быстрее загружать такой файл в Windows 98 и более новых версиях ОС.
Вернуть прежнюю величину выравнивания можно, задав недокументированную опцию компоновщика /opt:nowin98:
cl /MD test.cpp user32.lib /link /opt:nowin98
Размер EXE в результате составляет менее 3-х килобайт! Но не забудьте, что такой файл будет медленнее загружаться в память, и что он по-прежнему требует наличия MSVCRT.DLL.
Радикальные меры: отказываемся от CRT StartupЕсли ампутация кажется вам разумной хирургической операцией, то стартовый код CRT можно выбросить из программы совсем.
Что это означает? Отказавшись от некоторых привычных удобств, которые предоставляет CRT, можно писать на C/C++, не используя возможностей, которые требуют поддержки со стороны CRT.
В мире Windows API такое решение не пугает многих. Взгляните, например, на NullSoft Installer
В самом деле, для файловых операций можно использовать функции Win API, вместо динамической памяти C++ использовать кучу (хип) Windows, для форматирования можно использовать wsprintf вместо sprintf, для сравнения строк – lstrcmp вместо strcmp и т.д.
При этом важно понимать, что CRT – это обычная библиотека, функции которой вполне можно вызывать из такой программы (как и из программы на ассемблере). Главное – это отказаться от функций, которые влекут за собой включение раздутого кода инициализации (или, в крайнем случае, включить его необходимую часть самостоятельно).
Мэтт Питрек, давний ведущий колонки "Under The Hood" в Microsoft Systems Journal (ныне – MSDN Magazine), посвятил этому вопросу цикл статей в MSJ под общей тематикой "Code Liposuction" ("обезжиривание кода"). Интересующиеся могут найти их в архиве Periodicals MSDN.
Более свежая информация содержится в его статье "Reduce EXE and DLL Size with LIBCTINY.LIB" в январском выпуске MSDN Magazine за 2001 год. Предлагаемая автором версия "крохотной" библиотеки исполнения выполняет минимальную инициализацию (например, вызывает конструкторы глобальных объектов) и даже предоставляет собственные версии таких функций, как printf и malloc. При этом размер выполняемого модуля оказывается зачастую меньше 3 Кб.
Но не будем забираться так далеко – ведь в нашем коде нет никаких конструкторов, правда?
В данном случае можно просто указать, что функция main будет точкой входа в программу (вместо функции инициализации):
cl test.cpp user32.lib /link /entry:main /opt:nowin98 /subsystem:console
В результате также получим исполняемый файл размером менее 3 Кб (я вновь использовал опцию /opt:nowin98). Разница теперь лишь в том, что он не требует внешней CRT-библиотеки (библиотека user32.lib необходима для функции MessageBox, но она является частью ядра Windows).
Версия ATL: макрос _ATL_MIN_CRTПригодность этого подхода доказывается тем, что с его помощью создано множество легких COM-компонентов. Но непонимание принципов его работы может легко завести в тупик, как видно из цитаты в начале статьи.
В составе библиотеки ATL версии 3 и более ранних имеется файл atlimpl.cpp. Он, как правило, включается в один из исходных файлов проекта (чаще всего в stdafx.cpp) с помощью директивы #include. В atlimpl.cpp находится "облегченная" реализация стартового кода CRT: в нее входят только вариант функции xxxCRTStartup, упомянутой ранее, и "обертки" для работы с динамической памятью – функции malloc, calloc, realloc, free и операторы new/delete. Они непосредственно вызывают функции Windows для работы с кучей – HeapAlloc и HeapFree. Как ни странно, этого достаточно, чтобы заставить заработать без CRT startup множество программ.
Собственно, сама эта реализация доступна, только если определен символ препроцессора _ATL_MIN_CRT. Таким образом, есть возможность легко управлять включением или исключением стартового кода CRT.
ПРИМЕЧАНИЕ
Важный момент при использовании макроса ATL_MIN_CRT: по-прежнему нельзя включать объявления глобальных переменных, классы которых имеют конструкторы или деструкторы, так как код, их вызывающий, содержится только в CRT.
Эта проблема решена в библиотеке ATL 7.0 (не удивляйтесь, как и многие другие приложения Microsoft, ATL перескочила с версии 3 на версию 7), поставляемой с компилятором MS VC++ 7.0. Тем же, кто пользуется прежними версиями компилятора, могу посоветовать воспользоваться отличной библиотекой Andrew Nosenko's ATL/AUX Library, в которой содержится код вызова конструкторов/деструкторов. Для этого необходимо включать в проект вместо atlimpl.cpp файл AuxCrt.cpp из комплекта библиотеки.
Кто виноват?Теперь ясно, что причиной появления ошибки "unresolved external symbol _main" стало включение стартового кода CRT. То есть, была явно или неявно использована какая-либо функция, которая содержит ссылку на структуру данных, находящуюся в модуле с кодом инициализации. При включении компоновщиком в программу этого модуля возникает следующая внешняя ссылка: в теле mainCRTStartup есть вызов main. Вот и все, мы получили наше "любимое" сообщение об ошибке.
Отдельной "увлекательной" стадией сборки приложения является поиск функции или фрагмента кода, вызвавшего такую ситуацию. Для этого применяются следующие шаги:
• Включается опция компоновщика /verbose, при которой он выдает значительно большее количество диагностической информации.
• Включается опция компоновщика /nodefaultlib (или /nod), которая подавляет при сборке поиск библиотек, кроме указанных явно. При этом в списке неразрешенных внешних ссылок будут как "безобидные" функции CRT (которые можно будет включить явно), так и "тянущие" за собой стартовый код CRT.
• Локализовав модуль или функцию проекта, в которой появилась нежелательная внешняя ссылка на CRT, можно включить генерацию ассемблерного листинга (опция компилятора /FA) и простым поиском обнаружить, где происходит реальное включение.