Перенос Linux-приложений на 64-разрядные системы

Советы и приемы для плавного перехода

С появлением 64-разрядных архитектур все более актуальным становится готовность работы на них вашего программного обеспечения Linux®. Узнайте, как избежать ошибок переноса при создании объявлений и присваиваний, сдвиге бит, объявлении типов, форматировании строк и т.д.

Харша С. Адига, инженер-программист, IBM

Харша С. Адига (Harsha S. Adiga) работает в IBM Software Group в Бангалоре, Индия и активно участвует в различных рабочих группах и сообществах, занимающихся Linux и программным обеспечением с открытым исходным кодом.



12.04.2006

Linux была одной из первых кросс-платформенных операционных систем, использующих 64-разрядные процессоры, а сейчас 64-разрядные системы стали применяться повсеместно для серверов и настольных компьютеров. Многие разработчики столкнулись с необходимостью переноса приложений с 32-разрядных на 64-разрядные системы. С появлением Intel® Itanium® и других 64-разрядных процессоров подготовка приложений для работы с ними становится все более важной.

Аналогично UNIX® и другим UNIX-подобным операционным системам, Linux использует стандарт LP64, согласно которому указатели и длинные целые (long integer) числа имеют 64 бита, но обычные целые (integer) остаются 32-разрядными. Хотя некоторые языки высокого уровня не чувствительны к различиям в размере, на другие это изменение может оказывать влияние (например, язык С).

Работа по переносу приложения с 32 разрядов на 64 может быть как тривиальной, так и очень сложной, в зависимости от того, как были написаны эти приложения. Множество коварных деталей реализации может вызвать проблемы даже в хорошо написанном, переносимом приложении, поэтому в этой статье рассматриваются такие детали и предлагаются способы работы с ними.

Преимущества 64 разрядов

32-разрядные платформы имеют ряд ограничений, которые все больше расстраивают разработчиков больших приложений, например, баз данных, и особенно тех разработчиков, которые хотят пользоваться преимуществами развития аппаратного обеспечения. В то время как научные вычисления обычно основаны на математике с плавающей точкой, некоторые приложения, например финансовые вычисления, требуют более ограниченных по диапазону чисел, но с большей точностью, чем предлагаемая числами с плавающей точкой. 64-разрядная математика обеспечивает такую повышенную точность чисел с фиксированной точкой в соответствующем диапазоне. Сейчас в компьютерной индустрии ведется много дискуссий об ограничении, накладываемом 32-разрядным адресом. 32-разрядные указатели могут адресовать только 4GB виртуального адресного пространства. Это ограничение можно обойти, но разработка приложений становится более сложной, а производительность значительно уменьшается.

Что касается реализации языка программирования, то современный язык C позволяет использовать тип данных "long long", имеющий длину 64 бита. Однако реализация может определить его имеющим еще больший размер.

Еще одной областью, нуждающейся в улучшении, являются даты. В Linux даты выражаются как 32-разрядные целые числа, представляющие количество секунд, прошедших с 1 января 1970 года. Они станут отрицательными в 2038 году. А в 64-разрядных системах даты выражаются как 64-битные целые числа со знаком, что превышает используемый диапазон.

В итоге 64-разрядная архитектура имеет следующие преимущества:

  • 64-разрядное приложение может напрямую обратиться к 4 экзабайтам (exabyte) виртуальной памяти, а процессор Intel Itanium предоставляет непрерывное линейное адресное пространство.
  • 64-разрядный Linux разрешает файлам иметь размер до 4 экзабайт (2 в степени 63), очень значительное преимущество для серверов, обращающихся к большим базам данных.

64-разрядная архитектура Linux

К сожалению, язык программирования C не предоставляет механизм добавления новых фундаментальных типов данных. Следовательно, обеспечение 64-разрядной адресации и возможностей целочисленной арифметики включает в себя изменение связываний или отображений существующих типов данных, либо добавление новых типов данных в язык программирования.

Таблица 1. Модели 32-разрядных и 64-разрядных данных
 ILP32LP64LLP64ILP64
char8888
short16161616
int32323264
long32643264
long long64646464
pointer32646464

Отличие между тремя 64-разрядными моделями (LP64, LLP64 и ILP64) заключается в типах данных, не являющихся указателями. На изменение длины одного или нескольких типов данных C в различных моделях приложения могут реагировать по-разному. Эти реакции можно разделить на две основные категории:

  • Размер объектов данных. Компиляторы выравнивают типы данных на естественной границе; другими словами, 32-разрядные типы данных выравниваются по 32-битной границе на 64-разрядных системах, а 64-разрядные типы данных выравниваются по 64-битной границе на 64-разрядных системах. Это означает, что размер таких объектов данных как structure или union будет разным на 32-разрядных и 64-разрядных системах.
  • Размер фундаментальных типов данных. Обычные предположения о взаимоотношениях между фундаментальными типами данных не могут больше быть корректными в 64-разрядной модели данных. Приложения, зависящие от этих взаимоотношений, на 64-разрядной платформе компилироваться не будут. Например, предположение sizeof (int) = sizeof (long) = sizeof (pointer) верно для модели данных ILP32, но неверно для других моделей.

