porting.jpg

Переносимость — это свойство ПО, обеспечивающее перенос (портирование) выполнения кода с одной аппаратной платформе, для выполнения на другой.

Для пользовательских программ переносимость не всегда является целью, но для ядер OC и загрузчиков она часто остается таковой. Это значит, что их мультипортируемый код должен правильно выполняться на значительном количестве аппаратных платформ.

Разработка переносимого кода

Портирование требует строгого учета множества факторов, а именно:

• тип платформы (64, 32, 16, 8 бит)
• размер машинного слова
• использование скрытых и специальных типов данных
• размеры типов данных
• знак char
• выравнивание в памяти (выравнивание нестандартных типов, структур и пр.)
• порядок следования байтов
• получение и использование частоты работы ЦП
• получение и использование частоты срабатывания системного таймера
• размер страницы памяти
• аппаратная зависимость механизмов синхронизации
• порядок выполнения операций ЦП (преемптивность, верхняя память, SMP)
• аппаратная зависимость для некоторых обработчиков прерываний
• знание поведения компилятора и обход реализационно-зависимого поведения
• отказ/принятие во внимание свойств C, зависящих от реализации
• использование длинных имён с внешней связью
• использование ассемблера и ассемблерных вставок
• аппаратная зависимость части кода инициализация памяти и некоторых регистров
• и т.д

Всегда помните о проблемах переносимости и стремитесь к созданию ясного и портируемого кода. В большинстве случаев нужно просто гарантировать, что типы данных используются правильно.

Выбор языка программирования C — первый шаг в сторону уменьшения затрат на переносимость. Но есть ещё много других вещей, которые придется учитывать.

Отметим, что низкоуровневый код и код, который должен выполняться очень быстро, следует разрабатывать аппаратно зависимым и обычно на языке ассемблера.

В целом разработка переносимого кода должна подразумевать следующее:
• Код нужно создавать с учетом самого общего сценария
• Нужно предполагать, что случиться может все
• Нужно принять меры на все, что может случиться
• Нельзя полагаться на то, что будут доступны все возможности
• Нужно опираться только на минимум, доступный всем аппаратным платформам
• Нужно всегда использовать только переносимые типы в интерфейсах

При создании переносимого кода изолируйте аппаратно-зависимый и системно-зависимый код в отдельных модулях. Дозировано используйте препроцессор для облегчения переносимости. Минимизируйте аппаратно-зависимый объем кода.

МОБИЛЬНОСТЬ ПРОГРАММ НА ЯЗЫКЕ СИ

Мобильность программ — это свойство, позволяющее выполнять программы на разных ЦП, работающих под управлением разных версий ОС UNIX, с минимальными изменениями.

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

Верификатор lint

Верификатор (программа семантического контроля) lint обеспечивает строгую проверку типов и выявляет многие конструкции, ухудшающие мобильность программ, написанных на языке Си. Если использовать верификатор lint на всех этапах разработки, то программный продукт будет гораздолегче переносить на любую версию ОС UNIX. Если вам потребуется переписать старую программу для повышения мобильности, то верификатор lint поможет вам выявить все сомнительные места.

Верификатор lint — это хорошее сервисное средство для разработки мобильных программ. Однако не надейтесь, что ваша программа является мобильной, если верификатор lint «не имеет к ней никаких претензий», ине полагайтесь на него, если программа написана плохо, так как верификатор может пропустить некоторые немобильные конструкции. Следуйте изложенным в этом разделе рекомендациям, улучшающим стиль программ.

Зависимость от компилятора

Некоторые детали в языке Си не стандартизированы. Это может проявиться в небольших различиях при обработке программы разными компиляторами. Какими бы «малыми» эти различия не были, они могут породить серьезные проблемы при переносе программ с одной вычислительной системы на другую. Например, в описании языка Си не определен порядок вычисления операндов большинства бинарных операций, таких, как сложение и умножение. Следовательно,не определен порядок появления возможных побочных эффектов. Поэтомуне делайте никаких предположений о реализации свойств языка, которые строго не определены.

Зависимость от аппаратуры

Не полагайтесь на определенный размер машинного слова.

Размер данных типа int зависит от размера машинного слова, различающегося у разных ЦП. Если вы не уверены в результате операций над целыми числами, используйте тип long, чтобы избежать проблемы переполнения.

