Преобразование 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 – шестнадцатью.

Тип, возвращаемый оператором sizeofunsigned 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 Karpov, "20 issues of porting C++ code on the 64-bit platform", March, 2007. http://www.viva64.com/articles/20%20issues%20of%20porting%20C++%20code%20on%20the%2064-bit%20platform.pdf

опубликовал Andrey Март 12, 2007 at 12:23 PM MSK #

Я занимаюсь вопросами переноса приложений на 64-битные системы и являюсь одним из основателей проекта Viva64. На нашем сайте http://www.Viva64.com приведен ряд статей посвященных вопросам миграции 32-битных программ на 64 битные Windows системы. Через сайт статьи доступны только на английском языке. Мы решили разместить прямые ссылки на русские варианты некоторых статей. Надеюсь, Вам будет интересно ознакомиться с ними.

Андрей Карпов, Евгений Рыжков. 20 ловушек переноса Си++ - кода на 64-битную платформу
Аннотация. Рассмотрены программные ошибки, проявляющие себя при переносе Си++ - кода с 32-битных платформ на 64-битные платформы. Приведены примеры некорректного кода и способы его исправления. Перечислены методики и средства анализа кода, позволяющие диагностировать обсуждаемые ошибки.
Замечание. На сайте RSDN.ru лежит старый вариант этой статьи, содержащий много ошибок и неточностей. По приведенной ссылке расположен более свежий вариант.

Андрей Карпов Проблемы тестирования 64-битных приложений
Аннотация. В статье рассмотрен ряд вопросов связанных с тестированием 64-битного программного обеспечения. Обозначены сложности, с которыми может столкнуться разработчик ресурсоемких 64-битных приложений, и пути их преодоления.

Евгений Рыжков. Проблемы 64-битного кода на примерах
Аннотация. При переносе 32-битного программного обеспечения на 64-битные системы в коде приложений, написанных на языке Си++, могут проявляться отсутствующие ранее ошибки. Причина этого кроется в изменении базовых типов данных (а точнее отношений между ними) на новой аппаратной платформе. В статье приводится примеры ошибок в коде, приводящие к неработоспособности Си++ программ при переносе их в среду Windows X64.

Андрей Карпов. 64 бита для Си++ программистов: от /Wp64 к Viva64
Аннотация. Развитие рынка 64-битных решений поставило новые задачи в области их верификации и тестирования. В статье говорится об одном из таких инструментов - Viva64. Это lint-подобный статический анализатор Си++ кода, предназначенный специально для выявления ошибок, связанных с особенностями 64-битных платформ. Освещены предпосылки для создания данного анализатора и отражена его связь с режимом &quot;Detect 64-Bit Portability Issues&quot; в Си++ компиляторе Visual Studio 2005.

Евгений Рыжков Viva64: что это и для кого?
Аннотация. Одним из возможных решений для поиска ошибок при переносе кода является использование программ специального класса – статических анализаторов кода. Представителем данной группы программ и является Viva64. Viva64 – это анализатор кода, который выявляет в приложениях, написанных на языках программирования Си и Си++, потенциальные проблемы переноса кода.

С уважением, Андрей Карпов.
E-Mail: kar#DEL#pov@viva64.com
Site: http://www.Viva64.com

опубликовал Андрей Август 02, 2007 at 10:13 AM MSD #

Опубликовать комментарий:
  • HTML Syntax: Отключен
About

Articles, news, notes on dbx, the Sun Studio debugger and other stuff.

Search

Categories
Archives
« Апрель 2014
ПнВтСрЧтПтСбВс
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Сегодня