Лисья Нора

Оглавление


§ Регистры

Как я и упоминал в предыдущей главе, регистры в процессоре составляют основу его работы. Обычно в современных процессорах набор регистров называют "регистровым файлом", и кстати говоря, в современных процессорах регистровых файлов может быть очень много, из-за особенностей так называемой "суперскалярности" и конвейерной обработки инструкции. Эти темы очень сложны и я не буду касаться их. В 386 процессоре хоть и был конвейер, но я все равно не стану его реализовывать по той причине, что это очень сложно. Материал и без того очень сложный, чтобы дополнительно разбирать в нем то, как устроен конвейер.
Ниже я приведу общую схему регистров процессора:
31...16 |15..8 7..0| N
--------+-----------+---
e | AH | AL | 0 = AX
e | CH | CL | 1 = CX
e | DH | DL | 2 = DX
e | BH | BL | 3 = BX
e | SP | 4
e | BP | 5
e | SI | 6
e | DI | 7
--------+-------+---+---
- | ES | 0
- | CS | 1
- | SS | 2
- | DS | 3
- | FS | 4
- | GS | 5
--------+-----------+---
e | IP |
e | FLAGS |
Отмечу, что регистры AX, BX, CX и DX можно разделить 2 части, а точнее, на два 8-битных значения. Например, регистр AX имеет старшую часть в виде регистра AH и младшую в виде регистра AL. Это один и тот же регистр, поделенный на две части. Это значит, что мы способны оперировать старшей и младшей частью регистра AX, указывая его составные части. Это удобно, но вот для регистра EAX это уже не так. Мы не имеем прямого доступа к старшей 16-битной части, поэтому для того, чтобы прочитать старшую часть, необходимо сделать "разворот" битов. О том, как именно это делается, я думаю, опишу далее, чтобы не усложнять материал с самого начала до невозможности. Всему свое время.
Для чего необходимы регистры? Они хранят промежуточные значения вычислений. Я продемонстрирую на простом примере как процессор вычисляет что-либо.
Задача: необходимо получить 2 значения из памяти, сложить их друг с другом и записать в память.
Исполнение:
Конечно, стоит отметить, что ячеек памяти придется использовать 4, потому что 1 байт памяти равен 8 бит, а мы оперируем 32х битными регистрами, так что процессору придется записать 4 байта в память.
Значение регистров для процессора огромно. Без них он, фактически, не сможет выполнить ни одну операцию, потому что необходимо где-то хранить промежуточное значение, неважно даже, один это будет регистр или несколько. Да, для того чтобы создать минимально работающий процессор, нам нужны как минимум 2 регистра: это аккумулятор (EAX) и программный счетчик (EIP). Без программного счетчика процессор не будет знать где он читает программу, а без аккумулятора не совершит ни единой арифметической или логической операции.

§ Программный счетчик

Второй по важности, а на самом деле, важность с регистрами одинаковая, является регистр программного счетчика. Без него невозможно выполнение программ в принципе. Почему так, легко догадаться. Представьте себе ситуацию – вы читаете книгу, и вы точно знаете, где именно, на какой строке и на какой букве или слове находится ваш взгляд. Это и есть "программный счетчик", поскольку вы знаете 1) страницу, где читаете 2) номер строки 3) номер символа. Да, это пример не до конца удачный, потому что никто не считает ни страницы, ни строки, ни символы, но сам принцип заключается в том, что как для читателя имеет значение точка где сейчас находится его взгляд, так и для процессора имеет огромное значение точка, где он читает программу и выполняет её. И эта точка называется "программный счетчик".
Что такое выполнение программы? Программа – это набор инструкции для выполнения процессором. Для процессора программа выглядит как набор идущих друг за другом байт, также как для человека это набор идущих друг за другом символов, образующих слова. Слово для читателя – инструкция для процессора. Одно слово имеет разную длину, как и инструкция может также иметь разную длину. У каждого слова есть его "корень" (опкод), а также дополнительные атрибуты, такие как "приставка", "суффикс", "окончание". Для процессора это "префикс" (приставка), "операнды" (суффикс), "непосредственное значение" (окончание).
В каком-то смысле, исполнение инструкции для процессора это чтение книги из оперативной или постоянной памяти (зависит от памяти). В любом случае, процессор смотрит в память по определенному адресу, извлекает из него следующее слово (стадия называется FETCH), читает его (стадия DECODE) и выполняет (EXECUTE).

§ Флаги

