Простые типы данных.
Машинное представление простых типов.
Операции с простыми типами.

1999, 2007, 2017, 20.08.2018

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

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

Попытаюсь заполнить этот пробел. При этом предполагается, что читатель знаком, по крайней мере, с системами счисления. Десятичной (ну это все знают), двоичной и шестнадцатиричной. В основном будет описываться машинное представление данных для процессоров Intel и AMD (то есть, привычной все архитектуры x86), но так же, или очень похоже, реализовано машинное представление и в других архитектурах (в частности, в популярных сейчас микроконтроллерах).

Машинные форматы данных

Бит

Бит - это наименьшая представимая порция информации. Столько ее содержится в ответе "ДА"/"НЕТ", или "ВКЛЮЧЕНО"/"ВЫКЛЮЧЕНО". Бит может иметь только два значения: 0 или 1. Бит не может быть знаковым или беззнаковым. Однако можно выполнить любые вычисления оперируя только битами. Так работают последовательные процессоры. Так выполняются вычисления с большой точностью. На обработке битов основаны методы вычислений, получивши обобщенное название "цифра за цифрой". И, разумеется, из битов состоят все остальные типы данных.

Операции с битами

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

  • Установка (SET) и сброс (RESET). Фактически, это операции присваивания. Установка присваивает биту значение 1, сброс - значение 0.

  • Проверка (TEST). По другому, чтение бита, или получение значения. Выделена мной потому, что биты часто используются для представления условий в программе. Отсюда и такое странное, на первый взгляд, название операции. Большинство процессоров и микронтроллеров поддерживают операции проверки бита (test, bt, и им подобные).

  • Логическая операция НЕ (NOT). Она же ИНВЕРСИЯ. Меняет значение бита на противоположное, т.е.

    NOT 0 = 1
    NOT 1 = 0
  • Логическая операция И (AND). Она же, арифметическая операция УМНОЖЕНИЯ. Выполняется над двумя битами.

    0 AND 0 = 0
    0 AND 1 = 0
    1 AND 0 = 0
    1 AND 1 = 1
  • Логическая операция ИЛИ (OR). Выполняется над двумя битами. Можно было бы назвать эту операцию сложением если бы она подразумевала межбитовые переносы, которые мы будем обсуждать далее.

    0 OR 0 = 0
    0 OR 1 = 1
    1 OR 0 = 1
    1 OR 1 = 1
  • Логическая операция ИСКЛЮЧАЮЩЕЕ ИЛИ (XOR). Выполняется над двумя битами. Эту операцию часто используют для контроля четности и вычисления контрольных сумм.

    0 XOR 0 = 0
    0 XOR 1 = 1
    1 XOR 0 = 1
    1 XOR 1 = 0
  • Логическая операция ВЗЯТИЕ ПО МОДУЛЮ 2 (M2). Выполняется над двумя битами. Является инверсией операции ИСКЛЮЧАЮЩЕЕ ИЛИ. Эту операцию так же часто используют для контроля четности и вычисления контрольных сумм. Обратите внимание, результат операции ИСКЛЮЧАЮЩЕЕ ИЛИ равен единице, когда биты имеют разные значения. Результат операции ВЗЯТИЕ ПО МОДУЛЮ 2 равен единице, когда биты имеют одинаковые значения. Это важное отличие. Нужно отметить, что данную операцию довольно редко рассматривают как отдельную, однако она встречается в некоторых экзотичеких процессорах. В дальнейшем я не буду рассматиривать эту операцию как отдельную.

    0 M2 0 = 1
    0 M2 1 = 0
    1 M2 0 = 0
    1 M2 1 = 1
  • Арифметическая операция СЛОЖЕНИЯ. Выполняется над двумя битами. Результат операции СЛОЖЕНИЕ выходит за границы единичного бита, появляется два неявных дополнительных бита - входящий и исходящий переносы, которые используются для многоразрядных (многобитовых) операций сложения. Входящий перенос является исходящим переносом сложения предыдущих бит, а исходящий перенос используется как входящий для сложения последующих бит. Кажется сложным и запутанным, но точно такие же правила действуют и при привычном нам сложении десятичных чисел, вспомните сложение столбиком. Поскольку межразрядные переносы нужны только для многобитовых машинных типов данных, пример такого сложения будет дальше, при рассмотрении байт. А здесь приведу только таблицу истинности сложения бит, но с учетом переносов (они будут указаны в скобках). В первом столбце указаны входящие переносы (как результат предыдущей операции), а впоследней исходящие, получившиеся в результате данной операции (они будут входящими для последующих операций)

    (0) + 0 + 0 = 0 (0)
    (0) + 0 + 1 = 1 (0)
    (0) + 1 + 0 = 1 (0)
    (0) + 1 + 1 = 0 (1)
    (1) + 0 + 0 = 1 (0)
    (1) + 0 + 1 = 0 (1)
    (1) + 1 + 0 = 0 (1)
    (1) + 1 + 1 = 1 (1)
  • Арифметическая операция ВЫЧИТАНИЯ. Выполняется над двумя битами. Результат операции ВЫЧИТАНИЕ, так же, как и сложения, выходит за границы единичного бита, появляется два неявных дополнительных бита - входящий и исходящий заемы, которые используются для многоразрядных (многобитовых) операций вычитания. Входящий заем является исходящим заемом вычитания предыдущих бит, а исходящий заем используется как входящий для вычитания последующих бит. Кажется сложным и запутанным, но точно такие же правила действуют и при привычном нам вычитании десятичных чисел, вспомните вычитание столбиком. Поскольку межразрядные заемы нужны только для многобитовых машинных типов данных, пример такого вычитания будет дальше, при рассмотрении байт. А здесь приведу только таблицу истинности вычитания бит, но с учетом заемов (они будут указаны в скобках). Входящий заем показан в третьем столбце.

    0 - 0 - (0) = 0 (0)
    0 - 1 - (0) = 1 (1)
    1 - 0 - (0) = 1 (0)
    1 - 1 - (0) = 0 (0)
    0 - 0 - (1) = 1 (1)
    0 - 1 - (1) = 0 (1)
    1 - 0 - (1) = 0 (0)
    1 - 1 - (1) = 1 (1)
  • Арифметическая операция ДЕЛЕНИЯ. Выполняется над двумя битами. Правила этой операции просты, но возможны исключительные ситуации - деление на 0 это ошибка

    0 / 0 = ОШИБКА
    0 / 1 = 0
    1 / 0 = ОШИБКА
    1 / 1 = 1