Не гарантируется, что размер данных типа int совпадает с размером машинного слова. В языке Си определяется только, что размер данных типа short меньше или равен размеру данных типа int, который, в свою очередь, меньше или равен размеру данных типа long. Размер слова может сказаться на обработке двоичных масок.

П р и м е р

#define MASK 0177770 /* неправильно*/ int х;

х &= MASK;

В этом примере три правых бита целого х будут обнуляться только в том случае, если данные типа int занимают 16 бит. Но если размер данных типа int больше 16 бит, то кроме этого будут обнулятьсялевые биты данного х. Чтобы избежать таких проблем, используйте следующее макроопределение:

#define MASK (~07) /* правильно */

int х;

х &= MASK;

Этот пример корректен для всех ЦП независимо от размера данных типа int.

Тщательно проверяйте операции сдвига

Максимальное число бит, которые могут быть сдвинуты вправо или влево, различно на разных ЦП. Если заданный в операции сдвиг превысит допустимый максимум, то результаты операции будут непредсказуемы.

Перед сдвигом преобразуйте целые значения к типу unsigned. На некоторых ЦП сдвиг выполняется логически, т. е. освобождающиеся разряды обнуляются. На других сдвиг производится арифметически, и освобождающиеся разряды заполняются значением знакового разряда. Однако в языке Си гарантируется, что значения типа unsigned сдвигаютсялогически.

Используйте поименованные константы

Использование в программе числовых констант, особенно когда смысл их неочевиден, является плохим стилем программирования. Числовые константы лучше определять в программе символическими именами, связанными с числовыми константами командой препроцессора #define. Такие определения легко находить и модифицировать, если ониразмещены в некоторомстандартном месте. Обычно это начало программы или файл заголовка.

П р и м е р

#define SCREENWIDTH 80

Такое определение позволяет использовать поименованную константу SCREENWIDTH вместо числа 80.

Определяйте размер объекта операцией sizeof

Для определения размера некоторого объекта часто используют константы, что снижает мобильность программ. Использование операции sizeof позволяет решить эту проблему.

П р и м е р

#define NUMELEM(ARRAY)\ (sizeof(ARRAY) / sizeof(*(ARRAY)))

Такое макроопределение обеспечивает мобильный способ определения числа элементов в массиве ARRAY.

Не используйте несколько символов в одной символьной константе

Поскольку символьные константы представляются значениями типа int, определение языка Си позволяет в принципе задать символьную константу, состоящую из нескольких символов. Однако порядок размещения символов в машинном слове различен на разных ЦП.

Не полагайтесь на внутреннюю кодировку целых чисел

Большинство ЦП представляет целые числа в дополнительном коде, но некоторые — вобратном коде. Поэтому не используйте возможности, которые предоставляет дополнительный код. Например, сдвиг на 1 бит влево отрицательного числа (чтобы уменьшить его значение в два раза) не приведет к желаемому результату на ЦП с обратным двоичным кодом.

Формат чисел с плавающей точкой различен на разных ЦП

Представление чисел с плавающей точкой на разных ЦП различно. Поэтому точность результатов арифметических вычислений с плавающей точкой может быть разной на различных ЦП. Избегайте или в крайнем случае подробнодокументируйте все вычисления, зависящие от точности.

Не полагайтесь на определенный порядок и число байт в слове

Число байт и порядок их размещения а машинном слове различны у разных ЦП.

П р и м е р

Следующая функция определена неправильно — на выход будет записан нулевой символ, есликакой-тобайт в слове имеет меньший адрес, чем младший байт;

#define STDOUT 1 
putchar(c); /* неправильно */
int с;
{
write(STDOUT, (char *) &c, 1);
}

В данном примере аргумент с должен описываться как имеющий тип char; в этом случае преобразование типа данных станет ненужным.

Не полагайтесь на определенное число бит в байте

Поскольку число бит в байте у разных ЦП различно, то не предполагайте, что байт всегда занимает 8 бит. Чтобы определить число бит в байте, используйте поименованную константу, содержащуюся в стандартном файле:

/usr/include/values.h

З а м е ч а н и е. Все системные файлы-заголовкиразмещаются в каталоге /usr/include.

