На аппаратном уровне вся информация в компьютере представляет последовательность электрических сигналов. Например, в какой-то определенной ячейке памяти может быть иметься сильное напряжение, или оно может быть очень слабым. Для описания состояния сигнала информатике используется термина бит. По сути бит является наименьшей единицей информации в компьютере. Бит может иметь значение 1 (есть сигнал, что обычно соответствует напряжению от 2 до 5 V) или 0 (сигнал отсутствует или слабый - обычно от 0 до 2 V).
8 битов составляют байт. Фактически байт — это наименьшая единица информации, которую можно прочитать или записать в память большинством современных процессоров.
Один бит может принимать два значения: 0 и 1. Два бита вместе могут принимать четыре значения: 00, 01, 10 и 11. Три бита могут принимать восемь значений: 000, 001, 010, 011, 100, 101, 110 и 111. Обобщая, группа из n битов может принимать 2n значений. Таким образом, группа из 8 бит или 1 байт может представлять 28, то есть 256 уникальных значений. Таким образом, вся информация в компьютере фактически представляет последовательность бит.
Поскольку бит может иметь только два значения - 1 и 0, то для записи битов применяют двоичную систему исчисления. Вообще система исчисления представляет способ записи чисел. Например, в поседневной жизни мы пользуемся десятичной системой исчисления. Это значит, что основанием этой системы является число 10, а каждый символ числа может иметь 10 вариантов значений - от 0 до 9. В десятичной системе каждое число можно представить как сумму цифер чисел, умноженных на 10 в степени, соответствующей порядковому номеру цифры в этом числе (нумерация начинается с нуля). Например, стандартное число 123 можно представить следующим образом:
12310 = 1 * 102 + 2 * 101 + 3 * 100 = 100 + 20 + 3
Или возьмем другое десятичное число - 123,45
123,4510 = 1 * 102 + 2 * 101 + 3 * 100 + 4 * 10-1 + 5 * 10-2 = 100 + 20 + 3 + 0,4 + 0,05
В двоичной системе каждый символ числа может иметь только два значения - 1 и 0, например, число 1101. Чтобы перевести число из двоичной системы в десятичную умножаем значение каждого бита (1 или 0) на число 2 в степени, равной номеру бита (нумерация битов идет от нуля):
// перевод двоичного числа 1101 в десятичную систему 1 * 23 + 1 * 22 + 0 * 21 + 1 * 20 = 1 * 8 + 1 * 4 + 0 * 2 + 1 * 1 = 8 + 4 + 0 + 1 = 13
Перевод десятичного числа в двоичную систему выглядит несколько сложнее. Стандартный алгоритм преобразования подразумевает деление числа и результатов последующих делений на 2 и помещение остатков от деления в результат. Например, переведем десятичное число 13 в двоичную систему:
// перевод десятичного числа 13 в двоичную систему 13 / 2 = 6 // остаток 1 (13 - 6 *2 = 1) 6 / 2 = 3 // остаток 0 (6 - 3 *2 = 0) 3 / 2 = 1 // остаток 1 (3 - 1 *2 = 1) 1 / 2 = 0 // остаток 1 (1 - 0 *2 = 1)
Общий алгоритм состоит в последовательном делении числа и результатов деления на 2 и получение остатков, пока не дойдем до 0. Затем выстраиваем остатки в линию в обратном порядке и таким образом формируем двоичное представление числа. Конкретно в данном случае по шагам:
Делим число 13 на 2. Результат деления - 6, остаток от деления - 1 (так как 13 - 6 *2 = 1)
Далее делим результат предыдущей операции деления - число 6 на 2. Результат деления - 3, остаток от деления - 0
Делим результат предыдущей операции деления - число 3 на 2. Результат деления - 1, остаток от деления - 1
Делим результат предыдущей операции деления - число 1 на 2. Результат деления - 0, остаток от деления - 1
Последний результат деления равен 0, поэтому завершаем процесс и выстраиваем остатки от операций делений, начиная с последнего - 1101
Если число большое, то запись двоичных чисел может быть довольно длинной и поэтому не очень удобной. Например, число 23410
в двоичной системе равно 111010102
.
И для упрощения работы с двоичными числами применяется шестнадцатеричная система.
В шестнадцатеричной системе счисления двоичные числа разделены на группы по 4 бита. При 4 битах в группе количество возможных значений равно 24 или 16. Первым 10 из этих 16 чисел присваиваются цифры 0–9, а последним 6 — буквы A-F:
2-чная | 16-чная | 10-чная |
0000 | 0 | 0 |
0001 | 1 | 1 |
0010 | 2 | 2 |
0011 | 3 | 3 |
0100 | 4 | 4 |
0101 | 5 | 5 |
0110 | 6 | 6 |
0111 | 7 | 7 |
1000 | 8 | 9 |
1001 | 9 | 9 |
1010 | A | 10 |
1011 | B | 11 |
1100 | C | 12 |
1101 | D | 13 |
1110 | E | 14 |
1111 | F | 15 |
Двоичное число 11101010 можно представить более компактно, разбив его на две 4-битные группы (1110 и 1010) и записав их в виде шестнадцатеричных цифр EA
.
Алгоритм перевода из шестнадцатеричной системы в десятичную и обратно тот же, что и для двоичной, только вместо 2 используется число 16. Например, шестнадцатеричное число E6
можно представить следующим образом:
e6 = e * 161 + 6 * 160 = 14 * 16 + 6 = 23010 (десятичная система) = 1110 01102 (двоичная система)
А чтобы получить из 10-тичного числа 16-ричное, делим число на 16 и получаем остатки:
// перевод десятичного числа 230 в шестнадцатеричную систему 230 / 16 = 14 // остаток 6 (230 - 16 * 14 = 230 - 224) 14 / 16 = 0 // остаток 14 или E в 16-й системе // результат 0xE6
Чтобы указать, что число шестнадцатеричное, перед ним указываются символы 0X
или 0x
, например, 0xE6
Стоит отметить, что 4 бита, которые соответствуют одной шестнадцатеричной цифре, называется nibble
или полубайт(слог, тертрада)
При работе с разными системами счисления легко запутаться. Например, какую систему в реальности представляет число 1010
? Оно может равным образом представлять и
десятичную, и двоичную, и шестнадцатеричную. И чтобы указать, что число относится к определенной системе счисления, используют различные обозначения. Так, для указания,
что число является двоичным, перед число обычно ставится префикс 0b:
0b1010 - двоичное число (в десятичной системе равно 10, а в шестнадцатеричной - A)
Чтобы указать, что число является шестнадцатеричным, перед число обычно ставится префикс 0x:
0x1010 - шестнадцатеричное число (в десятичной системе равно 4112, а в двоичной - 1000000010000)
Для представления отрицательных чисел обычно применяется two’s complement(дополнение до 2). С точки зрения математики чтобы получить
отрицательный аналог числа надо от 0 (нуля) вычесть это число. Например, для получения -1 надо произвести операцию 0 - 1 = -1
. С точки зрения архитектуры компьютера в качестве
0 выступает число 2N. В данном случае степень N представляет количество битов в числе.
Например, наше число состоит из 8 бит (1 байт), наподобие 0000 0001
(1 в десятичной системе). И мы хотим получить число -1. Для этого выполняем следующую операцию:
28 - 1 = 256 - 1 = 255
Но 255 - это в десятичной системе. А как это будет выглядеть в двоичной системе:
28 - 1 = 10000 0000 - 0000 0001 = 1111 1111
Таким образом, для 8 битное отрицательное число -1 в двоичной системе будет представлять 1111 1111
Аналогичная операция в шестнадцатеричной системе:
28 - 1 = 0x100 - 1 = 0xFF
Если же мы выполним обратную операцию - к 1111 1111
прибавим изначальное число 0000 0001
, то мы получим степень двойки. Поэтому подобное представление отрицательных чисел и называется
дополнение до 2-х.
Простой способ получить из положительного числа отрицательного и наборот (то есть фактически умножение на -1) заключается в том, чтобы инвертировать биты - биты 0 поменять на 1, а 1 на 0, и затем прибавить 1. Например, получим число -3. Для этого сначала возьмем двоичное представление числа 3:
310 = 0000 00112
Инвертируем биты
~0000 0011 = 1111 1100
И прибавим 1
1111 1100 + 1 = 1111 1101
Таким образом, число 1111 1101
является двоичным представлением числа -3, что в шестнадцатеричной системе аналогично 0xFD
Другой пример, число 1 в двоичной системе равно 0b0000001
. Чтобы получить число -1, сначала инвертируем биты:
~0000 0001 = 1111 1110
Далее прибавляем 1:
1111 1110 + 1 = 1111 1111
То есть число -1 в двоичной системе равно 1111 1111
или 0xFF
(в шестнадцатеричной системе)
Подобным образом можно получить обратно число 1:
~1111 1111 = 0000 0000 0000 0000 + 1 = 0000 0001
Соответственно, в зависимости от того, какое именно это число - положительное или отрицательно, интерпретировать это число можно по разному. Например,
если число 1111 1111
рассматривается как положительное, то в десятичной системе оно равно 255. Если же оно рассматривается как отрицательное, то в десятичной системе
оно равно -1.
Таким образом, 8-битные числа со знаком охватывают диапазон от -128 до 127, а 8-битные числа без знака - от 0 до 255.
Основу программы на ассемблере составляют инструкции - некоторые действия, например, сложение двух значений, помещение в регистр значения и т.д. При выполнении программы процессор выбирает и интерпретирует каждую инструкцию. Как и все данные, каждая инструкция, каждое действие в программе представляет последовательность битов. Каждой инструкции сопоставляется определенный машинный двоичный код, который также называется кодом инструкции или кодом операции (опкод, opcode).
Код операции — это один байт, определяющий основную операцию инструкции. Например, инструкция, которая копирует в регистр RAX число 1, имеет опкод C7 в шестнадцатеричной форме или 11000111 в двоичной форме. В зависимости от инструкции, ее операндов опкод меняется. Например, инструкция, которая копирует в регистр EAX число 1, имеет опкод B8 в шестнадцатеричной форме или 10111000 в двоичной форме. К опкодам инструкций следует добавить коды/значения операндов - регистра и чисел.
Написание машинного кода вручную возможно, но излишне громоздко. На практике вместо опкодов применяются так называемые мнемоники - человекочитаемые названия инструкций. Например, инструкция, которая копирует в регистр некоторое значение, имеет мнемонику mov (от слова "move" - помещать, поместить). А чтобы скопировать в регистр RAX число 1, в ассемблере GAS нам достаточно написано команду
movq $1, %rax
А чтобы скопировать в регистр EAX число 1, нам достаточно написано команду
movl $1, %eax
Это довольно удобнее, чем в бинарной форме вводить команды.
Программа состоит из набора подобных инструкций. Процессор запускает программы через цикл выборки-выполнения (fetch-execute cycle). Компьютер считывает по одной инструкции за раз. Для этого процессор обращается к специальному регистру - указателю команд (или регистр IP), который также называется программным счетчиком (или PC) и который хранит адрес инструкции для выполнения. По сути, компьютер выполняет бесконечный цикл следующих операций:
Считывает инструкцию с адреса памяти, указанного указателем инструкции - регистром IP/PC
Декодирует инструкцию (т. е. выясняет, что означает инструкция)
Перемещает указатель инструкций (регистр IP/PC) к следующей инструкции
Выполняет указанную инструкцию
Особенностью инструкций в ассемблере GAS является то, что многие инструкции имеют вариации для работы с различными типами данных. Все эти вариции инструкций образуются по одной схеме: к названию инструкции прибавляется суффикс, который указывает на тип данных. Так, в ассемблере GAS мы можем работать с 8-разрядными, 16-разрядными, 32-разрядными и 64-разрядными числами. Для каждого из этих типов чисел предназначен свой суффикс:
q: для 64-разрядных чисел
l: для 32-разрядных чисел
w: для 16-разрядных чисел
b: для 8-разрядных чисел
Соответственно для инструкции mov, которая копирует данные, есть следующие вариации
movq: для копирования 64-разрядных чисел
movl: для копирования 32-разрядных чисел
movw: для копирования 16-разрядных чисел
movb: для копирования 8-разрядных чисел