Monthly Archives: November 2013

Компилятор 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 не приведет к предупреждению времени исполнения. В сущности, вы можете проинициализировать переменную, выполняя операцию взятия ее адреса. В последнем фрагменте кода оператор & работает как оператор присваивания. (???)

Стековая и регистровая архитектура виртуальной машины и Dalvik VM

Эта статья – перевод статьи

Виртуальная машина (VM) это абстракция над уровнем операционной системы, которая позволяет эмулировать физическую машину. Виртуальная машина позволяет запускать одну и ту же платформу под различными операционными системами и аппаратными архитектурами. Интерпретаторы Java и Python можно рассматривать как примеры, в которых код компилируется в специфический для их виртуальных машин байт код. Тоже можно наблюдать и в архитектуре Microsoft .Net, где код компилируется в промежуточный язык для CLR (Common Language Runtime).

Что должно входить в реализацию виртуальной машины? Она должна эмулировать операции физического процессора, а так же в идеале содержать следующие концепции:

  • компиляция исходного кода в специфический для данной виртуальной машины байткод
  • структуры данных для хранения инструкций и операндов (данные и процесс их обработки)
  • стек вызовов для выполнения операций в функции
  • “указатель инструкции” (IP) указывающий на следующию выполняемую инструкцию
  • виртуальный ЦП – обрабатывающего инструкции
    • доставляемые указателем инструкции
    • декодирование операндов
    • выполнение инструкции

Существует два основных способа реализации виртуальной машины: стековый и регистровый. Пример стековой виртуальной машины – виртуальная машина Java, .Net CLR, это широко используемый метод реализации виртуальной машины. В качестве регистровой виртуальной машины можно назвать Lua VM и Dalvik VM (которую мы кратко рассмотрим). Разница между этими двумя подходами в механизме используемом для записи и получения операндов и результатов выполнения команд.

Стековая виртуальная машина

Стековая виртуальная машина реализует основные, выше описанные свойства виртуальной машины, но в качестве структуры данных, куда помещаются операнды, используется стек. Операции получают данные из стека, обрабатывают их и заносят в стек результат по правилу LIFO (последний пришел, первый ушел). В стековой виртуальной машине, операция сложения двух чисел должна выполняться следующим способом (где 20, 7, и "результат" – операнды):

    POP   20
    POP   7
    ADD   20, 7, result
    PUSH  result

Из-за операций PUSH и POP для операции сложения требуется 4 инструкции. Преимущество стековой модели в том, что операнды задаются неявно указателем стека (на рисунке – SP). Это означает, что виртуальной машине не нужно явно указывать адреса операндов, указатель стека указывает на следующий операнд. В стековых виртуальных машинах все арифметические и логические операции выполняются посредством получения операндов и возврата результатов в стек.

Регистровые виртуальные машины

В регистровой реализации виртуальной машины структура данных, в которую помещаются операнды, основана на регистрах процессора. При этом не требуются операции PUSH или POP, но инструкции должны явно содержать адреса (регистры) в которых содержатся операнды. То есть, операнды для инструкций, в отличии от стековой модели, указываются явно. Например, операция сложения в регистровой виртуальной машине выглядит приблизительно так:

   ADD R1, R2, R3; # складывает содержимое R1 и R2, результат  заносит в R3

За счет отсутствия операций POP и PUSH команды в регистровой виртуальной машине выполняются быстрее аналогичных команд стековой виртуальной машины.

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

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

Интересная статья, в которой содержится простая реализация на С регистровой виртуальной машины. Если вы интересуетесь реализацией виртуальных машин и интерпретаторов то вас может заинтересовать книга автора ANTLR Теренса Парра "Language Implementation Pattern: Create your own domain-specific and general programming languages".

Виртуальная машина DALVIK

DALVIK – реализованная google виртуальная машина для Android и выполняющая функцию интерпретатора java кода на устройствах под управлением этой ОС. Для выполнения процесса Android создает отдельный экземпляр виртуальной машины. Это снижает вероятность краха системы при падении одного из приложений. Dalvik реализует регистровую модель и в отличаи от стандартного java байткода, который выполняет 8 битные инструкции на стековой JVM, использует 16 битные инструкции. Регистры реализованы в Dalvik в виде 4 битных полей.

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

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

  • ядро запускает инициализирующую программу, которая является родительским процессом по отношении ко всем другим процессам.
  • инициализирующая программа запускает системные демоны и очень важный сервис "Зиготу".
  • процесс зиготы создает экземпляр Dalvik, являющийся прародителем всех экземпляров Dalvik в системе.
  • процесс зиготы так же запускает BSD сокет, который прослушивает входящие запросы.
  • при получении очередного запроса на создание нового экземпляра Dalvik VM, процесс зиготы разветвляет родительский Dalvik VM процесс и передает дочерний процесс запрашивающему приложению.

Это краткое описание того, как создается и используется виртуальная машина Dalvik в ОС Android.

Возвращаясь к теме виртуальных машин, Dalvik отличается от обычной виртуальной машины Java тем, что она выполняет байткод Dalvik, отличный от обычного java байткода. Промежуточный шаг между Java компилятором и Dalvik VM, на котором происходит преобразование Java байткода в байткод Dalvik берет на себя DEX компилятор. Раличие между JVM и Dalvik проилюстрировано на следующей диаграмме (заимствованной из книги – Learning Android).

DEX компилятор преобразует .class файлы java в .dex файлы, которые имеют меньший размер и оптимизированы для Dalvik VM.

В заключение …

Нельзя однозначно сказать, что стековая виртуальная машина лучше чем регистровая или наоборот. Этот вопрос остается дискуссионным и интересной областью для исследований. Вот интересная статья в которой автор реализует традиционную JVM как регистровую VM, и описывает некоторые полученные интересные результаты.