Теперь давайте посмотрим, как биты размещаются в машинной памяти и как процессор получает к ним доступ. Большинство микропроцессоров и микроконтроллеров в настоящее время могут напрямую манипулировать битами, то есть, имеют команды манипулирования битами. При этом, процессоры Intel x86 получили такие команды только начиная с модели 80386, а до этого приходилось использовать логические операции с байтами и сдвиги. Как это делать рассмотрим чуть позже. Однако далеко не все процессоры могут адресовать биты непосредственно. Чаще всего бит является составной частью более крупных машинных данных, например, байта. То есть, адрес бита состоит из адреса байта и номера бита в байте (слове). А это приводит к невозможности получения адреса битовой переменной в языках высокого уровня, например, С, в отличии от адресов других типов данных. Прямая адресация битов возможна в микроконтроллерах семейства MCS51, где есть область памяти в которой каждый бит имеет свой собственный адрес, наравне с байтовым адресом. Это упрощает создание битовых переменных, обычно используемых как различные флажки в программах. Однако и там есть ограничение в виде невозможности косвенной адресации бит.

Тетрада (полубайт), Байт

Тетрада, как следует из названия (тетра - четыре), состоит из 4 бит. В виде машинного типа данных сейчас встречается редко, однако процессор Intel 4004 (далекий предок 8080 и 8086) был как раз 4 разрядным, то есть работал с тетрадами. Некоторые популярные ранее секционные процессоры (отечественные серии 584 и 1804, например) так же 4 разрядные, но позволяют строить процессоры любой разрядности, кратной 4. В настоящее время термин тетрада используется для чисел формата BCD и неупакованных. Часто существует и специальный флаг, в слове состояния процессора, который фиксирует перенос/заем между тетрадами. Следует заметить, что в формате BCD часто выдают значения микросхемы периферии, например, часы реального времени (RTC).

Байт, это набор из 8 бит (двух тетрад). Именно байт чаще всего является наименьшей адресуемой единицей информации в процессорах. В настоящее время большинство привыкло считать, что байт состоит именно из 8 бит, но это не совсем верно. Изначально (фактически, в до-цифровую эпоху), байт использовался для представления одного символа передаваемой информации. По телеграфным каналам связи символ передавался в виде 5 временных интервалов, то есть, был 5 битным. Может некоторые из Вас видели 5 дорожечную перфоленту от старых телетайпов. Стандартный код ASCII семибитный. В памяти с коррекцией ошибок, например, физический байт содержит более 8 бит. В советской ЭВМ БЭСМ-6 байт (символ) так же состоял из 6 бит, а в Минск-32 из 7 бит. Однако, с точки зрения современных процессоров и микроконтроллеров, байт все таки состоит из 8 бит.

Формат байта

Формат представления байта показан на рисунке 1. Старший значащий бит еще называют MSB, а младший LSB. Рисунок знакового байта может вызвать вопросы. Дело в том, что старшим значащим битом, в этом случае, все равно является самый левый бит, а у нас показан второй слева. Дело в том, что с точки зрения программиста в знаковом байте, для представления числа можно использовать не 8, а только 7 разрядов. Поэтому, с точки зрения программиста, старшим значащим битом является именно показанный. Восьмой же бит является знаковым, т.е. индицирует знак хранимого в байте числа. Единичное значение этого разряда, обычно, соответствует отрицательному числу, а нулевое - положительному.

