Я вообще хотел без примеров
Думал, если возникнут вопросы — отвечу.
Ну ладно, напишу, а то жалко выбрасывать. Надеюсь, не сильно испорчу.
Минимально необходимые сведения о хранении целых чисел.Надеюсь, все знают, что такое бит, байт, двоичная система счисления и шестнадцатеричная система счисления.
Также надеюсь, что все знают про битовые операции OR, AND, XOR — они в ассемблере точно такие же, как в обычных языках программирования.
(на всякий случай: в 16-ричной системе счисления цифры A=10, B=11, C=12, D=13, E=14, F=15; имеется очень простое соотношение между 16-ричной и двоичной записями, а именно: 4 бита всегда соответствуют одной 16-ричной цифре: 0=0000, 1=0001, 2=0010, ..., 7=0111, ..., A=1010, ..., F=1111).
В ассемблерах по умолчанию числа записываются в десятичной системе счисления. Поддерживаются также двоичная (постфикс "b" после строки двоичных цифр), шестнадцатеричная (постфикс "h", число ОБЯЗАНО начинаться с цифры 0-9) и восьмеричная (постфикс "o", сейчас почти не используется):
11 = 0Bh = 13o = 1011b
Также некоторые ассемблеры (не знаю точно про FASM) поддерживают запись шестнадцатеричных чисел в нотации языка Си (0xCF) или Паскаля ($A000). Для иллюстративных целей путаницы не должно быть.
Оперативная память в программах под Windows представляется очень просто: это массив байтов, для 32-битной программы - длиной
, для 64-битной программы — длиной
. Это всё теоретически, конечно, некоторые области памяти недоступны и при попытке обращения к ним возникает исключение, а некоторые области памяти доступны только для чтения. В этом массиве индекс чего угодно (например, какой-либо структуры в памяти) называется
адресом этой структуры. В 32-битных программах адрес 32-битный (4 байта), в 64-битной программе — 64-битный (8 байт).
Информацию в байтах памяти/регистрах, вообще говоря, можно интерпретировать как угодно. Например, значение 00000001b можно интерпретировать как "Январь" или как "Тёмно-синий цвет", но мы здесь интересуемся только целыми числами. Процессор умеет интерпретировать значения в регистрах и байтах памяти, как целые числа. Родными для процессоров x86 являются форматы 8-битного (ВНЕЗАПНО, называется byte), 16-битного (называется word), 32-битного (dword) и 64-битного (qword) целого числа. Команды ассемблера для размещения в памяти этих значений называются соответственно db ("Define Byte(s)"), dw ("Define Word(s)"), dd ("Define Double word(s)"), dq ("Define Quad word(s)"):
db 13, 10, 255 ; три байта
dd 1, 1000000000 ; 8 байт
Целые числа могут интерпретироваться как
беззнаковые либо как
знаковые.
Например, значение 0D1h = 11010001b, интерпретируемое как 8-битное значение без знака, представляет число 13*16+1=209, это обычная 16-ричная или двоичная арифметика. Числа без знака всегда неотрицательные и задают значения от 0 до
, где
— разрядность: однобайтовые числа без знака представляют значения от 0 до
, 16-битные — от 0 до
, 32-битные — от 0 до
и т.д.
У чисел со знаком за знак отвечает старший двоичный разряд, если он равен 0 — число положительное или 0, если 1 — отрицательное. Однако тут новичков ждёт сюрприз: число -1 представляется не значением 81h = 10000001b, как можно было бы подумать, а значением 0FFh = 11111111b. Почему так?
Дело в том, что операции сложения и вычитания целых чисел в процессоре работают одинаково, независимо от того, без знака у нас число или со знаком. Можно смело сказать, что процессор не знает, какие значения он складывает или вычитает: знаковые или беззнаковые. При этом эти операции всегда успешны при любых слагаемых и вычитаемых, никогда не вызывают ошибок из-за переполнения. Как мы уже знаем, переполнение при вычислениях игнорируется, в назначении (регистре или памяти) сохраняются младшие 8, 16, 32 или 64 бита результата, а если был перенос двоичной единицы в следующий (9-й, 17-й, 33-й или 65-й), то он "пропадает". На самом деле пропадает не совсем, он попадает в так называемый "флаг переноса", который можно проверить инструкциями JC/JNC/SETC/SETNC. Выше уже упомянули инструкции ADC/SBB, которые незаменимы для организации сложения/вычитения чисел с произвольно большой разрядностью, например, для арифметических действий с 64-битными числами в 32-битной программе.
Так вот, если мы к 8-битному значению 0FFh добавим единицу, мы получим в назначении 0. Значит, если мы сделаем обратную операцию и отнимем от 0 единицу, мы должны получить 8-битное значение 0FFh, значит, именно оно должно интерпретироваться как -1. И т.д.: 0FEh должно интерпретироваться как -2, 0FDh = -3, ..., 80h = -128. Вышеупомянутое значение 0D1h = 11010001b интерпретируется как 209-256 = -47.
Такой способ записи целых отрицательных чисел называется "дополнительный код" (англ. two's complement). Насколько мне известно, он используется во всех современных процессорах, так что можно считать, что других способов записи не существует.
При попытке отнять от -128 = 80h единицу мы получим 7Fh = 01111111b = 127 (знаковый бит 0, значит, число положительное). Получается переполнение, хотя если бы мы интерпретировали 80h как беззнаковое, то переполнения не было бы: 128-1=127. Кроме "флага переноса" в процессоре есть ещё и "флаг знакового переполнения" или же просто "флаг переполнения", который устанавливается, когда результат операции отличается от результата знакового сложения. Проверить флаг знакового переполнения можно инструкциями JO/JNO/SETO/SETNO.
Итак, получается удивительная штука: процессор складывает и вычитает числа, однако сам не ведает, какие они: знаковые или беззнаковые. Только мы сами интерпретируем операнды и результат как знаковые или беззнаковые и проверяем, было ли переполнение или нет, анализируя флаги "переноса" и "переполнения".
Итак, знаковые 8-битные значения представляют числа от -128 до +127. И в общем, знаковые N-битные значения представляют числа от
до
.
Например, 16-битные значения со знаком представляют
до
. На всякий случай: 32-битное значение со знаком -1 будет записываться как 0FFFFFFFFh.
Другие операции, помимо сложения и вычитания, могут "знать" о знаковости числа. Например, если нам нужно перевести 8-битное число 0FFh в 16-битную форму, нам нужно знать, оно знаковое или беззнаковое. Если оно беззнаковое, то это 255, и оно должно быть представлено как 00FFh. Но если оно знаковое, то это -1 и должно быть представлено как 0FFFFh. Для "расширения" разрядности
знаковых чисел в процессоре есть специальные инструкции (MOVSX, CBW, CWD, CWDE, CDQ) в то время как для
беззнаковых используются другие инструкции (MOVZX либо явное обнуление старших разрядов инструкцией AND). Также существуют разные инструкции для знакового и для беззнакового умножения и деления, почему — попробуйте разобраться самостоятельно.
При хранении многобайтных значений в памяти размещается сначала байт, представляющий 8 самых младших разрядов, а затем байты, представляющие более старшие разряды. Это так называемый Little endian (LE) byte order (порядок байт), он справедлив для процессоров x86. Для других, в т.ч. и широко распространённых процессоров, может применяться Big Endian (BE) порядок байт, когда старшие байты размещаются вначале! Но мы не будем рассматривать архитектуру Big Endian. Поэтому, например, команда
будет у нас всегда эквивалентна команде
(-1633911 в 16-ричном виде равно 0FFE71189h)
Многобайтные значения в памяти адресуются своим первым (в нашем случае младшим) байтом. В некоторых случаях инструкция записывается так, что непонятно, сколько байт она должна прочитать/записать. Например, инструкция "прибавить единицу к числу по адресу 012345678h" могла бы быть записана так:
однако из такого способа записи непонятно, нужно ли добавить единицу к 8-битному значению по этому адресу, к 16-битному, к 32-битному или же к 64-битному. Здесь мы вынуждены явно указывать размер операнда:
для 8-битных значений — byte ptr
для 16-битных значений — word ptr
для 32-битных значений — dword ptr
для 64-битных значений — qword ptr:
add word ptr [012345678h], 1
В некоторых инструкциях это излишне, поскольку один из операндов - регистр, у которого разрядность видна сразу из названия (значит, у другого операнда разрядность такая же):
в этом случае AX — 16-разрядный регистр, поэтому указание "word ptr" для второго операнда излишне.