Преобразование 32-биного приложения в 64-битное: что нужно учитывать
Преобразование 32-биного приложения в 64-битное: что нужно учитывать
Команда Sun Studio, январь 2005
Главная причина проблем при преобразовании 32-битного приложения в 64-битное это изменение размера типа int по отношению к long и указателям. При трансформации 32-битного приложения в 64-битное, размер с 32 бит на 64 бита меняют исключительно тип long и указатели; размер целых типа int остается равным 32 битам. Это может вызвать проблемы с поторей информации при присваивании переменным типа int указателей или переменных типа long. Также трудности могут возникнуть со знаковым расширением при присваивании типам unsigned long или указателям выражений с использованием типов короче, чем int. В этой статье обсуждаются способы избежать или устранить эти проблемы.
Учитывайте разницу между 32-битными и 64-битными моделями
Самая существенная разница между 32-битной и 64-битной средами
компиляции заключается в смене моделей представления типов данных.
Моделью представления типов данных C для 32-битных приложений является
ILP32, названная так потому, что типы int, long и
указатели (pointers) имеют размер в 32 бита. Модель представления типов
данных для 64-битных приложений – это LP64, получившая название от
того, что типы long и указатели вырастают до 64 бит.
Остающиеся целые типы данных и типы данных с плавающей точкой языка C
одинаковы в обоих моделях.
На данный момент для 32-битных программ предположение о том, что типы int, long и указатели имеют одинаковый размер не является необычным. Всвязи с тем, что тип long
и указатели изменяются в модели представления данных LP64, одно лишь
это изменение существенно осложняет переход от ILP32 к ILP64 .
Используйте lint для проверки кода, написанного одновременно для компиляции в 32-битной и 64-битной средах
Указывайте опцию -errchk=longptr64 для выдачи предупреждений, связанных с LP64. Опция -errchk=longptr64
проверяет переносимость кода в среду, где размер длинных целых и
указателей 64 бита, а размер простого целого 32 бита. Эта опция
проверяет присваивания простым целым выражений с указателями и длинными
целыми, даже если используется явное приведение.
Используйте опцию -errchk=longptr64,signext для
того чтобы найти код, в котором обычные правила сохранения значения ISO
C позволяют знаковое расширение знакового интегрального значения в
выражении с беззнаковыми интегральными типами. Опция lint -Xarch=v9
может быть использорвана в том случае, если проверяемый код
предназначен исключительно для компиляции в среде Solaris 64-bit SPARC.
Воспользуйтесь -Xarch=amd64 для проверки кода, который предполагается запускать в 64-битной x86 среде.
Когда lint выдает предупреждения, он печатает номер
строки проблемного кода, сообщение, описывающее проблему, и информацию
об участии в этом указателя. Предупреждающее сообщение также указывает
размеры вовлеченных типов данных. Когда известно об участии указателя и
размерах типов данных, становится возможным найти характерные 64-битные
проблемы, а также избежать ранее существовавших проблем с 32-битными и
меньшими типами.
Предупреждения в конкретной строке кода можно подавить путем размещения комментария в виде "NOTE(LINTED(<optional message>))"в
предыдущей строке. Это полезно в том случае, если требуется пропустить
определенные строки кода, такие как приведения и присваивания.
Проявляйте особую осторожность в использовании комментария "NOTE(LINTED(<optional message>))", поскольку он может скрывать настоящие проблемы. При использовании NOTE включайте файл <note.h>. За дополнительной информацией обращайтесь к man-странице lint.
Учитывайте изменение размера указателя по отношению к размеру обычных целых
Так как в среде компиляции ILP32 обычные целые и указатели имеют
одинаковый размер, 32-битный код повсеместно опирается на это
предположение. Указатели часто приводятся к int или unsigned int для выполнения адресных рассчетов. Ввиду того, что тип long и указатели имеют один размер как в ILP32, так и в LP64 моделях представления данных, указатели можно приводить к типу unsigned long. Однако, скорее предпочтительно использовать uintptr_t вместо явного unsigned long
так как это лучше выражает намерения и делает код более переносимым,
предохраняя его от изменений в будущем. Для того чтобы воспользоваться uintptr_t и intptr_t, включайте файл <inttypes.h>.
Взгляните на следующий пример:
char *p;
p = (char *) ((int)p & PAGEOFFSET);
% cc ..
warning: conversion of pointer loses bits
Нижеследующая модификация будет работать верно при компиляции как для 32-битной, так и для 64-битной платформ:
char *p;
p = (char *) ((uintptr_t)p & PAGEOFFSET);
Учитывайте изменение размера длинных целых по отношению к размеру обычных целых
По той причине, что целые и длинные целые никогда по-настоящему не различались в модели представления данных ILP32, существующий код, вероятно, использует их беспорядочно. Любой код, равнозначно использующий целые и длинные целые, следует изменить так, чтобы он соответствовал требованиям как ILP32, так и LP64 моделей. В то время как целые и длинные целые имеют размер 32 бита в модели ILP32, длинное целое занимает 64 бита в модели представления данных LP64.
Рассмотрие следующий пример:
int waiting;
long w_io;
long w_swap;
...
waiting = w_io + w_swap;
% cc
warning: assignment of 64-bit integer to 32-bit integer
Проверьте наличие знаковых расширений
Знаковые расширения это общая проблема при переходе на 64-битную среду компиляции, поскольку конвертация типов и правила продвижения в некоторой степени невразумительны. Чтобы избежать проблем со знаковым расширением, используйте явное приведение для достижения предполагаемых результатов.
Чтобы понять, откуда возникает знаковое расширение, стоит разобраться с правилами приведения ISO C. Правила приведения, которые, по-видимому, вызывают основные проблемы со знаковым расширением между 32-битной и 64-битной средами компиляции вступают в силу при следующих операциях:
- Интегральное продвижение
В любом выражении, предусматривающем целое, можно использовать как знаковые, так и беззнаковые типыchar, short, перечисление или битовое поле. Если целое может вместить все возможные значения исходного типа, это значение преобразуется в целое; в противном случае, это значение преобразуется в беззнаковое целое. - Преобразование между знаковыми и беззнаковыми целыми
Когда отрицательное целое преобразутся в беззнаковое целое того же или большего типа, сначала оно преобразуется в знаковый эквивалент большего типа, а затем в беззнаковое значение.
Если следующий пример скомпилировать как 64-битную программу, знаковое расширение применяется к переменной addr, даже несмотря на то что обе переменные addr и a.base являются беззнаковыми.
%cat test.c
struct foo {
unsigned int base:19, rehash:13;
};
main(int argc, char *argv[])
{
struct foo a;
unsigned long addr;
a.base = 0x40000;
addr = a.base << 13; /* Здесь происходит знаковое расширение! */
printf("addr 0x%lx\n", addr);
addr = (unsigned int)(a.base << 13); /* А здесь знакового расширения не происходит! */
printf("addr 0x%lx\n", addr);
}
Это знаковое расширение происходит оттого, что правила приведения применяются следующим образом:
- Поле структуры
a.baseпреобразуется из битового поля типаunsigned intвintсогласно правилу интегрального продвижения. Другими словами, так как беззнаковое 19-битное поле помещается в 32-битное целое, битовое поле продвигается до целого, а не беззнакового целого. Таким образом, выражениеa.base << 13имеет типint. Если бы результат присваивалсяunsigned int, это бы не имело значения, так как знаковое расширение еще не произошло. - Выражение
a.base << 13имеет типint, но оно преобразуется вlongи затем вunsigned longперед тем как будет присвоеноaddrиз-за правил знакового и беззнакового продвижений. Знаковое расширение происходит в момент совершения преобразования изintвlong.
Соответственно, программа, будучи скомпилирована как 64-битная, выдаст следующий результат:
% cc -o test64 -xarch=v9 test.c
% ./test64
addr 0xffffffff80000000
addr 0x80000000
%
Если же программа скомпилирована как 32-битная, размер unsigned long будет таким же, как у int, так что знакового расширения не произойдет.
% cc -o test test.c
% ./test
addr 0x80000000
addr 0x80000000
%
Проверьте упаковку структуры
Проверьте внутренние структуры данных приложения на наличие пустот,
то есть заполнения между полями структуры с целью выполнения требований
по выравниванию. Это добавочное заполнение выделяется в том случае,
если поля типа long или указатель увеличиваются в размере в модели представления данных LP64 и появляются после int, размер которого остается равным 32 битам. Так как типы long и указатели выравниваются по границе 64 бит в модели LP64, заполнение появляется между int и long
или указателями. В следующем примере поле p выравнено по 64 битам, и
таким образом заполнение находится между полем k и полем p.
struct bar {
int i;
long j;
int k;
char *p;
}; /* sizeof (struct bar) = 32 байта */
Кроме этого, структуры выравниваются по размеру самого их большого
члена. Следовательно, в представленной выше структуре заполнение
образуется между полями i и j.
При перепаковке структуры, следуйте простому правилу переносить поля типа long или указателя в начало структуры. Обратите внимание на следующее определение структуры:
struct bar {
char *p;
long j;
int i;
int k;
}; /* sizeof (struct bar) = 24 байта */
Проверьте несбалансированные размеры членов объединения
Проверьте объединения, так как их поля могут поменять размер при
переходе между моделями представления данных ILP32 и LP64, что приводит
к изменению размеров полей. В представленном ниже объединении, члены _d и массив _l имеют одинаковые размеры в модели ILP32, но разные в модели LP64 из-за того, что тип long увеличивается до 64 бит в LP64, а double – нет.
typedef union {
double _d;
long _l[2];
} llx_
Размер членов объединения может быть сбалансирован путем изменения типа массива _l с long на int.
Убедитесь в том, что в константных выражениях указаны типы констант
Недостаток точности может вызвать потерю информации в некоторых
константных выражениях. Следует быть точным в указании типов данных в
константных выражениях. Указывайте тип целых констант, добавляя
комбинации {u,U,l,L}. Также можно использовать приведения для указания типа константного выражения. Взгляните на следующий пример:
int i = 32;
long j = 1 << i; /* j будет присвоен 0, поскольку выражение справа имеет тип int */
Этот код можно заставить работать как предполагается, добавив тип константе 1 следующим образом:
int i = 32;
long j = 1L << i; /* теперь j будет присвоено число 0x100000000, как и предполагалось */
Проверьте форматные строки
Убедитесь в том, что форматные строки printf(3S), sprintf(3S), scanf(3S) и sscanf(3S) согласованы с типами long и указателями. В качестве спецификатора формата для указателя следует указывать %p, работающий как в 32-битной, так и в 64-битной среде компиляции. Для типов long, спецификатор увеличенного размера, l, должен быть добавлен к началу спецификатора формата в форматной строке.
Кроме этого, стоит убедиться в том, что буфер, передаваемый первым аргументом sprintf, содержит достаточно места для увеличенного количества цифр в представлении типов long и указателей. Например, указатель выражается восемью шестадцатиричными цифрами в модели ILP32, а в модели LP64 – шестнадцатью.
Тип, возвращаемый оператором sizeof – unsigned long
В модели представления данных LP64 sizeof имеет тип unsigned long. Если sizeof передается функции, ожидающей аргумент типа int, а также если присваивается или приводится к int,
урезание может вызвать потерю информации. Это может привести к
проблемам только в приложениях с базами данных значительного размера,
содержащих крайне большие массивы.
Используйте переносимые типы данных или фиксированные целые типы для представления данных бинарного интерфейса
Для тех структур данных, что разделяются между 32- и 64-битными
вариантами приложения, придерживайтесь использования типов данных,
имеющих один размер в ILP32 и LP64 программах. Избегайте использования
типов long и указателей. Также не следует пользоваться
производные типы, размер которых отличается в 32- и 64-битных
приложениях. Например, следующие типы, определенные в <sys/types.h>, имеют разный размер в моделях ILP32 и LP64:
clock_t, представляющий системное время в тактахdev_t, используемый для нумерации устройствoff_t, используемый для указания размера файлов и смещенийptrdiff_t, являющийся знаковым интегральным типом, представляющим разницу между двумя указателямиsize_t, отражающий размер объекта в памяти в байтахssize_t, используемый функциями, возвращающими количество в байтах или признак ошибкиtime_t, представляющий время в секундах
Для внутренних данных хорошо использовать производные типы данных из <sys/types.h>,
поскольку это помогает предохранить код от изменения модели
представления данных. Однако, именно из-за того, что размеры этих типов
подвержены изменению при смене модели представления данных, не
рекомендуется их использовать в данных, разделямых 32- и 64-битными
приложениями или в других обстоятельствах, где размер данных должен
быть фиксированным. Тем не менее, как и в случае с оператором sizeof, обсуждавшемся выше, перед изменением кода подумайте, будет ли потеря точности иметь реальное влияние на программу.
Рассмотрите возможность использования целых типов с фиксированной длиной из <inttypes.h> для бинарного интерфейса. Эти типы хорошо подходят для явного бинарного представления следующего:
- Спецификаций бинарного интерфейса
- Дисковых данных
- Данных, передаваемых по сети
- Регистров
- Бинарных структур данных
Проверьте наличие побочных эффектов
Отдавайте себе отчет в том, что изменение типа в одной области может
привести к неожиданной необходимости перехода к использованию 64-битных
типов в другой. К примеру, проверьте все точки вызова функций, которые
возвращали int, а теперь возвращают ssize_t.
Помните о влиянии массивов типа long на производительность
Большие массивы типов long или unsigned long могут привести к серьезной потере производительности в модели LP64 по сравнению с массивами типов int или unsigned int. Большие массивы типа long могут вызвать значительно более частое недополнение кэш и потребляют больше памяти. Следовательно, если int служит целям приложения так же хорошо как и long, лучше использовать int, но не long. Это также является аргументом в пользу использования массивов int
вместо массивов указателей. Некоторые приложения, написанные на языке
C, претерпевают значительное ухудшение производительности после
преобразования в модель LP64 оттого, что они полагаются на большое
количество огромных массивов указателей.
Перевод с английского: Максим Карташев, 2006 г.
опубликовал Andrey Март 12, 2007 at 07:23 PM MSK #
Андрей Карпов, Евгений Рыжков. 20 ловушек переноса Си++ - кода на 64-битную платформу
Аннотация. Рассмотрены программные ошибки, проявляющие себя при переносе Си++ - кода с 32-битных платформ на 64-битные платформы. Приведены примеры некорректного кода и способы его исправления. Перечислены методики и средства анализа кода, позволяющие диагностировать обсуждаемые ошибки.
Замечание. На сайте RSDN.ru лежит старый вариант этой статьи, содержащий много ошибок и неточностей. По приведенной ссылке расположен более свежий вариант.
Андрей Карпов Проблемы тестирования 64-битных приложений
Аннотация. В статье рассмотрен ряд вопросов связанных с тестированием 64-битного программного обеспечения. Обозначены сложности, с которыми может столкнуться разработчик ресурсоемких 64-битных приложений, и пути их преодоления.
Евгений Рыжков. Проблемы 64-битного кода на примерах
Аннотация. При переносе 32-битного программного обеспечения на 64-битные системы в коде приложений, написанных на языке Си++, могут проявляться отсутствующие ранее ошибки. Причина этого кроется в изменении базовых типов данных (а точнее отношений между ними) на новой аппаратной платформе. В статье приводится примеры ошибок в коде, приводящие к неработоспособности Си++ программ при переносе их в среду Windows X64.
Андрей Карпов. 64 бита для Си++ программистов: от /Wp64 к Viva64
Аннотация. Развитие рынка 64-битных решений поставило новые задачи в области их верификации и тестирования. В статье говорится об одном из таких инструментов - Viva64. Это lint-подобный статический анализатор Си++ кода, предназначенный специально для выявления ошибок, связанных с особенностями 64-битных платформ. Освещены предпосылки для создания данного анализатора и отражена его связь с режимом "Detect 64-Bit Portability Issues" в Си++ компиляторе Visual Studio 2005.
Евгений Рыжков Viva64: что это и для кого?
Аннотация. Одним из возможных решений для поиска ошибок при переносе кода является использование программ специального класса – статических анализаторов кода. Представителем данной группы программ и является Viva64. Viva64 – это анализатор кода, который выявляет в приложениях, написанных на языках программирования Си и Си++, потенциальные проблемы переноса кода.
С уважением, Андрей Карпов.
E-Mail: kar#DEL#pov@viva64.com
Site: http://www.Viva64.com
опубликовал Андрей Август 02, 2007 at 05:13 PM MSD #