Компилятор Visual Studio (начало работы)

Это пока что лишь черновик

Цель данной статьи осветить некоторые тонкие моменты компиляции c++ приложения компилятором Visual Studio. Все примеры будут компилироваться из командной строки (bat файлами) это позволит лучше понять происходящее. В примерах я использую Visual Studio Express 2012 for Windows Desktop.

Рассмотрим следующий простой пример консольного приложения Windows в котором печатаются все параметры, передаваемые через командную строку, и переменные окружения (envp — это указатель на массив, содержащий переменные окружения с их значениями, разделенные знаком равенства (=)):

#include <windows.h> 
#include <tchar.h>

int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
{
    _tprintf(TEXT("argc = %d\n"), argc); 
    for(int i = 0; i < argc; ++i) 
    { 
        _tprintf(TEXT("argv[%d] = \"%s\"\n"), i, argv[i]); 
    } 

    int n = 0;
    while(envp[n] != NULL)
    {
        _tprintf(TEXT("envp[%d] = \"%s\"\n"), n, envp[n]);
        n++;
    }
    return 0;
}

.bat файл, компилирующий этот пример, имеет вид:

set "vc_path=C:\Program Files\Microsoft Visual Studio 11.0\VC\"
call "%vc_path%vcvarsall.bat" x86

set "bin=bin\"
set "src=src\"
set "progname=simple"

"%vc_path%bin\cl.exe" /c /ZI /nologo /W3 /Od /Oy- /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_UNICODE" /D "UNICODE" ^
                      /Gm /EHsc /RTC1 /MDd /GS /fp:precise /Zc:wchar_t /Zc:forScope /Fo"%bin%%progname%.obj" ^
                      /Fd"%bin%%progname%.pdb" /Gd /TP /analyze- /errorReport:prompt %src%%progname%.cpp

"%vc_path%bin\link.exe" /ERRORREPORT:PROMPT /OUT:"%bin%%progname%.exe" /INCREMENTAL /NOLOGO kernel32.lib user32.lib ^
                       gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib ^
                       odbc32.lib odbccp32.lib /MANIFEST /MANIFESTUAC:"level='asInvoker' uiAccess='false'" ^
                       /manifest:embed /DEBUG /PDB:"%bin%%progname%.pdb" /TLBID:1 /DYNAMICBASE /NXCOMPAT ^
                       /IMPLIB:"%bin%%progname%.lib" /MACHINE:X86 %bin%%progname%.obj 

pause

При этом предполагается следующее расположение файлов

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

    int WINAPI _tWinMain(
       HINSTANCE hInstanceExe,
       HINSTANCE,
       PTSTR pszCmdLine,
       int nCmdShow);

    int _tmain(
        int argc,
        TCHAR *argv[],
        TCHAR *envp[]);

_tWinMain и _tmain это в действительности макросы, которые раскрываются как WinMain или wWinMain для _tWinMain и main или wmain для _tmain в зависимости от того используется Unicode или нет.

На самом деле входная функция операционной системой не вызывается. Вместо этого происходит обращение к стартовой функции из библиотеки C/C++, заданной во время компоновки параметром -entry: командной строки. Она инициализирует библиотеку С/С++, чтобы можно было вызывать такие функции, как malloc и free, а также обеспечивает корректное создание любых объявленных вами глобальных и статических С++-объектов до того, как начинается выполнение вашего кода. В следующей таблице показано, в каких случаях реализуются те или иные входные функции.

Типы приложений и соответствующие им входные функции

Тип приложения Входная функция Стартовая функция, встраиваемая в исполняемый файл
GUI-приложение, работающее с ANSI-символами и строками _tWinMain (WinMain) WinMainCRTStartup
GUI-приложение, работающее с Unicode-символами и строками _tWinMain (wWinMain) wWinMainCRTStartup
CUI-приложение, работающее с ANSI-символами и строками _tmain (main) mainCRTStartup
CUI-приложение, работающее с Unicode-символами и строками _tmain (wmain) wmainCRTStartup

Компоновщик отвечает за выбор подходящей стартовой функции из библиотеки С/С++ при компоновке исполняемого файла. Если задан ключ /SUBSYSTEM:WINDOWS, компоновщик ищет в коде функцию WinMain или wWinMain. Если этих функций нет, компоновщик сообщает об ошибке "unresolved external symbol". В противном случае компоновщик выбирает WinMainCRTStartup или wWinMainCRTStartup, соответственно.

Аналогичным образом, если задан ключ /SUBSYSTEM:CONSOLE, компоновщик ищет в коде функцию main или wmain и выбирает соответственно mainCRTStartup или wmainCRTStartup; если в коде нет ни main, ни wmain, сообщается о той же ошибке – "unresolved external symbol".

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

Теперь несколько замечаний по .bat файлу и ключам компилятора и компоновщика

При создании .bat файла нужно четко следить за тем, чтобы создаваемый текстовый файл имел формат завершения строки в стиле Windows или Unix, но не Mac, иначе bat файл просто не будет запускаться.

Для нормальной работы компилятора перед вызовом cl.exe или link.exe необходимо вызвать call “%vc_path%vcvarsall.bat” x86. Этот bat файл инициализирует переменные окружения INCLUDE, LIB, LIBPATH, PATH и некоторые другие необходимые для работы cl.exe и link.exe. Например, в моем случае было