Для представления отрицательных чисел может использоваться два метода. Первый - знаковый бит используется для индикации знака, остальные разряды для представления числа в прямом коде. Второй - знаковый бит используется для индикации знака, остальные разряды для представления числа в дополнительном коде. На первый взгляд, различие не большое. Однако давайте рассмотрим два числа, 00000000 и 10000000. Если отрицательные числа хранятся в прямом коде, то оба эти числа являются 0. Но! Первое это положительный 0, второе отрицательный! Эту проблему вы возможно уже встречали, по крайней мере в старых книгах по вычислительной технике. Но это не единственная проблема прямого представления отрицательных чисел. Попробуем сложить плюс 1 и минус 1, 00000001 + 10000001. Простое сложение даст 10000010, а это отнюдь не 0. Значит нужна коррекция результата, а это удорожает оборудование. Поэтому в современных процессорах, за редким исключением, отрицательные числа представлены в дополнительном коде. В дополнительном коде отрицательное число представляется как число, которое надо прибавить к хранящемуся для получения в результате 0. Звучит туманно? На самом деле все просто, возьмем число 11111111, для получения 0, к этому числу надо прибавить 1. При этом произойдет перенос из старшего разряда и получится 00000000. Следовательно, 11111111 это -1. Вообще, смена знака числа в дополнительном коде выполняется так: исходное число инвертируется и к результату прибавляется единица. Нет никаких сложностей и со сложением, 11111111 + 00000001 = 00000000. Не требуется усложнение оборудования.

Храниться в машинной памяти, и передаваться по последовательным каналам связи, байт может как со стороны старшего бита, так и со стороны младшего. Однако, во всех современных процессорах младший бит расположен справа, а старший слева.

Теперь о диапазоне значений чисел, хранящихся в байтах. Беззнаковый байт позволяет хранить числа от 0 до 255. Байт со знаком позволяет хранить числа от -128 до +127.

