Содержание
Главная причина проблем при преобразовании 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%lxn", addr);
addr = (unsigned int)(a.base << 13); /* А здесь знакового расширения не происходит! */
printf("addr 0x%lxn", 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 оттого, что они полагаются на большое
количество огромных массивов указателей.
Оригинал статьи: http://developers.sun.com/prodtech/cc/articles/ILP32toLP64Issues.html
Перевод с английского: Максим Карташев, 2006 г. |