Помимо регистров общего и не только (таких как указатель стека), назначения, а также программного счетчика, в процессоре есть регистр флагов, без которого обойтись не то что бы нельзя, но крайне затруднительно. Существуют процессоры, в которых регистр флагов даже отсутствует, но такие процессоры настолько примитивны, что их даже нигде особо и не используют.
Для чего нужны флаги? Они отображают текущее состояние процессора после выполнения очередной инструкции, а также в регистре флагов содержится важная информация о том, как именно должны работать некоторые инструкции и то, как вообще должен действовать процессор. Флаги – это текущая информация о том, что процессор только что сделал или может быть, сделает. Если например, сравнивать процессор и офисного работника, то можно сказать, допустим, что есть на столе у работника небольшой флажок, который может быть или опущен или поднят и он означает то, занят ли работник какой-то задачей или нет. К примеру, если работник занят задачей, то он возводит флаг "занят" и все проходящие мимо видят, что флаг возведен и что бедного трудягу лучше не трогать, а то он бывает нервным, да и некогда ему заниматься ерундой вроде пустопорожних разговоров, чем я занят, кстати говоря, сейчас.
Теперь представим, что работник закончил задачу и опустил флаг "занят", но возвёл флаг "готово", чем сильно порадовал мимо же проходящего начальника. Он одобрительно кивнул в его сторону, взял необходимые бумаги и работник опустил флаг "готово".
Этот пример наглядно показывает, для чего в целом необходимы флаги процессору. Существуют два типа флагов – арифметические и управляющие. Первый тип флагов меняется в зависимости от результата выполнения очередной инструкции, связанной с арифметикой или логикой, или инструкциями, которые определенным образом работают с флагами, к примеру, устанавливают и снимают некоторые флаги. Второй тип флагов является, как можно догадаться, именно управляющей функцией. Они обычно не устанавливаются и не снимаются постоянно, а управляют процессом выполнения инструкции. К примеру, есть флаг, который называется DF (Direction Flag), если он возведен (равен =1), то тогда любая строковая инструкция, например, моя любимая – STOSB двигает индексный регистр налево (уменьшает или декремент) после того как выполнено копирование из регистра AL в память, а если DF=0, то STOSB двинет регистр направо (увеличение или инкремент).
Помимо регистра флагов EFLAGS существуют флаги в регистрах управления состоянием процессора CRx и DRx, но об этом сильно позже, потому что в основном и целом они касаются защищенного режима работы процессора.
Ознакомимся с некоторыми наиболее важными флагами.
# Имя Описание
0 | CF | Перенос (Carry)
1 | 1 | Всегда =1
2 | PF | Четность (Parity)
3 | 0 | Всегда =0
4 | AF | Полуперенос (Aux)
5 | 0 | Всегда =0
6 | ZF | Признак нуля (Zero)
7 | SF | Знак (Sign)
8 | TF | "Ловушка" (Trap)
9 | IF | Разрешение прерываний (Interrupt)
10 | DF | Направление (Direction)
11 | OF | Переполнение (Overflow)
Помимо первых 12-ти флагов есть также флаги IOPL (занимает 11-12 биты), NT (14-й бит), RF (16-й) и VM (17-й). Некоторые были введены начиная с 80386 и занимаются управлением уровнем привилегии программы в защищенном режиме и управлением многозадачностью.
Отмечу что не все биты в регистре EFLAGS используются. Старшие биты, особенно начиная с 22 и заканчивая 31, вообще зарезервированы и даже не используются в самих современных процессорах и по сей день (4 янв 2026 г.). Вместо этого в новых процессорах есть инструкция CPUID, которая выдает абсолютно исчерпывающую информацию о возможностях процессора, а также флаги в регистрах управления CRx (Control Register 0-3).
Некоторые флаги, такие как CF используются не только в арифметически-логических инструкциях, но также инструкциях, которые разными способами крутят, ворочают, сдвигают значение по битам влево или вправо. В любом случае данный флаг имеет значение "перенос" или "заем". Допустим, нам надо сложить два числа (ADD), 255 и число 3, и постараться уместить результат в 8-битном регистре. Ясное дело, что число большее чем 255 в регистр 8 бит вместится не сможет и потому там появится число 2, но флаг CF будет равным 1, что сигнализирует о том, что после выполнения сложения у нас образовался перенос в старший разряд. Дальше, зная этот факт, мы сможем с помощью инструкции ADC сложить старшие разряды, и при сложении двух чисел будет учитываться также то, что находится во флаге CF – если там 1, то после сложения добавится 1 сверху, и да, после такого сложения тоже может возникнуть перенос разряда, если значение не вместилось.
Другой флаг, ZF ставится, если результат в итоге получился 0. Да, если мы сложим 255 + 1 и получим 0, то флаг ZF станет равным =1.
Особо много расписывать значение каждого флага не стану, поскольку более исчерпывающие сведения можно получить и если открыть страницу в Википедии, так и далее по тексту во время разработки той или иной инструкции, все это будет повторено не один раз. Расскажу лишь вкратце. Существуют 2 редко или почти никогда не используемых флага – PF, который определяет, четно ли количество единиц в младших 8 битах результата, и AF, который работает точно также как и CF, но учитывается не старший разряд операнда (который может быть разной длины – 8, 16 или 32 бита), а только 4-го бита. Он предназначен для работы с инструкциями двоично-десятичной направленности, для коррекции результата. Эти инструкции (DAA, DAS например) сейчас совершенно нигде не используются и остаются только в целях совместимости. Да, в 64-х битном режиме процессора данные инструкции вообще удалены.
Что касательно флага SF, то он указывает на то что после исполнения инструкции в результате получился отрицательный (=1) или положительный результат (=0). Число, если оно представлено как знаковое, в старшем разряде если установлена 1, то такое число считается отрицательным (меньше 0), иначе положительным (более или равным 0).
А вот флаг OF (переполнение) требует отдельного разговора в отдельной главе, потому что его понимание далеко не так просто. В знаковых операциях важно сохранить значение знака. Так как возможна ситуация, когда допустим, к числу 127 добавляется 1, то по логике, должно стать число 128, но это не так, потому что происходит перенос из 6 в 7-й разряд, и получается что число приобретает отрицательный знак, что дает вместо 128 число -128. Здесь мы говорим о том, что произошло переполнение знакового результата.
Да, это может пока показаться сложным, но я объясню это позже. Сейчас же мы пока познакомились с главными арифметическими флагами. Касательно флагов TF, IF, DF – это флаги управления. Если TF=1, то это говорит процессору, что после каждой выполненной инструкции необходимо вызвать прерывание #1. Ранее очень часто использовался для отладки, но теперь в этом больше необходимости нет, особенно с приходом в процессорную архитектуру технологии виртуализации. Флаг IF, если установлен, разрешает процессору реагировать на внешние прерывания, а значение флага DF я уже объяснил ранее.