Операции с байтами

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

  • Операция ИНВЕРСИИ БАЙТА (NOT). По другому это называется операция ОТРИЦАНИЯ (НЕ). Над каждым битом байта выполняется операция НЕ. Следует заметить, что эта операция не является операцией смены знака числа, хотя и меняет значение знакового разряда. Знак числа (знакового байта) меняется операцией двоичного дополнения. Если вы не можете уловить разницу, прочтите еще раз повнимательней параграф о форматах представления отрицательных чисел. Фактически, данная операция недопустима для знаковых чисел с точки зрения семантики.

    NOT 01100100 = 10011011
  • Операция ДВОИЧНОЕ ДОПОЛНЕНИЕ (NEG) меняет знак числа. По другому это называется ИНВЕРСИЯ (СМЕНА) знака. Делается это просто: выполняется инверсия байта, а затем к байту прибавляется единица. Еще раз обращаю Ваше внимание на различие этих двух операций, дополнения и инверсии. Как показывает практика, это является камнем преткновения для многих. Инверсия имеет смысл для беззнаковых байтов, а дополнение меняет знак числа хранящегося в знаковом байте. Фактически, данная операция недопустима для беззнаковых чисел с точки зрения семантики.

    NEG 01100100 = 10011100
  • Операция ПОРАЗРЯДНОГО ЛОГИЧЕСКОГО И (AND) выполняется над двумя байтами. Результатом операции является байт, в котором каждый бит является результатом операции И соответствующих бит исходных байтов. Операция имеет смысл для беззнаковых байтов. Точнее, процессору все равно, знаковый или беззнаковый байт участвует в операции, но Вам, как программистам, это явно не все равно.

    01100100 AND 10011101 = 00000100
  • Операция ПОРАЗРЯДНОГО ЛОГИЧЕСКОГО ИЛИ (OR) выполняется над двумя байтами. Аналогична операции И, за исключением того, что над битами выполняется операция ИЛИ. Имеет смысл для беззнаковых байтов.

    01100100 OR 10011101 = 11111101
  • Операция ПОРАЗРЯДНОГО ИСКЛЮЧАЮЩЕГО ИЛИ (XOR) выполняется над двумя байтами. Аналогична операции И, за исключением того, что над битами выполняется операция ИСКЛЮЧАЮЩЕГО ИЛИ. Имеет смысл для беззнаковых байтов.

    01100100 XOR 10011101 = 11111001
  • Операция СЛОЖЕНИЯ (ADD) выполняется над двумя байтами. Сложение выполняется точно так же, как мы складываем десятичные числа, начиная с младшего разряда и заканчивая старшим. Точно так же выполняются и межразрядные переносы. Правила сложения бит я уже описывал выше. Однако, в отличии от привычной нам всем математики, где количество цифр в получившейся сумме нам, в общем и целом, безразлично, при сложении байтов дело обстоит не так просто. Речь идет о ситуации, когда число разрядов в результате операции превышает размер байта. В нашем случае, это более 8 бит для байта без знака, и более 7 бит для байта со знаком. Тут то и появляются термины перенос и переполнение. Термин перенос Вам уже знаком из операции сложения бит. Для индикации этих ситуаций, в слове состояния процессора, обычно предусматриваются специальные биты. Бит индицирующий перенос, обычно, обозначается С или CF, а переполнение О или OF. Попробуем разобраться поподробнее. Рассмотрим, обещанный ранее, пример сложения двух беззнаковых байт:

    Сложение беззнаковых байт

    Как видно, результат занимает 9 бит. Этот девятый бит, не помещающийся в байте, как ранее неявный дополнительный бит, не помещающийся в бите, и называется переносом. Считать ли данную ситуацию ошибочной? И да и нет. Если результат должен помещаться в байт, то это безусловно ошибка. Если же это часть операции сложения многобайтных чисел, то ошибки нет. Перенос, как результат сложения, имеет точно такое же значение, что и поразрядные переносы при выполнении десятичного сложения. Он не является признаком ошибки и используется при организации сложения многобайтных чисел. Итак, термин перенос имеет смысл для беззнаковых байт. Он не является ошибкой при сложении двух байт, являющихся частью многобайтных чисел. В противном случае он сигнализирует об ошибке.

    Теперь разберемся с переполнением. Рассмотрим пример сложения знаковых байт.

    Сложение знаковых байт

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

  • Операция ВЫЧИТАНИЯ (SUB) выполняется над двумя байтами. Как и операция сложения, операция вычитания очень похожа на вычитание обычных десятичных чисел. Точно так же, как и при обычном вычитании, формируются межразрядные заемы. Правила вычитания для бит я уже описывал ранее. Как и в операции сложения, могут быть сформированы признаки заема, как результата операции, и переполнения. Научная формулировка условия формирования переполнения звучит точно так же, как и для сложения. А вот как сказать это более человеческим языком, я оставляю Вам в качестве упражнения.

    Поскольку я так много и подробно написал про сложение, буду кратким. Собственно, вычитание реализуется очень просто. У вычитаемого меняется знак, для чего выполняется операция NEG. Затем выполняется сложение. Это чуть медленее, чем просто вычитание, но не требует усложнения процессора. При вычитании, вместо термина перенос, применяется термин заем. Термин переполнение сохраняет свое значение. Теперь небольшой пример:

    Вычитание байт
  • Операция ИНКРЕМЕНТА (INC). Просто увеличение байта на 1. Это настолько частая операция, что во всех современных процессорах для нее имеется специальная команда.

  • Операция ДЕКРЕМЕНТА (DEC). Просто уменьшение байта на 1. Это настолько частая операция, что во всех современных процессорах для нее имеется специальная команда.

  • Операция УМНОЖЕНИЯ (MUL) выполняется над двумя байтами. Аналогична операции умножения десятичных чисел. Правила умножения бит я уже давал ранее, при описании бит.

    Однако, умножение, как и деление впрочем, имеет несколько особенностей. Первая, это то, что умножение (и деление) бывает знаковым и беззнаковым. Как Вы уже наверно догадались, при выполнении знакового умножения знаковые биты перемножаемых байт не участвуют в операции собственно умножения, а определяют знак результата. В операции беззнакового умножения участвуют, на равных, все биты перемножаемых байт. Второе отличие - результат умножения имеет в два раза больше бит, чем сомножители. То есть при умножении двух восьмибитных байт результат будет иметь 16 бит. Приведу пример:

    Умножение байт

    Самый левый бит, пятнадцатый, в нашем случае равен 0. Может возникнуть вопрос, зачем тогда его показывать? Вспомним сложение. Этот бит может запросто оказаться не нулевым в результате переноса. Итак, в результате умножения двух однобайтных сомножителей мы получили два байта, которые можно назвать словом или полусловом. Но эти термины мы обсудим позже. А сейчас попытаемся разобраться с переполнением и переносом при умножении.

    При обсуждении переноса и переполнения при умножении я должен сделать одну очень важную оговорку. Речь будет идти о процессорах INTEL 80х86 и AMD. Дело в том, что установка признаков результата, и даже количество бит в результате (8 или 16), очень сильно зависит от конкретного процессора. В процессорах Intel 80х86, при умножении двух байт, будет получаться 16-ти разрядный результат, который состоит из двух половин, младшей и старшей. Для нашего примера младшая половина 01110010, а старшая 00110110. Для беззнакового умножения признаки CF и OF будут устанавливаться, если старшая половина результата не нулевая. Для знакового умножения признаки CF и OF будут установлены, если старшая половина результата не равна 00000000 или 11111111. Почему такое странное условие для знакового умножения? Наберитесь терпения, это будет понятно из описания операции деления. Считать ли установку признаков переноса и переполнения ошибкой? Да, если результат должен помещаться в байте. В противном случае это просто говорит о том, что старшая половина результата содержит значащие разряды.

  • Операция ДЕЛЕНИЯ (DIV) выполняется над двумя байтами. Как и все ранее рассмотренные операции, деление байт аналогично привычному делению десятичных чисел. Основное отличие состоит в том, что делимое должно занимать два байта, а не один. Что же делать, если нужно разделить именно байт? Если делимое беззнаковый байт, то его размещают на месте младшей половины делимого, а в старшую заносят 0. Если делимое знаковый байт, то выполняют расширение знака. Для этого в старшую половину делимого заносят 00000000, если делимое положительно, или 11111111, если делимое отрицательно. То есть, устанавливают все биты старшей половины делимого равными значению знакового бита младшей половины делимого. Вспомните условие формирования признаков CF и OF при выполнении знакового умножения, теперь оно должно быть понятным для Вас. Как и умножение, деление может быть знаковым и беззнаковым. Приведу пример:

    Деление байт

    Обратите внимание, что результат деления занимает 9 бит. Правда девятый бит нулевой, и это важно. При реализации операции деления разработчики процессоров Intel 80х86 отошли от привычной схемы формирования признаков результата. После выполнения деления ни призак CF, ни признак OF не имеют смысла и могут принимать любое значение. Если результат деления двух байт не помещается в байт, то есть, если в результате есть ненулевые биты, кроме младших восьми, то выполняется прерывание. Прерывания будут описаны в отдельной статье, сейчас Вам просто надо запомнить, что после операции деления признаки результата не определены, что вызывает дополнительные сложности.

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

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

  • Операции ЛОГИЧЕСКОГО СДВИГА ВПРАВО (SHR) и ЛОГИЧЕСКОГО СДВИГА ВЛЕВО (SHL). Имеют смысл для беззнаковых байт. При этом сдвиг влево, фактически, выполняет умножение на 2, а сдвиг вправо, деление на 2 (с отбрасыванием остатка).

    Логические сдвиги байт

    Так из байта 01101001 (десятичное 105), в результате логического сдвига вправо, получится байт 00110100 (десятичное 52), а в результате сдвига влево 11010010 (десятичное 210). Обратите внимание на необычное использование признака CF, он содержит значение последнего выдвигаемого бита. Именно последнего, поскольку сдвиг может выполняться не на один бит, или по другому разряд, а сразу на несколько. Кстати, признаки результата, в частности CF, по другому называются флажками.

  • Операции АРИФМЕТИЧЕСКОГО СДВИГА ВПРАВО (SAR) и АРИФМЕТИЧЕСКОГО СДВИГА ВЛЕВО (SAL). Имеют смысл для знаковых байт. Аналогичны операциям логических сдвигов, за исключением сохранения знака байта при сдвиге вправо. Логический и арифметический сдвиги влево полностью идентичны.

    Арифметические сдвиги байт

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

  • Операции ЦИКЛИЧЕСКОГО СДВИГА ВПРАВО (ROR), ЦИКЛИЧЕСКОГО СДВИГА ВЛЕВО (ROL), ЦИКЛИЧЕСКОГО СДВИГА ВПРАВО ЧЕРЕЗ ПЕРЕНОС (RCR), ЦИКЛИЧЕСКОГО СДВИГА ВЛЕВО ЧЕРЕЗ ПЕРЕНОС (RCL). Имеют смысл для беззнаковых байт.

    Циклические сдвиги байт

    Особенно комментировать эти операции не буду. Обращаю Ваше внимание только на использование флажка CF в операциях.

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

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