Итак, компиляторы выравнивают типы данных по естественной границе, это означает, что для выполнения такого выравнивания компилятор будет вставлять пробелы, как в типах C structure или union. Члены structure или union выравниваются на основе своего самого большого члена. В листинге 1 приведена эта структура.

Листинг 1. C structure
struct test {
	int i1;
	double d;
	int i2;
	long l;
}

В таблице 2 показан размер каждого члена этой структуры и размер структуры на 32-разрядной и 64-разрядной системах.

Таблица 2. Размер структуры и ее членов
Член структурыРазмер на 32-разрядной системеРазмер на 64-разрядной системе
struct test {  
int i1;32 бита32 бита
 32 бита заполнителя
double d;64 бита64 бита
int i2;32 бита32 бита
 32 бита заполнителя
long l;32 бита64 бита
};Размер структуры равен 20 байтРазмер структуры равен 32 байт

Обратите внимание на то, что на 32-разрядной системе компилятор может не выравнивать переменную d, даже если это 64-разрядный объект, поскольку аппаратура рассматривает его как два 32-разрядных объекта. Однако 64-разрядная система выравнивает и d, и l, добавляя два 4-байтных заполнителя.


Перенос с 32-разрядной на 64-разрядную системы

В этом разделе показано, как исправить обычные проблемные места:

  • Объявления
  • Выражения
  • Присваивания
  • Числовые константы
  • Порядок байтов (Endianism)
  • Определения типов
  • Побитный сдвиг
  • Форматирующие строки
  • Параметры функций

Для того чтобы ваш код работал и на 32-разрядных, и на 64-разрядных системах, при работе с объявлениями обратите внимание на следующее:

  • При объявлении констант integer используйте "L" или "U" в зависимости от ситуации.
  • Для беззнаковых целых обязательно используйте unsigned int.
  • Если вам нужны переменные размером 32 бита на обеих системах, определите их как int.
  • Если переменная должна иметь длину 32 бита на 32-разрядных системах и 64 бита на 64-разрядных системах, объявите ее как long.
  • Объявляйте числовые переменные как int или long для выравнивания и лучшей производительности. Не пытайтесь сохранять байты, используя типы char или short.
  • Объявляйте символьные указатели и байты символов как unsigned, для того чтобы обойти проблемы расширения знака в 8-битных символах.

Выражения

В C/C++ выражения основаны на ассоциативности, старшинстве операторов и наборе правил арифметического расширения. Для того чтобы ваши выражения работали корректно и на 32-разрядных и на 64-разрядных системах, помните о следующих правилах:

  • Сложение двух чисел с типом signed int дает в результате signed int.
  • Сложение int и long дает в результате long.
  • Если один из операндов имеет тип unsigned int, а второй signed int, выражение становится unsigned.
  • Сложение int и double дает в результате double. int преобразуется в double перед сложением.

Присваивания

Поскольку длина pointer, int и long на 64-разрядных системах больше не одинакова, могут возникнуть проблемы, зависящие от того, как переменным присваиваются значения и как они используются в приложении. Несколько советов по этому поводу:

  • Не используйте int и long попеременно из-за возможного усечения значимых цифр. Например, не делайте так:
    int i;
    long l;
    i = l;
  • Не используйте int для хранения указателя. Следующий пример работает на 32-разрядных системах, но не работает на 64-разрядных, поскольку 32-разрядный тип int не может хранить 64-разрядный указатель. Например, не делайте так:
    unsigned int i, *ptr;
    i = (unsigned) ptr;
  • Не используйте указатель для хранения int. Например, не делайте так:
    int *ptr;
    int i;
    ptr = (int *) i;
  • В тех случаях, когда unsigned и signed 32-разрядные int смешиваются в выражении и присваиваются signed long, приводите тип одного из операндов в его 64-разрядный тип. Это вызовет расширение второго операнда в 64-разрядный тип, и дальнейшее преобразование при присваивании выражения не понадобится. Еще одним решением является приведение типа всего выражения, так чтобы распространение знака происходило при присваивании. Например, рассмотрим проблему, вызванную следующим:
    long n;
    int i = -2;
    unsigned k = 1;
    n = i + k;

    Арифметически в выделенном выше жирным шрифтом выражении результат должен быть -1. Но из-за того, что выражение имеет тип unsigned, распространения знака не происходит. Решение заключается в приведении типа одного из операндов в его 64-разрядный тип (как в первой строке ниже) или приведении типа всего выражения (как во второй строке ниже):

    n = (long) i + k;
    n = (int) (i + k);