Будьте осторожны с символами, имеющими знак

На некоторых ЦП символы представляются как целые значения со знаком, и при вычислении выражений данные типа char обрабатываются с учетом знака. Для повышения мобильности можно использовать явное описание типа unsigned char или преобразовывать символы перед обработкой к типу unsigned char. В других случаях надо использовать данные целого типа.

П р и м е р

Если на данной ЦП допустимы символы со знаком, то существует опасность, что индексация символом некоторого массива приведет к выходу за его границы:

#define TABSIZE 256 char с;

extern char table [TABSIZE];

с = table [c]; /* неправильно */

Чтобы избежать этого, опишите переменную с как имеющую тип unsigned char или индексируйте таблицу значением, преобразованным к типу unsigned char.

П р и м е р

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

#include 

/* неправильно */
char с;
if ((с = getchar()) != EOF)

Если значение символа не может быть отрицательным, то переменная с никогда не станет равной поименованной константе EOF, которая равна-1.Библиотечная функция getchar возвращает значение типа int, поэтому с надо описать как переменную типа int.

Не комбинируйте разные поля бит

Не используйте поля бит для представления данных на внешних носителях

Поля бит можно сделать мобильными, если не объединять разные поля. Максимальный размер поля бит зависит от размера машинного слова, и поля бит не могут пересекать границу слова. Кроме того, порядок размещения полей в слове (слева направо или справа налево) зависит от типа ЦП.

Использование полей бит для описания размещения данных на внешних носителях делает программу немобильной. Для того чтобы упростить эту проблему, поместите определения полей бит в файл заголовка и укажите, что они зависят от типа ЦП.

Используйте операцию преобразования типа для указателей

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

Учитывайте выравнивание при изменении значения указателя

Если вы преобразуете указатели из одного типа в другой, то при выполнении программы может возникнуть ошибка адресации вследствие ограничений на выравнивание данных в машинной памяти Используйте библиотечную функцию malloc, воэвращающую указатель на символ, выравненный в памяти в соответствии с требованиями данной ЦП, так что этот указатель может быть преобразован в указатель любого типа.

Следите за сравнением указателей, имеющих знак

Некоторые ЦП выполняют сравнение указателей с учетом знака, другие делают беззнаковое сравнение. Это различие несущественно, если сравнивать указатели, содержащие правильные адреса. Если указателю будет присвоено значение -1,то в зависимости от ЦП оно бупет рассматриваться или как наибольшее допустимое значение, или как недопустимое значение (меньше минимально допустимого).

Единственная константа, которую можно «безопасно» присваивать указателю, — это нуль, преобразованный к типу соответствующего указателя.

Следите за переполнением значения указателей

Арифметические преобразования указателей могут привести к переполнению или потере значимости. Такие циклические преобразования значений (от наибольшего к наименьшему или наоборот) могут возникнуть при адресации массива, расположенного в начале или в конце машинной памяти

П р и м е р

Этот фрагмент программы показывает возможность появления потери значимости:

struct
large x[SIZE], *p;

/* неправильно */
for (p=x[SIZE - l]; p >=x; p--)

Если массив х расположен в начале памяти, то возможна ситуация, при которой х — 1 будет не меньше, а больше х вследствие перехода через нижнюю границу диапазона значений указателей (потери значимости).

Не полагайтесь на конкретную кодировку символов

Не используйте в программе предположения, что символы в кодовом наборе располагаются последовательно.

П р и м е р

char с;

if (с >= 'а' && с <= 'z' ) /* неправильно */

Такая проверка символа с на принадлежность к строчным буквам не является мобильной. Чтобы такая проверка правильно выполнялась на других ЦП, сделайте так:

char с;

if (islower(c)) /* правильно */

Библиотечная функция islower определена в библиотеке стандартных функцийввода-выводаи являетсямашинно-зависимойПоскольку ее спецификация мобильна, то данная функция обеспечивает мобильную проверку символов.

Учтите, что разные символьные коды могут отличаться по числу входящих в них символов. Не используйте разность значений двух букв для вычисления лексикографического расстояния между ними.

Не используйте программные трюки, зависящие от аппаратуры

Любое повышение эффективности выполнения программы, достигаемое за счет знания особенностей конкретной ЦП, обычно не оправдывает связанную с этим потерю мобильности.