Слово. Полуслово. Двойное слово.

Если набор бит называется байтом, то как называется набор байт? Словом. Если байт может состоять из разного количества бит, то как обстоят дела со словами? Так же. Количество байт в слове зависит от типа процессора. В шестнадцатиразрядных процессорах слово состоит из двух байт. В тридцати двух разрядных процессорах слово состоит из четырех байт. Двойное слово, как и следует из названия, содержит ровно в два раза больше байт, чем просто слово. Встает вопрос, как тогда называется набор из двух байт для тридцати двух разрядного процессора? От специалистов, пришедших в мир персональных компьютеров с больших ЭВМ, можно услышать термин - полуслово (сейчас этот термин употребляется редко). Теперь перейду к конкретному типу процессора - 80х86. Как Вы знаете, младшие модели этих процессоров были 16-разрядными, а начиная с 80386 стали 32-разрядными. Несмотря на это, фирма Intel сохранила терминологию 16-разрядных моделей. В официальной документации на процессоры Intel словом, или коротким словом, называется набор из 2 байт, то есть 16 разрядов. Это верно даже для Pentium. Набор из 4 байт, или 32 разряда, называется двойным словом, или длинным словом. Очевидно, что это сделано для единства терминологии, независимо от конкретной модели. Хотя вопрос спорный.

Формат слова (32 бита)