§ Сегментные регистры

Из последнего, что я разберу в этой главе, будут сегментные регистры.
Вначале, до того как был создан процессор 8086, адресное пространство составляло всего лишь 64Кб (16-бит), и если и можно было как-то расширять, то только маппить через порты определенный участок памяти. Конечно, с ростом количества доступной памяти, потребовалось как-то иначе адресовать, чтобы можно было указывать больший объем. Для этого в архитектуру 8086 завели новые регистры – сегментные. Их использование помогло расширить объем доступной памяти до 1 Мб (20 бит), при этом оставаясь в рамках всё того же 16-битного кода и адресации.
Оставался вопрос: каким способом теперь их использовать? Переделывать систему команд при успешно работающем уже существующем программном обеспечении (ПО) было бы нерационально и привело бы к краху, так что инженеры додумались до того, чтобы использовать префиксы, которые бы меняли сегмент, который используется по умолчанию в адресации памяти. То есть так – перед опкодом ставится байт (префикс), который означает выбор сегмента по умолчанию и далее инструкция работает как обычно, но вместо стандартного сегмента (либо DS – данные, либо SS – стек) использует указанный.
Всего существует (в 386-м) шесть сегментных регистров, каждый из которых имеет свое собственное назначение. CS необходим для указания адреса исполняемого кода, то есть он всегда идет совместно с IP и EIP. SS всегда идет в паре с регистром SP/ESP, указывая на положение стека. Сегмент DS – сегмент по умолчанию для любых операции с данными, а ES может использоваться и как вспомогательный сегмент и участвовать в строковых инструкциях.
Как формируется адрес, используя сегментную модель? Довольно просто. Сегмент – это "параграф", единица составляет масштаб из 16 байт. Пример: если у нас в сегменте CS=10, то необходимо умножить 10 на 16 и получим 160. При записи в 16-ричной системе счисления подобное вычисление очень простое, ведь необходимо всего лишь добавить справа один ниббл (4 нуля), чтобы получить необходимое смещение в памяти. Общая формула для расчета полного сегмента будет такой:
Адрес = Сегмент (16 бит) * 0x10 + Смещение (16 бит)
Если мы попробуем записать в сегмент максимальное значение FFFFh, и в смещение тоже максимальное значение, то получится следующее:
Адрес = FFFFh x 10h + FFFFh = FFFF0h + FFFFh = 10FFEFh
С помощью сегментной модели возможно адресовать память даже выше 1Мб (на почти 64кб), что успешно использовалось более старшими версиями DOS для того чтобы хранить свой код в так называемой HMA (High Memory Area), освобождая место в памяти под другие программы, и в большинстве случаев это здорово помогало.
Однако с приходом 32-х битной адресации, с помощью которой становилось доступным адресация до 4 Гб памяти, немыслимого объема для того времени, сегментные регистры не ушли, но поменяли свой "профиль". В защищенном режиме процессора, который был уже добавлен в 80286-м процессоре, сегменты стали называться "селекторами", и вместо того чтобы указывать на параграф в памяти, они теперь указывали на ячейку в таблице дескрипторов, а также сам селектор приобрел 2 флага – помимо номера элемента в дескрипторной таблице, указывалось также из какой именно берется элемент (локальная или глобальная таблица) и текущий уровень доступа (кольцо от 0 до 3). Так что при помощи селекторов теперь можно было указывать любой участок памяти любого размера, ограничивая возможность записать "не туда", и обеспечивая защиту памяти от несанкционированного доступа.
О принципах работы защищенного режима можно рассказать много. Думаю, что однажды это получится, но перед тем как сделать поддержку защищенного режима и более сложных вещей вроде страничной адресации, нам придется совершить долгий и трудный путь по разработке базовых инструкции 8086 и многого другого.