Мой канал Old Programmer весь здесь: Программирование. Тематическое оглавление моего Zen-канала (Old Programmer). А здесь все о программировании на ассемблере.
- Список разделов канала Old Programmer, канала о программировании и программистах
Это вторая статья о числах в ассемблере. Первая здесь. Материал был взят из моей книги "Ассемблер и дизассемблирование" и частично переработан.
Параграф 1.7.
Представление чисел в компьютере
Беззнаковые целые числа в компьютере
Принцип представления беззнаковых целых чисел в компьютере достаточно прост:
- число должно быть переведено в двоичную систему счисления;
- должен быть определен объем памяти для этого числа.
Как мы уже говорили, это удобно делать, когда число представлено в шестнадцатеричной системе счисления, и тогда станет ясно, сколько ячеек памяти необходимо для хранения этого числа. Принято выделять: одинарные ячейки памяти (байты), двойные ячейки (слова), четвертные ячейки (четыре байта), наконец есть 8-байтовые. В языке ассемблера GAS (именно его я рассматриваю на своем канале) имеются специальные директивы для резервирования памяти для числовых констант и переменных:
- .byte - размещает каждое выражение как 1 байт
- .short - 2 байта
- .long - 4 байта
- .quad - 8 байт
Если речь идет о переменной, а чаще так и бывает, необходимо определить диапазон, в котором будет меняться значение переменной и, исходя из этого, резервировать для нее память. Поскольку современные процессоры Intel ориентированы на операции с 64-битными числами то, оптимальнее, все же ориентироваться на переменные такой же размерности.
Рассмотрим программу на языке C (700.c).
В данном программе задано четыре переменных: однобайтовая e, двухбайтовая c, четырехбайтовая t, восьмибайтовая a. С помощью этой программы, к выведем область памяти, где хранятся эти переменные. Вот дамп памяти, который выдает программа (рисунок 1).
Внимательно посмотрев на данную цепочку байтов, вы без труда обнаружите все наши переменные. Что важного можно почерпнуть из данной последовательности?
- Мы видим, что младшие байты чисел (переменных) в слове занимают в памяти младшие адреса. В свою очередь младшие слова в удвоенном слове — младший адрес. И, наконец, если рассматривать 64-битную переменную, то в ней младшее удвоенное слово должно занимать младший адрес. Это очень важный момент именно для анализа двоичного кода. В дальнейшем по одному виду области памяти вы сможете во многих случаях сразу идентифицировать переменные.
- Как видно на все переменные затрачивается объем памяти, кратный четырехбайтовой величине. После каждой инициализированной переменной компилятор вставляет директиву выравнивания по 32-битной границе (Align 4). Впрочем, все совсем не так просто, и при другом порядке следования переменных выравнивание может быть иным.
И так. 16-битное число 0xА890 в памяти будет храниться как последовательность байтов 90 A8, 32-битное число 0x67896512 как последовательность 12 65 89 67. И, наконец, 64-битное число 0xF5C68990D1327650 — как 50 76 32 D1 90 89 C6 F5.
Числа со знаком в компьютере
Поскольку в памяти нет ничего, кроме двоичных разрядов, то вполне логично было бы выделить для знака числа отдельный бит. Например, имея одну ячейку, мы могли бы получить диапазон чисел от –127 до 127 (11111111 - 01111111). Подход был бы не так уж и плох. Вот только пришлось бы вводить отдельно сложение для знаковых и беззнаковых чисел. Существует и другой, альтернативный способ введения знаковых чисел. Алгоритм построения заключается в том, что мы объявляем некоторое число заведомо положительным и далее ищем для него противоположное по знаку исходя из очевидно тождества: a + (– a) = 0.
На множестве однобайтовых чисел за единицу естественно взять двоичное число 00000001. Решая уравнение 00000001 + x = 00000000, мы приходим к неожиданному, на первый взгляд, результату x = 11111111, другими словами за -1 мы должны принять число 11111111 (255 в десятичном эквиваленте и FF в шестнадцатеричном). Попробуем развить нашу теорию. Очевидно, что -1 - (1) = -2, т. е. по логике вещей, за -2 мы должны принять число 11111110. Но с другой стороны число 00000010 вроде бы должно представлять +2. Посмотрите 11111110 + 00000010 = 00000000, т. е. выполняется очевидное тождество +2 + (-2) = 0. Итак, мы на верном пути и процесс можно продолжить (см. Рисунок 2).
Внимательно посмотрите на таблицу (рисунок 2). Что же у нас получилось? Знаковые числа оказываются в промежутке -128 до 127.
Таким образом, однобайтовое число можно интерпретировать и как число со знаком, и как беззнаковое число. Тогда, например, 11111111 в первом случае будет считаться -1, а во втором случае 255. Все зависит от нашей интерпретации. Еще интереснее операции сложения и вычитания. Эти операции будут выполняться по одной и той же схеме и для знаковых и для беззнаковых чисел. По этой причине и для операции сложения и для операции вычитания у процессора имеется всего по одной команде: add и sub. Разумеется, при выполнении действия может возникнуть переполнение или перенос в несуществующий разряд, но это отдельный разговор, и решить проблему можно, зарезервировав еще одну ячейку памяти. Все наши рассуждения легко переносятся на двух- четырех- и восьмибайтовые числа. Так максимальное двухбайтовое беззнаковое число будет 65 535, а знаковые числа окажутся в промежутке от -32 768 до 32 767. Еще один интересный момент касается старшего бита. Как мы видим, по нему можно определить знак. Но в данной схеме бит совсем не изолирован и участвует в формировании значения числа вместе с остальными битами.
Уметь хорошо ориентироваться в знаковых и беззнаковых числах очень важно для программиста на языке ассемблера. Встретив, скажем, команду
cmp rax, 0xFFFFFFFFFFFFFFFE
следует иметь в виду, что в действительности это, возможно, команда
cmp rax, -2
Рассмотрим последовательность переменных:
signed char e=-2;
short int c=-3;
int b=-4;
__int64_t a=-5;
Как видим, это все знаковые переменные с отрицательным значением. При выводе фрагмента памяти, содержащего данные переменные, получим следующую последовательность байтов:
fe00 00 00 fd ff 00 00 fc ff ff ff 00 00 00 00 fb ff ff ff ff ff ff ff
Итак, значение однобайтовой переменной -2 в памяти компьютера представлено байтом 0xFE, значение двухбайтовой переменной -3, представлено последовательностью 0xFFFD, значение четырехбайтовой переменной -4 — последовательностью 0xFFFFFFFC, и, наконец, отрицательное восьмибайтовая переменная со значением -5 представлена байтами 0xFFFFFFFFFFFFFFFB. Напоминаю, что при представлении восьмибайтового числа младшие четыре байта должны находиться по адресу, меньшему, чем старшие.
Вещественные числа
Для того чтобы использовать вещественные числа в командах процессора Intel(командах арифметического сопроцессора), они должны быть представлены в памяти компьютера в нормализованном виде. В общем случае нормализованный вид числа выглядит так:
Здесь ZN - знак числа, M - мантисса числа, обычно удовлетворяет условию M < 1, N - основание системы счисления, q показатель, в общем случае может быть и положительным и отрицательным числом. Числа, представленные таким образом, называют еще числами с плавающей точкой (или числами с плавающей запятой).
Рассмотрим конкретный пример. Попытаемся представить в нормализованном виде число 5,75. Переведем вначале это число в двоичную систему счисления. В данном случае это делается достаточно легко. Действительно, 5 — это 101, а 0,75 - это (1/2) + (1/4). Другими словами 5,75 = 0b101,11. Пишем далее 0b101.11 = 1.00111 * (2^3). Таким образом, мы имеем следующие компоненты нормализованного числа:
Заметим, что первая цифра мантиссы в таком представлении всегда равна 1, а, следовательно, ее можно и вообще не хранить, и в формате Intel так и поступают. Кроме этого нужно иметь в виду, что показатель q в действительности (для процессора Intel) хранится в памяти в виде суммы с некоторым числом, так чтобы всегда быть положительным. Процессор Intelможет работать с тремя типами вещественных чисел:
- короткое вещественное число. Всего для хранения отводиться 32 бита. Биты 0-22 резервируются для мантиссы. Биты 23-30 предназначены для хранения показателя q, сложенного с числом 127. Последний 31-й бит, предназначен для хранения знака числа (1 - знак отрицательный, 0 - положительный);
- длинное вещественное число. Для хранения такого числа отводится 64 бита. Биты 0-51 нужны для хранения мантиссы. Биты 52-62 предназначены для хранения числа q, сложенного с числом 1024. Последний 63-й бит определяет знак числа (1 - знак отрицательный, 0 - положительный);
- расширенное вещественное число. Для хранения числа отводится 80 битов. Биты 0-63 - мантисса числа. Биты 64-78 — показатель q, сложенный с числом 16383. 79-й, последний бит отводится для знака числа (1 - знак отрицательный, 0 - положительный).
Очевидно, пришла пора разобрать конкретный пример представления в памяти вещественного числа. Итак, пусть в программе на языке Си имеем объявление переменной:
float a=-80.5
Тип float - это короткое вещественное число, т. е. в памяти оно, согласно выше записанному, будет занимать 32 бита. Попытаемся теперь нашим обычным способом заглянуть в память. Вот они, четыре байта, которые и призваны представлять наше число:
00 00 a1 c2
Чтобы легче было разбираться, представим последовательность из четырех байтов в двоичном виде:
00000000 00000000 10100001 11000010
Или более понятным способом, начиная со старшего байта для выделения мантиссы, показателя и знака:
11000010 10100001 00000000 00000000
Выделим мантиссу. На нее отводиться 23 бита. Имеем, таким образом, двоичное число 0100001. Учтите, что биты мантиссы, отсчитываются, начиная со старшего (в данном случае 22-го) бита, а оставшиеся нули естественно отбрасываются, поскольку вся мантисса располагается справа от запятой. Правда, это еще не совсем мантисса. Как ранее было сказано, единица перед запятой в представлении отбрасывается. Так что мы должны восстановить ее. Поэтому мантиссой будет число 0b1,0100001. Знак всего числа, как мы видим, определяется единицей, следовательно, задает отрицательное число. А вот показатель нам следует получить из двоичного числа 0b10000101. В десятичном представлении это число 133. Вычитая число 127 (для короткого вещественного числа), получим 6. Следовательно, для того чтобы получить из мантиссы истинное дробное число, нужно сместить в ней точку на шесть разрядов вправо. Окончательно получаем 0b1010000,1. В шестнадцатеричной системе счисления это просто 0x50,8, а в десятичной получаем как раз 80,5.
В качестве тренировки я бы вам предложил следующую цепочку байтов:
00 80 fb 42
Попытайтесь доказать, что это есть ничто иное, как представление числа 125,75.
Из сказанного в данном разделе можно сделать вывод, что при использовании в программе вещественных чисел они могут стать приближенными еще до того, как с ними были произведены какие-либо действия. Это вызвано тем, что для записи чисел в память их нормализуют.
--> Глава 1. Параграф 1.8. <--Глава 1. Параграф 1.6.
Ассемблер вам в радость! Пока! Подписываемся на мой канал Old Programmer.
Я вижу, что вы забыли поставить ЛАЙК, не так ли?