Формат длинного слова (с точки зрения Intel) показан на рисунке 5. Для представления отрицательных чисел используется дополнительный код, как и в случае байта. Теперь рассмотрим порядок байт в слове, а точнее, порядок байт в слове, хранящемся в оперативной памяти. Суть вот в чем. Процессор, а следовательно и написанная Вами программа, может получить доступ как к слову в целом, так и к каждому из составляющих его байт в отдельности. Сразу возникают вопросы. Адрес какого байта, составляющего слово, будет считаться адресом слова? Если известен адрес слова, то какой адрес будет, например, у младшего или старшего байта? В оющем случае, это зависит от типа процессора. Например длинное слово 14AFB820, состоящее из 4 байт, может храниться в таком порядке: 20, B8, AF, 14. А может и в таком: 14, AF, B8, 20. Это имеет значение, если Вам надо, например, преобразовать длинное слово в два коротких, или наоборот. Это имеет значение, если Вам надо обмениваться информацией с компьютерами, собранными на процессоре другого типа, например PC и Mac, PC и ЕС ЭВМ, PC и специализированный вычислитель. В процессорах Intel слова хранятся в памяти начиная с младшего байта, и за адрес слова принимается адрес младшего байта. То есть короткое слово 53C6 в памяти хранится так: С6, 53. А длинное слово 14AFB820 так: 20, B8, AF, 14.

В беззнаковом слове можно хранить числа от 0 до 65535, в двойном слове от 0 до 4294967295. В слове со знаком можно хранить числа от -32768 до 32767, в двойном слове от -2147483648 до 2147483647.

Операции со словами.

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

  • Операции ПРЕОБРАЗОВАНИЯ БАЙТА В СЛОВО, СЛОВА В ДВОЙНОЕ СЛОВО, ДВОЙНОГО СЛОВА В СЛОВО, СЛОВА В БАЙТ. Преобразование байта в слово мы уже рассматривали, когда разбирались с делением байт. Там же было упомянуто другое название этой операции - расширение знака. Преобразуемый байт помещается на место младшего байта слова, каждый бит старшего байта слова принимает значение знакового бита преобразуемого байта. Проще это звучит так, в старший байт слова заносится 11111111 если в исходном байте хранится отрицательное число, и 00000000, если положительное. Абсолютно так же выполняется операция преобразования слова в двойное слово, только знаковый бит в этом случае распространяется на старшее слово. Заметим, что расширение знака имеет смысл только для знаковых данных. Для беззнаковых достаточно заполнить старшый байт или слово нулями. В процессорах Intel 80х86 для этих операций есть специальные команды.

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

Что можно сказать о хранении слов памяти? В некоторых процессорах разрешено размещение слов с любого адреса, даже нечетного (младший бит адреса не равен 0). Для процессоров с разрядностью больше 16 более точно сказать, что адрес не кратен размеру слова. Например, для 32 разрядного процессора таким "нечетным/некратным" адресом будет адрес, у которого два младших бита не 0. Такими процессорами являются процессоры 80x86. Однако, при размещении слов по кратным адресам обращения к памяти будут более эффективны и программа будет выполняться чуть быстрее. Некоторые процессоры, например процессоры DEC PDP (отечественная серия СМ ЭВМ, Электроника-60, ДВК) при обращении к слову по нечетному адресу генерируют ошибку.

Числа с плавающей запятой

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

Формат числа с плавающей запятой

Как видно, число с плавающей запятой (или точкой), состоит из мантиссы и порядка. Рассмотрим число 24324,43534. Его можно представить, в так называемой научной нотации, как 2,432443534·104. Здесь мантисса 2,432443534, а порядок 4. В общем виде число с плавающей запятой можно представить так:

[+|-]d0,d1d2d3...dn·10[+|-]dp

Здесь [+|-] обозначает необязательный знак числа или порядка. di> это десятичная цифра (от 0 до 9), причем d0 обязательно должна быть не равна 0, естественно, если само число не 0. dp> это десятичный порядок, который тоже состоит из цифр от 0 до 9. Для двоичных чисел общий вид будет таким же, за маленьким исключением:

[+|-]b0,b1b2b3...bn·2[+|-]bp

Здесь bi это двоичная цифра (0 или 1), причем b0 обязательно должна быть 1. bp это двоичный порядок, состоящий из двоичных цифр. Обратите внимание, что для двоичного числа с плавающей запятой порядок это степень двойки, а не 10.

Я буду рассматривать формат чисел с плавающей запятой только применительно к процессорам Intel 80х86. А вот операции с этими числами описываются обычными математическими правилами и не зависят от типа процессора. Итак, в процессорах Intel, числа с плавающей запятой могут хранится в трех форматах: одинарной точности, двойной точности и расширенной точности. Основное отличие этих форматов заключается в количестве бит, отводимых под мантиссу и порядок. Для чисел одинарной точности мантисса занимает 24 бита, а порядок 8. Для двойной точности, мантисса 53 бита, порядок 11 бит. Для расширенной точности, мантисса 64 бита, порядок 15 бит. Знаковый бит как всегда определяет знак числа, однако мантисса хранится не в дополнительном, а в прямом коде. Следовательно возможно появление двух нулей, положительного и отрицательно, +0 и -0. Порядок не только не выровнен на границу байта, но и хранится в экзотическом, смещенном, виде:

