Урок 5. Сборка 64-битного приложения
Хочется сразу предупредить читателя, что невозможно всесторонне описать процесс сборки 64-битного приложения. Настройки любого проекта достаточно уникальны, поэтому к адаптации настроек для 64-битной системы всегда надо подходить внимательно. В уроке будут описаны только общие шаги, которые важны для любого проекта. Эти шаги подскажут вам, с чего начать процесс.
Библиотеки
Прежде чем попытаться собрать ваше 64-битное приложение, проверьте, что установлены все необходимые версии 64-битных библиотек и к ним корректно прописаны пути. Например, 32-битные и 64-битные библиотечные lib файлы отличаются и, как правило, находятся в различных каталогах. Исправьте найденные недочеты.
Примечание. Если библиотеки представлены в исходных кодах, то должна присутствовать 64-битная конфигурация проекта. Учтите, что самостоятельно занимаясь модернизацией библиотеки для сборки ее 64-битной версии, вы можете нарушить лицензионные соглашения.
Ассемблер
Visual C++ не поддерживает 64-битный встроенный ассемблер. Вы должны использовать или внешний 64-битный ассемблер (например, MASM), или иметь реализацию той же функциональности на языке Си/Си++.
Примеры ошибок и предупреждений при компиляции
Начав сборку проекта, вы обнаружите большое количество ошибок компиляции и предупреждений, связанных с явным и неявным приведением типов. Продемонстрируем пример возникновения одной из ошибок. Пусть мы имеем код:
void foo(unsigned char) {}
void foo(unsigned int) {}
void a(const char *str)
{
foo(strlen(str));
}
Данный код успешно компилируется в 32-битном режиме. А в 64-битном компилятор Visual C++ выдаст сообщение:
error C2668: 'foo' : ambiguous call to overloaded function
.\xxxx.cpp(16): could be 'void foo(unsigned int)'
.\xxxx.cpp(15): or 'void foo(unsigned char)'
while trying to match the argument list '(size_t)'
Функция strlen() возвращает тип size_t. На 32-битной системе тип size_t совпадает с типом unsigned int и компилятор выбирает для вызова функцию void foo(unsigned int). В 64-битном режиме типы size_t и unsigned int не совпадают. Тип size_t становится 64-битным, а тип unsigned int по-прежнему остается 32-битным. В результате компилятор не знает, какой функции foo() отдать предпочтение.
Теперь рассмотрим пример предупреждения, выдаваемого компилятором Visual C++ при сборке кода в 64-битном режиме:
CArray<char, char> v;
int len = v.GetSize();
warning C4244: 'initializing' : conversion from 'INT_PTR' to 'int',
possible loss of data
Функция GetSize() возвращает тип INT_PTR, который совпадает с типом int в 32-битном коде. В 64-битном коде тип INT_PTR имеет размер 64 бита, и происходит неявное приведение этого типа к 32-битному типу int. При этом теряются значения старших битов, о чем и предупреждает компилятор. Неявное приведение типа может привести к ошибке, если количество элементов в массиве превысит INT_MAX. Для устранения предупреждения и потенциальной ошибки следует использовать для переменной "len" тип INT_PTR или ptrdiff_t.
Не торопитесь исправлять предупреждения до того, как познакомитесь с паттернами 64-битных ошибок. Вы можете случайно, не исправив ошибку, замаскировать ее и тем самым сделать ее обнаружение в дальнейшем более сложной задачей. О паттернах 64-битных ошибок, методах их обнаружения и исправления вы сможете познакомиться в последующих уроках. Также вы можете обратиться к следующим статьям: "20 ловушек переноса Си++ - кода на 64-битную платформу", "64-битный конь, который умеет считать".
Типы size_t и ptrdiff_t
Поскольку большинство ошибок и предупреждений на этапе компиляции будет связано с несовместимостью типов данных, рассмотрим два типа size_t и ptrdiff_t, представляющих для нас наибольший интерес с точки зрения создания 64-битного кода. Если вы используете компилятор Visual C++, то эти типы являются для него встроенными и подключения библиотечных файлов не требуется. Если вы используете GCC, то вам потребуется использовать заголовочный файл stddef.h.
size_t - базовый беззнаковый целочисленный тип языка Си/Си++. Является типом результата, возвращаемого оператором sizeof. Размер типа выбирается таким образом, чтобы в него можно было записать максимальный размер теоретически возможного массива любого типа. Например, на 32-битной системе size_t будет занимать 32 бита, на 64-битной - 64 бита. Другими словами, в переменную типа size_t может быть безопасно помещен указатель. Исключение составляют указатели на функции классов, но это особый случай. Тип size_t обычно применяется для счетчиков циклов, индексации массивов, хранения размеров, адресной арифметики. Аналогами данному типу являются: SIZE_T, DWORD_PTR, WPARAM, ULONG_PTR. Хотя в size_t можно помещать указатель, для этих целей лучше подходит другой беззнаковый целочисленный тип uintptr_t, само название которого отражает эту возможность. Типы size_t и uintptr_t являются синонимами.
ptrdiff_t - базовый знаковый целочисленный тип языка Си/Си++. Размер типа выбирается таким образом, чтобы в него можно было записать максимальный размер теоретически возможного массива любого типа. На 32-битной системе ptrdiff_t будет занимать 32 бита, на 64-битной - 64 бита. Как и в size_t в переменную типа ptrdiff_t может быть безопасно помещен указатель, за исключением указателя на функцию класса. Также ptrdiff_t является результатом выражения, где один указатель вычитается из другого (ptr1-ptr2). Тип ptrdiff_t обычно применяется для счетчиков циклов, индексации массивов, хранения размеров, адресной арифметики. Аналогами данному типу являются: SSIZE_T, LPARAM, INT_PTR, LONG_PTR. У типа ptrdiff_t есть синоним intptr_t, название которого лучше отражает, что тип может хранить в себе указатель.
Типы size_t и ptrdiff_t были созданы для того, чтобы осуществлять корректную адресную арифметику. Долгое время было принято считать, что размер int совпадает с размером машинного слова (разрядностью процессора) и его можно использовать в качестве индексов для хранения размеров объектов или указателей. Соответственно адресная арифметика также строилась с использованием типов int и unsigned. Тип int используется в большинстве обучающих материалов по программированию на Си и Си++ в телах циклов и в качестве индексов. Практически каноническим является код:
for (int i = 0; i < n; i++)
a[i] = 0;
С развитием процессоров и ростом их разрядности стало нерационально дальнейшее увеличение размерностей типа int. Причин для этого много: экономия используемой памяти, максимальная совместимость и так далее. В результате появилось несколько моделей данных, описывающих соотношение размеров базовых типов языков Си и Си++. Соответственно, не так просто стало выбрать тип переменной для хранения указателя или размера объекта. Чтобы наиболее красиво решить эту проблему , и появились типы size_t и ptrdiff_t. Они гарантированно могут использоваться для адресной арифметики. Теперь каноническим должен стать следующий код:
for (ptrdiff_t i = 0; i < n; i++)
a[i] = 0;
Именно он может обеспечить надежность, переносимость, быстродействие. Почему - станет ясно из дальнейших уроков.
Описанные типы size_t и ptrdiff_t можно назвать memsize-типами. Термин "memsize" возник как попытка лаконично назвать все типы, которые способны хранить в себе размер указателей и индексов самых больших массивов. Под memsize-типом следует понимать все простые типы данных языка Си/Си++, которые на 32-битой архитектуре имеют размер 32-бита, а на 64-битной архитектуре - 64-бита. Примеры memsize-типов: size_t, ptrdiff_t, указатели, SIZE_T, LPARAM.
Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com).
Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com.