Числовые константы

Шестнадцатиричные константы обычно используются как маски или битовые значения. Шестнадцатиричные константы без суффикса определяются как unsigned int, если они помещаются в 32 бита, и включен бит старшего порядка.

Например, константа OxFFFFFFFFL имеет тип signed long. На 32-разрядной системе при этом устанавливаются все биты, но на 64-разрядной системе устанавливаются только 32 бита младшего порядка, что приводит к значению 0x00000000FFFFFFFF.

Если вы хотите установить все биты, переносимый способ сделать это - определить константу signed long со значением -1. При этом включатся все биты, поскольку задействуется арифметика дополнения до двух:

long x = -1L;

Еще одной проблемой, которая может возникнуть, является установка самого старшего разряда. На 32-разрядной системе для этого используется константа 0x80000000. Но более переносимый способ сделать это - использовать выражение сдвига:

1L << ((sizeof(long) * 8) - 1);

Порядок байт (Endianism)

Порядок байт задает способ хранения данных и определяет, как адресуются байты в целочисленных типах данных и типах данных с плавающей точкой.

Прямой порядок байт (little-endian) означает, что самый младший разряд имеет самый меньший адрес в памяти, а самый старший разряд - самый старший адрес в памяти.

Обратный порядок байт (big-endian) означает, что самый старший разряд имеет самый меньший адрес в памяти, а самый младший разряд - самый старший адрес в памяти.

В таблице 3 показана примерная схема 64-разрядного типа long int.

Таблица 3. Схема 64-разрядного типа long int
 Младший адрес      Старший адрес
Прямой порядок байт (little-endian)байт 0байт 1байт 2байт 3байт 4байт 5байт 6байт 7
Обратный порядок байт (big-endian)байт 7байт 6байт 5байт 4байт 3байт 2байт 1байт 0

Например, 32-разрядное слово 0x12345678 будет размещаться на машине с обратным порядком байт следующим образом:

Таблица 4. 0x12345678 на системе с обратным порядком байт (big-endian)
Смещение памяти0123
Содержимое памяти0x120x340x560x78

Если бы мы посмотрели на 0x12345678 как на два полуслова 0x1234 и 0x5678, то на машине с обратным порядком байт увидели бы следующее:

Таблица 5. 0x12345678 как два полуслова на системе с обратным порядком байт
Смещение памяти02
Содержимое памяти0x12340x5678

Но на машине с прямым порядком байт слово 0x12345678 размещалось бы так:

Таблица 6. 0x12345678 на системе с прямым порядком байт (little-endian)
Смещение памяти0123
Содержимое памяти0x780x560x340x12

Аналогично, два полуслова 0x1234 и 0x5678 выглядели бы так:

Таблица 7. 0x12345678 как два полуслова на системе с прямым порядком байт
Смещение памяти02
Содержимое памяти0x34120x7856

Следующий пример демонстрирует различие между системами с прямым и обратным порядком байт.

Приведенная ниже C-программа выводит "Big endian", если она компилируется и запускается на машине с обратным порядком байт, и "Little endian" в противном случае.

Листинг 2. Сравнение обратного и прямого порядка байт
#include <stdio.h>
main () {
int i = 0x12345678;
if (*(char *)&i == 0x12)
printf ("Big endian\n");
else if (*(char *)&i == 0x78)
    		printf ("Little endian\n");
}

Порядок байт важен тогда, когда:

  • Используются битовые маски
  • Непрямые указатели адресуют части объекта

В C и C++ имеются битовые поля, которые помогают справиться с проблемами порядка байт. Я рекомендую использовать битовые поля вместо полей маски или шестнадцатиричных констант. Существует несколько функций, использующихся для преобразования 16-разрядных и 32-разрядных типов из "host-byte-order" в "net-byte-order". Например, htonl (3), ntohl (3) используются для преобразования 32-разрядных целых чисел. Аналогично, htons (3), ntohs (3) используются для 16-разрядных целых чисел. Однако нет стандартного набора функций для 64 разрядов. Но Linux предоставляет следующие макросы на обеих системах (с прямым и обратным порядком байт):

  • bswap_16
  • bswap_32
  • bswap_64

Определения типов

Я рекомендую вам не кодировать ваши приложения с родными типами данных C/C++, которые меняют размер на 64-разрядной операционной системе, а пользоваться определениями типов или макросами, которые явно показывают размер и тип данных, содержащихся в переменной. Некоторые определения типов помогают сделать код более переносимым.

  • ptrdiff_t:
    Тип signed integer, получаемый из вычитания двух указателей.
  • size_t:
    unsigned integer и результат оператора sizeof. Используется при передаче параметров в функции, например malloc (3), и возвращается из нескольких функций, например fred (2).
  • int32_t, uint32_t etc.:
    Определяют типы integer предопределенного размера.
  • intptr_t and uintptr_t:
    Определяют типы integer, в которые может быть преобразован любой корректный указатель на void.