set "INCLUDE=C:\Program Files\Microsoft Visual Studio 11.0\VC\INCLUDE;C:\Program Files\Windows Kits\8.0\include\shared;C:\Program Files\Windows Kits\8.0\include\um;C:\Program Files\Windows Kits\8.0\include\winrt;"

set "LIB=C:\Program Files\Microsoft Visual Studio 11.0\VC\LIB;C:\Program Files\Windows Kits\8.0\lib\win8\um\x86;"

set "LIBPATH=C:\Windows\Microsoft.NET\Framework\v4.0.30319;C:\Windows\Microsoft.NET\Framework\v3.5;C:\Program Files\Microsoft Visual Studio 11.0\VC\LIB;C:\Program Files\Windows Kits\8.0\References\CommonConfiguration\Neutral;C:\Program Files\Microsoft SDKs\Windows\v8.0\ExtensionSDKs\Microsoft.VCLibs\11.0\References\CommonConfiguration\neutral;"

При вызове cl.exe использовались следующие ключи:

  • – компиляция без связывания
  • /ZI – компилятор включает отладочную информацию в базу данных приложения (только для x86)
  • /nologo – подавляет отображение информации о компиляторе
  • /W3 – устанавливает уровень предупреждений компилятора на 3 уровень
  • /Od – отключает оптимизацию (так как /Od предотвращает перемещение кода, установка этого ключа облегчает процесс отладки)
  • /Oy – предотвращает создание указателей фрейма в стеке вызова, /Oy- отключает такое поведение (только для x86)
  • /D “_DEBUG” – определяется при компиляции с ключами /LDd, /MDd и /MTd
  • /D “_WINDOWS” – определяет, что целевая OS – Windows
  • /D “UNICODE” – указывает компилятору на необходимость использовать в приложении версии Win API функций для работы с Unicode
    Например, вот фрагмент из WinUser.h:
        #ifdef UNICODE
        #define CreateWindowEx CreateWindowExW
        #else
        #define CreateWindowEx CreateWindowExA
        #endif
    
    То есть, если мы при компиляции указываем /D “UNICODE” и в своем коде вызываем CreateWindowEx, то на самом деле происходит обращение к CreateWindowExW
  • /D “_UNICODE” – подобно UNICODE указывает на использование в приложении версий функций из библиотеки С для работы с Unicode строками. Так, например, в заголовочном файле TChar.h можно найти определение следующего макроса:
        #ifdef  _UNICODE
        #define  _tcslen  wcslen
        #else
        #define  _tcslen  strlen
        #endif
    
    Теперь при вызове _tcslen и определенном _UNICODE вызов разрешается в wcslen, в противном случае – в strlen. По умолчанию в новых С++-проектах Visual Studio определен _UNICODE (как и UNICODE)
  • /Gm – включает минимальную перекомпиляцию, которая позволяет перекомпилировать только файлы с измененным исходным кодом
  • /EHsc – перехватываются только С++ исключения и гарантируется, что внешние С функции, никогда не выбросят С++ исключение
  • /RTC (Run-Time Error Checks) (Проверка ошибок времени выполнения)

    /RTC1 – эквивалентно /RTCsu

    /RTCs – включает проверку стекового фрейма времени выполнения, что означает:

    • инициализацию локальных переменных ненулевыми значениями. Это позволяет идентифицировать баги, которые не проявляются в отладочной сборке. Больщая вероятность, что переменная в стеке будет оставаться нулевой в отладочной сборке нежели в рилизной сборке, так как компилятор оптимизирует стек переменных в рилиной сборке. Будучи однажды использованной память отведенная под стек не обнуляется компилятором. По этому, последующие не инициализированные переменные в стеке будут содержать значения оставшееся от предыдущего использования этой области памяти.
    • Проверка выхода за границы локальных переменных, таких как массивы. /RTCs не определяет выход за границы при обращении к памяти явившейся результатом выравнивания компилятором положения структуры в памяти. Такое может произойти при использовании выравнивания (С++ ), /Zp или pack или, если вы распологаете элементы структуры в таком порядке, который вынуждает компилятор вставлять отступы.
    • Проверка указателя стека, что позволяет определить разрушение указателя стека. Разрушение указателя стека может произойти при несоответствии типов вызова. Например, используя указатель на функцию, вы можите вызвать функцию из DLL, которая экспортируется как __stdcall, но вы определили указатель на функцию как __cdecl.

    Замечание:
    стековый фрейм (функции) – область памяти, выделяемая всякий раз, когда вызывается функция, предназначается для временного хранения аргументов и локальных переменных функции.

    /RTCu – заставляет компилятор выдавать предупреждения, когда переменная используется без инициализации. Например, команда, которая генерирует предупреждение (4-го уровня) С4701 может также генерировать ошибку времени выполнения при установленном ключе /RTCu. Любые команды, которые генерируют предупреждения компилятора (уровня 1 и 4) С4700, будут также генерировать ошибку времени выполнения при установленном ключе /RTCu.

    Тем не менее, рассмотрим следующий фрагмент кода:

        int a, *b, c;
        if ( 1 ){
               b = &a;
        }
        c = a;  // No run-time error with /RTCu
    

    Если переменная могла быть проинициализирована, установка ключа /RTCu не приведет к предупреждению времени исполнения. В сущности, вы можете проинициализировать переменную, выполняя операцию взятия ее адреса. В последнем фрагменте кода оператор & работает как оператор присваивания. (???)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>