хранимый порядок = истинный порядок + смещение

Значение смещения для соответствующих форматов, в десятичном эквиваленте, равно +127, +1023, +16383, или в двоичном виде 1111111, 1111111111, 11111111111111. Приведу примеры, для облегчения понимания, в десятичном виде и для чисел одинарной точности. Порядок -6 будет храниться как 121, а +17 как 144.

Мантисса тоже хранится достаточно хитро. Для начала посчитаем: один бит знаковый, 8 бит порядок, 24 бита мантисса, итого 33 бита! Как же так, ведь двойное слово занимает только 32 бита? Вспомним общий вид двоичного числа с плавающей запятой, точнее то, что b0 обязательно должна равняться 1. Раз одна из цифр числа всегда известна, то зачем ее хранить? Вот и получается, что хранятся только 23 бита из 24, 52 из 53. Для чисел расширенной точности сделано исключение - хранятся все цифры числа, даже b0. Говоря другими словами, в форматах одинарной и двойной точности мантисса хранится со скрытым битом. Теперь о десятичной, извиняюсь, двоичной точке. Эта точка не хранится! Мантисса хранится как целое число. Например мантисса 1.10101010111110011101001 в результате всех преобразований будет храниться как 10101010111110011101001.

Почему же для расширенного формата сделано исключение? Во первых, это нестандартный формат, он присутствует только в процессорах Intel. Во вторых, числа внутри процессора хранятся и обрабатываются именно в этом формате. При загрузке числа из памяти, или при сохранении в память, выполняются необходимые преобразования. Собственно все дело именно в использовании этого формата для выполнения операций внутри процессора. Хранение мантиссы со скрытым битом не позволяет представлять не нормализованные числа, а результат операции может оказаться как раз не нормализованным. Расширенный формат позволяет представлять такие числа благодаря хранению всех разрядов.

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

Операции над числами с плавающей точкой

В компьютерах с процессорами Intel 80х86, операции над числами с плавающей запятой выполняет так называемый сопроцессор 80х87. Раньше сопроцессор был отдельной микросхемой и устанавливался далеко не всегда. Сейчас он входит в состав собственно процессора. Этот сопроцессор имеет команды для арифметических операций, тригонометрических операций, извлечения корня, преобразований форматов чисел, служебные команды. Все операции выполняются по стандартным математическим правилам, поэтому я рассмотрю только четыре арифметических действия, причем в их десятичном эквиваленте.

  • СЛОЖЕНИЕ и ВЫЧИТАНИЕ. Вы можете сложить или вычесть два любых числа. Процессор сам выполнит приведение порядков. Например 1,34·104 + 1,2·102 = 1,34·104 + 0,012·104 = 1,352·104.

  • УМНОЖЕНИЕ и ДЕЛЕНИЕ. Выполняются по широко известным правилам: при умножении, мантиссы перемножаются, а порядки складываются, при делении, мантиссы делятся, а порядки вычитаются. Например: 1,34·104 · 1,2·102 = 1,608·106

Теперь Вы знаете многое о машинных форматах данных. Однако, есть еще специализированные процессоры, оперирующие комплексными числами, сигнальные процессоры для обработки аналоговых сигналов и построения цифровых фильтров. У них свои форматы данных. Существуют еще упакованные и зонные форматы. Их используют для вычислений высокой и сверхвысокой точности. Есть специальные форматы данных для команд MMX и 3D Now!. Есть формат представления данных в видеопамяти, причем зависящий от типа графического процессора. Некоторых из этих форматов я коснусь чуть дальше или в отдельных статьях. В любом случае, для Вас сейчас не должно составить труда разобраться с любым, самым экзотическим форматом.

Представление простых типов данных языков программирования