Пример 1:

64-разрядное возвращаемое значение из sizeof в следующем операторе усекается до 32-разрядов во время присваивания переменой bufferSize.

int bufferSize = (int) sizeof (something);

Решением проблемы должно быть приведение типа возвращаемого значения с использованием size_t и присваивание его переменой bufferSize, объявленной как size_t:

size_t bufferSize = (size_t) sizeof (something);

Example 2:

На 32-раазрядных системах system, int и long имеют одинаковый размер. Поэтому некоторые разработчики используют их как взаимозаменяемые. Это может послужить причиной присваивания указателей типу int и наоборот. Но на 64-разрядной системе, присваивание указателя типу int вызовет усечение старших 32 бит.

Решением проблемы является хранение указателей как тип pointer или как специальных типов, определенных для этой цели, например intptr_t и uintptr_t.

Побитный сдвиг

Нетипизированные целочисленные константы имеют тип (unsigned) int. Это может привести к нежелательному усечению при побитовом сдвиге.

Например, в следующем фрагменте кода максимальным значение переменной a может быть 31. Это следует из того, что 1 << a имеет тип int.

long t = 1 << a;

Для выполнения сдвига на 64-разрядной системе нужно использовать 1L:

long t = 1L << a;

Форматирующие строки

Функция printf (3) и родственные функции могут быть источником больших проблем. Например, на 32-разрядных платформах использование %d для вывода int или long обычно будет работать, но на 64-разрядных платформах будет происходить усечение long до его младших 32 бит. Правильной спецификацией для long является %ld.

Аналогично, когда малые типы integer (char, short, int) передаются в printf (3), они будут расширены до 64 бит, и при необходимости будет распространен знак. В приведенном ниже примере printf (3) предполагает, что указатель имеет длину 32 бита.

char *ptr = &something;
printf (%x\n", ptr);

Этот фрагмент кода не будет корректен на 64-разрядных системах и будет отображать только младшие 4 байта.

Для решения этой проблемы нужно использовать спецификацию %p, как показано ниже, которая будет хорошо работать и на 32-разрядных, и на 64-разрядных системах.

char *ptr = &something;
printf (%p\n", ptr);

Параметры функций

Существуют несколько моментов, которые вы должны помнить при передаче параметров в функции:

  • В тех случаях, когда тип данных параметра определен в прототипе функции, параметр преобразуется в этот тип в соответствии со стандартными правилами.
  • Если тип параметра не указан, параметр расширяется в больший тип.
  • На 64-разрядной системе целые типы преобразуются в 64-разрядные целые типы, а типы с плавающей точкой обычной точности (single) расширяются в типы с двойной точностью (double).
  • Если тип возвращаемого значения не указан, его типом по умолчанию является int.

Проблема возникает при передаче суммы signed int и unsigned int как long. Рассмотрим следующий случай:

Листинг 3. Передача суммы signed int и unsigned int как long
long function (long l);

int main () {
	int i = -2;
	unsigned k = 1U;
	long n = function (i + k);
}

Приведенный выше фрагмент кода не будет работать на 64-разрядных системах, поскольку выражение (i + k) является 32-разрядным выражением с типом unsigned int, и при расширении в long знак не распространяется. Решением проблемы является приведение одного из операндов в его 64-разрядный тип.

Существует еще одна проблема в основанных на регистрах системах, в которых для передачи параметров в функции вместо стека используются регистры. Рассмотрим следующий пример:

float f = 1.25;
printf ("The hex value of %f is %x", f, f);

В основанной на стеке системе выводится соответствующее шестнадцатиричное значение. Но в системе, основанной на регистрах, шестнадцатиричное значение читается из целочисленного регистра, а не из регистра с плавающей точкой.

Решением проблемы является приведение типа адреса переменной с плавающей точкой в указатель на int, который затем разыменовывается следующим образом:

printf ("The hex value of %f is %x", f, *(int *)&f);


Заключение

Основные производители аппаратного обеспечения недавно расширили предложение своих 64-разрядных продуктов из-за производительности, стоимости и масштабируемости, которую могут обеспечить 64-разрядные платформы. Ограничения 32-разрядных систем, особенно потолок в 4GB виртуальной памяти, побудили компании подумать о переходе на 64-разрядные платформы. Знание того, как переносить приложений в соответствии с 64-разрядной архитектурой, может помочь вам при написании переносимого и эффективного кода.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=146198
ArticleTitle=Перенос Linux-приложений на 64-разрядные системы
publish-date=04122006