Джонсон Харт - Системное программирование в среде Windows
Перенос имеющегося программного кода
Единая модель данных Windows призвана минимизировать объем возможных изменений исходного кода, но полностью избежать необходимости внесения изменений невозможно. Например, такие функции, как HeapCreate и HeapAlloc (глава 5), которые имеют дело непосредственно с распределением памяти и размерами блоков памяти, должны использовать либо 32-битовое, либо 64-битовое поле, в зависимости от модели. Точно так же, следует всегда тщательно проверять код, чтобы выяснить, не используются ли в нем скрытые допущения относительно размеров полей и указателей.
Сначала будут описаны изменения, связанные с использованием API, которые, главным образом, касаются функций управления памятью.
Изменения, связанные с использованием API
Наиболее заметные изменения, связанные с использованием API, затрагивают функции управления памятью, введенные в главе 5. В новых определениях в полях счетчиков используется тип данных SIZE_T (см. табл. 16.2). Например, теперь прототип функции HeapAlloc будет иметь следующий вид:
LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes);
Количество запрошенных байтов, указываемое в третьем поле, выражается данными типа SIZE_T и поэтому является 32– или 64-битовым целым без знака. Ранее данные в этом поле имели тип DWORD (всегда 32 бита).
Данные типа SIZE_T используются в соответствии с необходимостью в главе 5.
Изменения, связанные с устранением неявных допущений относительно предполагаемых размеров элементов данных
Источником многих проблем могут служить различного рода допущения относительно размеров данных. Несколько возможных примеров этого приводятся ниже.
• Тип DWORD больше нельзя использовать при указании размера блоков памяти. Вместо него следует применять типы данных SIZE_T или DWORD64.
• Необходимо тщательно проверять размеры полей, используемых взаимодействующими процессами, независимо от того, выполняются ли они на одной и той же или на разных системах. Так, в главе 12 для того, чтобы перенос программы на системы UNIX или Win64 не приводил к возникновению 64-битовых полей, поля размера в сообщениях сокетов определялись с использованием типа данных LONG32. При организации связи между процессами Windows, использующими разные модели, размеры блоков памяти не должны превышать 2 Гбайт.
• Для вычисления размера структур или типов данных следует использовать функцию sizeof; эти размеры будут разными для Win32 и Win64, если в структуру данных входят указатели или элементы данных SIZE_T. Литеральные константы размеров должны быть исключены (разумеется, этому совету было бы неплохо следовать при любых обстоятельствах).
• Необходимо проверять, не содержаться ли в объединениях, в которых указатели используются совместно с арифметическими типами данными, неявные предположения относительно размеров типов данных.
• Любое приведение типов или иное преобразование, в котором участвуют указатели и данные арифметического типа должно тщательно проверяться. Обратитесь, например, к фрагментам кода, приведенным в разделе "Пример: использование указательных типов данных".
• В частности, остерегайтесь неявного приведения 32-битовых целых к 64-битовым в вызовах функций. Нет никакой гарантии, что старшие 32 бита будут очищены, в результате чего функция может получить в качестве аргумента очень большое 64-битовое целое значение.
• Указатели выравниваются по 8-байтовым границам, в результате чего дополнение структур, обусловленное выравниванием, может увеличить размер структуры данных сверх необходимого и даже отрицательно повлиять на производительность. Перемещение указателей в начало структуры минимизирует последствия ее "разбухания".
• При выводе на печать указателей вместо спецификатора формата %x используйте спецификатор %p, а при выводе платформо-масштабируемых данных, например типа SIZE_T, — спецификатор %ld.
• Функции setjmp и longjmp должны использовать заголовочный файл <setjmp.h>, а не какие-либо допущения относительно возможного размера переменной jmp_buf, в которой должен храниться указатель.
Пример: перенос программы sortMM (программа 5.5)
В программе sortMM (программа 5.5) интенсивно используются указатели, и в частности, арифметика указателей. Подготовка этой программы к переносу, в результате чего ее можно будет компоновать и выполнять под управлением как Win32, так и Win64, иллюстрирует обычно используемые методики, а также демонстрирует, как легко невольно сделать допущения относительно размера указателя.
Использование предупреждающих сообщений компилятора
Какое бы большое значение визуальная проверка кода ни играла для обнаружения и устранении любых проблем, связанных с переходом к Win64, всегда целесообразно использовать компилятор или какое-либо иное средство, обеспечивающее просмотр кода и выдачу соответствующих предупреждающих сообщений.
Входящий в состав Microsoft Visual Studio 7.0 (.NET) компилятор C++ компании Microsoft может конфигурироваться для выдачи таких сообщений. Для этого достаточно задать в командной строке компилятора опции –Wp64 и –W3. В Visual Studio для установки этих опций потребуется выполнить следующие действия:
• Выберите страницу Project Properties (Свойства проекта).
• Откройте папку C++.
• Щелкните на кнопке General (Общие).
• Выберите вкладку Detect 64-bit Portability Issues (Определять элементы переноса в 64 разряда) и выберите вариант Yes (/Wp64) (Да (/Wp64)). Оставьте для уровня диагностики (warning level) значение 3.
После этого, в процессе сборки проекта в окне вывода будут отображаться соответствующие предупреждающие сообщения. При построении в Microsoft Visual Studio 7.0 проектов, которые находятся на Web-сайте книги, вывод предупреждающих сообщений конфигурировался именно так, как описано выше.
Код до подготовки к переносу
Большая часть программного кода sortMM.с не приводит к выдаче предупреждающих сообщений, но один участок кода на шаге 6 (см. программу 5.5) вызывает их генерацию. Соответствующий фрагмент кода вместе с номерами строк представлен в программе 16.1. Имейте в виду, что в последующих версиях этой программы номера строк могут поменяться.
Программа 16.1. sortMM.с: код до подготовки к переносув Win64, часть 1…
54 LPBYTE pXFile = NULL, pX;
55 TCHAR _based (pInFile) *pIn;
…
130
131 if (!NoPrint)
132 for (iKey = 0; iKey < FsX / RSize; iKey++) {
133 WriteFile(hStdOut, &ChNewLine, TSIZE, &nWrite, NULL);
134
135 /* Приведение типа рХ играет весьма важную роль, поскольку это
136 указатель на байт, а нам нужны четыре байта указателя типа _based. */
137 pIn = (TCHAR _based(pInFile)*)*(LPDWORD)pX;
138
139 while ((*pIn != CR || *(pIn + 1) != LF) && (DWORD)pIn < FsIn) {
140 WriteFile(hStdOut, pIn, TSIZE, &nWrite, NULL);
141 pIn++;
142 }
143 pX += RSize;
144 }
Сообщения компилятора далее приводятся, но прежде чем ознакомиться с ними, вы, возможно, захотите просмотреть код, чтобы определить возможные причины выдачи будущих предупреждающих сообщений. Не забывайте о том, что нашей целью является придание программе такого вида, который обеспечивает ее сборку и корректное выполнение как в режиме Win32, так и в режиме Win64.
Предупреждающие сообщения компилятора
Предупреждающие сообщения компилятора для этого фрагмента кода отчетливо демонстрируют неявное предположение о том, что размер указателя составляет 4 байта.
SORTMM.C(137) : warning C4312: 'type cast' : conversion from 'DWORD' to 'TCHAR __based(pInFile) *' of greater size
SORTMM.C(139) : warning C4311: 'type cast' : pointer truncation from 'TCHAR __based(pInFile) *' to 'DWORD'
Первое предупреждение (строка 137) является существенным. Разыменование рХ после его приведения (type cast) к типу LPDWORD приводит к 32-битовому значению, которое затем назначается указателю pIn. Почти с полной уверенностью можно утверждать, что разыменование pIn вызовет исключение или приведет к возникновению иной серьезной ошибки. Правильным решением для строки 137 будет замена приведения к типу LPDWORD приведением к типу указателя LPTSTR следующим образом:
pIn = (TCHAR _based(pInFile)*)*(DWORD_PTR)pX;
Сообщение для строки 139 довольно интересно, поскольку мы сравниваем базовый указатель с размером файла. Если предположить, что файл не является гигантским, то на это предупреждение можно не обращать внимания. При этих условиях можно было бы проигнорировать и сообщение для строки 137. Однако мы учтем перспективу и приготовимся к работе с гигантскими файлами, пусть даже типом FsSize пока и является DWORD. Допуская полный диапазон значений указателя, мы должны преобразовать строку 139 следующим образом:
while ((*pIn != CR || *(pIn + 1) != LF) && (SIZE_T)pIn < (SIZE_T)FsIn) {