Разные языки программирования позволяют использовать разные типы данных. Одни и те же машинные форматы в разных языках называются по разному. Однако, если не принимать во внимание сильно специфические языки, вроде Lisp или APL, то окажется, что все не так безнадежно. Сразу оговорюсь, что не рассматриваю случаи, когда переменная может хранить данные разных типов (такое возможно, напрмиер, в JS или Python). Но если говорить максимально упрощенно, то и там переменные в итоге хранятся так, как здесь описано. Просто дополнительно хранится информация о том, какой тип данных собственно сейчас хранит переменная и где искать в памяти сами данные.

  • Биты. В языках программирования для больших ЭВМ (сегодня, включая и персональные ЭВМ и, даже, смартфоны) этот тип данных, обычно, не представлен. А вот для микроконтроллеров используется часто. Занимает, действительно, один бит. Но этот бит, обычно, является составной частью байта или слова. При этом на такие переменные накладываются дополнительные ограничения. Исключений мало, например, семейство MCS-51. Компилятор языка высокого уровня может сам распределять такие переменные по байтам или оставить это в зоне ответственности программиста.

  • Символы (char, unsigned char, character). Казалось бы, что может быть проще? Но давайте заглянем внутрь. Начнем с того, что один алфавитно-цифровой символ совсем не обязательно занимает один байт. Причина кроется в необходимости поддерживать несколько национальных алфавитов, причем некоторые, например китайский, содержат куда больше 255 символов, которые можно представить одним байтом. Кроме однобайтовых кодовых таблиц, таких как CP866, Windows-1251, koi8-r, существуют кодовые таблицы с символами переменной длины. Вы наверно встречали аббревиатуры DBCS, набор двухбайтных символов, или MBCS, набор многобайтных символов. Это кодовые таблицы символов переменной длины. Работа с такими символами не удобна и медленна. Поэтому была сделана попытка ввести международный стандарт. На роль такого стандарта была выдвинута кодировка UNICODE. Фактически, это многобайтные (иногда и переменной длины, как в UTF-8) символы, но их формат стандартизован. Алфавитно-цифровые символы, или просто символы, как бы они не назывались в различных языках, часто хранятся в байте. При чем в некоторых языках, например в С, даже различают знаковые и беззнаковые символы. Кодировка Unicode требует для хранения символа нескольких байт. Строки символов, в некоторых языках они относятся к встроенным типам данных, обычно хранят в виде цепочек байт. Но тут возможны варианты. Строка может храниться с завершающим нулевым байтом (как в С) или с байтом (словом) длинны строки вначале (как в Pascal).

  • Целые числа (int, integer, long, short, unsigned int, unsigned long, unsigned short). Могут быть представлены байтом, словом, двойным словом. Это зависит от транслятора. В версиях языка С, работавших еще на машинах PDP (16 разрядные машины), тип int занимал два байта, short один байт, long четыре байта. Это было логично и диктовалось архитектурой PDP. Однако с тех пор прошло много времени, язык стремительно набирал популярность и переносился на другие аппаратные платформы, в том числе на 8 и 32 разрядные. Естественное соответствие длины и названия было утеряно. В настоящее время, можно с уверенностью сказать лишь то, что short занимает не больше разрядов, чем int, а int не больше чем long. Для IBM PC, 16 разрядные версии трансляторов С размещает int и short в двух байтах (в слове), а long в четырех (в длинном слове). 32 разрядные версии тех же трансляторов размещают short в двух байтах (слово), а int и long в четырех (длинное слово). Unsigned, как и следовало ожидать, означает беззнаковое представление. Трансляторы языков Fortran, Pascal и им подобных, не обеспечивающих такого разнообразия целых типов, как язык С, всегда хранят целые числа с естественным, для конкретного процессора, числом разрядов. Для 16 разрядных версий, это слово, для 32 разрядных - двойное слово. Кроме того, в этих языках целые числа всегда имеют знак.

  • Числа с плавающей запятой, вещественные числа (real, float, double, double precision). Обеспечивают работу с числами, имеющими целую и дробную части. Для процессоров, не имеющих аппаратуры для работы с такими числами, обычно предусматривается программная эмуляция соответствующего аппаратного обеспечения. Это позволяет работать с вещественными числами на любом компьютере. Однако время выполнения программ, в случае эмуляции, довольно значительно возрастает. Числа с плавающей запятой всегда имеют знак. Типы real и float хранятся как числа одинарной точности, занимая 32 разряда (двойное слово). Типы double и double precision хранятся как числа двойной точности, занимая 64 разряда (квадро слово). Формат расширенной точности не доступен из языков программирования высокого уровня. Трансляторы для компьютеров моделей отличных от IBM PC, например Mac, или использующих процессоры отличные от Intel совместимых, могут использовать другие форматы хранения чисел с плавающей запятой.

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

    Упакованный формат (BCD)

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

    Как Вы уже знаете, половина байта, состоящая из 4 бит, называется тетрада. Четырех бит достаточно для представления чисел от 0 до 15 (0F). Так как в десятичной арифметике используются только цифры от 0 до 9, то тетрада как раз подходит для размещения одной десятичной цифры. Значения от 10 (0A) до 15 (0F) являются запрещенными и требуют выполнения процедур коррекции результата. С упакованными числами связано понятие еще одного признака результата (флажка) - дополнительный перенос (AF). Это перенос между тетрадами в байте, когда значение младшей тетрады превышает 9. Рассмотрим пример сложения десятичных чисел 15+28=43. А теперь, как это выглядит для BCD формата: 15+28=3D. Видно, что результат требует коррекции, младшая тетрада должна быть 10-D=3. И возникает перенос между тетрадами, так как младшая тетрада больше 9. В результате значение старшей тетрады увеличивается на 1. Результат коррекции будет правильным - 43. По аналогичным правилам выполняются и другие арифметические операции. В процессорах Intel 80х86 предусмотрены специальные команды для выполнения операций над числами в формате BCD. Предусмотрен и флажек AF.

Вместо заключения

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