Хорошо организованные программы

Программа называется хорошо организованной, если ее легко читать, модифицировать,

эксплуатировать и, следовательно,переносить на другие ЦП.

Все определения, связанные с конкретной операционной средой и конкретной ЦП, помещайте в файл заголовка.

Важнейшим средством разработки мобильных программ являются команды препроцессора #include и#define. Помещайте все определения типов данных, поименованных констант, макроопределения, используемые более чем одной программой, в единый файл заголовка так, чтобы все возможные изменения были локализованы.

П р и м е р

#include 

С помощью этой команды в программу включается стандартный файл заголовка /usr/include/values.h, который содержит аппаратные константы.

Используйте файлы заголовка для общих определений одного проекта, т. е. множества связанных программ. Используйте локальные файлы заголовка для отдельных программ.

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

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

Для локализации программных фрагментов, зависящих от ЦП, используйте функции, условную компиляцию и команду #define.

Функции, зависящие от конкретной ЦП, объедините в отдельный исходный файл. Если таких файлов несколько, то соберите их в отдельном каталоге.

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

П р и м е р

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

int *stackptr; #ifdef MACHINE

*--stackptr= datum; /* растет вниз */

#else

*++stackptr = datum; /* растет вверх */

#endif

Для локализации характеристик конкретной ЦП можно использовать макроопределения

П р и м е р

#define BITSPERBYTE 8 #define BITS(TYPE)\ (sizeof(TYPE) * BITSPERBYTE)

Спецификация поименованной константы BITSPERBYTE мобильна, реализация -не мобильна. В макроопределении BITS мобильна как спецификация, так и реализация11.

Проверяйте число и тип аргументов, передаваемых функциям

Спецификацией здесь называется определяемая лексема, а реализацией - определяющее константное выражение.

Убедитесь, что число и тип аргументов, передаваемых функциям при вызове, согласуются с числом и типом формальных параметров этих функций. Даже если при конкретном вьвове можно передавать меньше аргументов, чем определено, не опускайте ни одного из них; опишите пустые фактические аргументы, такие как нулевые значения.

В файле /usr/mclude/varargs.h описаны средства для мобильного определения функций с переменным числом аргументов. Например, библиотечная функция printf реализована с использованием этих средств.

Используйте стандартные библиотечные функции; не определяйте собственные системные вызовы, если без этого можно обойтись.

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

Тщательно определяйте внешние имена

Для облегчения эксплуатации и увеличения мобильности программы определяйте все внешние переменные в отдельных исходных файлах. Не забудьте, что все остальные исходные файлы программы должны ссылаться на эти определения с помощью описаний extern. При этом можно использовать разные методы. Можно поместить эти внешниеописания в начало исходных файлов программы (с помощью включения файлов) илиописать эти внешние переменныев функциях, которые их используют.

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

Используйте описание typedef для локализации определения типов данных, зависящих от ЦП

Описание typedef обеспечивает локальные определения типов тех данных, которые зависят от конкретной ЦП. Если вы измените определение типа, заданное описанием typedef, то соответственно изменятся все переменные, описанные с помощью этого производного типа. Система обеспечивает набор стандартных определений в файле

/usr/include/sys/types.h

П р и м е р

typedef unsigned short ino_t; /* индекс файла */

Этот пример показывает типичное использование определения типа в файле

/usr/include/sys/types.h

Мобильность файлов данных

Для переноса файлов, содержащих двоичные данные, используйте символьный ввод-вывод.

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

Один из способов решения этой проблемы заключается в раз работке специальных программ преобразования для конкретных форматов данных. Другой подход заключается в записи байт, составляющих объект данных, в некотором машинно-независимомпорядке Для передачи символьных данных используйте библиотечные функции printf и scanf, хотя это и непрактично.

Ссылки

Опыт и нюансы реализации переносимого кода детально освещены в следующих статьях:
Р.Лав Разработка ядра Linux
Переносимость. Основы
Написание переносимых программ
Переносимость ПО GNU

 ▤  Наша Галактика оказалась необычно одинокой
 ▤  Парад победы 2015. Наземная часть
 ▤  Голландцы на референдуме сказали нет расширению ЕС
 ▤  Кратер Ньирагонго, Конго