Сколько бит помещается в регистр ax в процессорах семейства intel
Перейти к содержимому

Сколько бит помещается в регистр ax в процессорах семейства intel

  • автор:

Introducing new Paper mode

Как называется характеристика процессора, определяющая максимальное количество двоичных разрядов, которые процессор способен обработать за одну команду?

разрядность
микрокоманда
Multiple Choice
Please save your changes before editing any questions.
Как называются данные, необходимые для выполнения некоторой команды процессора?
разрядность
микрокоманда
Multiple Choice
Please save your changes before editing any questions.
Что хранится в регистре состояния процессора?
результат последней операции
степень загруженности процессора
свойства результата последней операции
результат проверки памяти
температура процессора
Multiple Choice
Please save your changes before editing any questions.
Отметьте все функции арифметико-логического устройства (АЛУ)
анализ результата
выполнение вычислений
определение местоположения данных
загрузка данных в регистры
расшифровка команд
Multiple Choice
Please save your changes before editing any questions.

Как называется характеристика процессора, которая определяет количество тактовых импульсов за 1 секунду?

разрядность
тактовая частота
микрокоманды
Multiple Choice
Please save your changes before editing any questions.
Отметьте все функции устройства управления (УУ)
расшифровка команд
выполнение вычислений
определение местоположения данных
анализ результата
загрузка данных в регистры
Multiple Choice
Please save your changes before editing any questions.
Сколько бит помещается в регистр AX в процессорах семейства Intel?
Multiple Choice
Please save your changes before editing any questions.
Как называется элементарное действие, из которых состоит каждая машинная команда?
разрядность
микрокоманда
Multiple Choice
Please save your changes before editing any questions.
Выберите правильное окончание фразы «RISC-процессор — это процессор с . »
рискованным набором команд
полным набором команд
сокращенным набором команд
изменённым набором команд
Multiple Choice
Please save your changes before editing any questions.
Выберите правильное окончание фразы «CISC-процессор — это процессор с . »
рискованным набором команд
полным набором команд
сокращенным набором команд
изменённым набором команд
Multiple Choice
Please save your changes before editing any questions.
Как называется интервал между двумя соседними управляющими импульсами, поступающими в процессор?
разрядность
микрокоманда
Multiple Choice
Please save your changes before editing any questions.
Какие блоки входят в состав процессора?
устройство управления
контроллеры
арифметико-логическое устройство
постоянное запоминающее устройство
Multiple Choice
Please save your changes before editing any questions.
Отметьте все правильные утверждения.
при тактовой частоте 4 ГГц процессор выполняет 4 млрд микрокоманд в секунду
разрядность процессора обычно определяют как размер регистров
разрядности шины данных и шины адреса всегда совпадают
тактовая частота полностью определяет быстродействие процессора
разрядность шины данных определяет максимальный объём памяти
Multiple Choice
Please save your changes before editing any questions.
Разрядность регистров определяется .
размером регистров процессора в битах
размером регистров процессора в байтах
количеством адресных линий
максимальным количеством битов
Multiple Choice
Please save your changes before editing any questions.
Разрядность шины данных определяется .
размером регистров процессора в битах
размером регистров процессора в байтах
количеством адресных линий
максимальным количеством битов
Multiple Choice
Please save your changes before editing any questions.
Разрядность шины адреса определяется .
размером регистров процессора в битах
размером регистров процессора в байтах
количеством адресных линий
максимальным количеством битов
Multiple Choice
Please save your changes before editing any questions.

Устройство, предназначенное для автоматического считывания команд программы, их расшифровки и выполнения, называется .

Multiple Choice
Please save your changes before editing any questions.
Отметьте все функции устройства управления (УУ)
извлечение из памяти очередной команды
выполнение вычислений
занесение в АЛУ исходных данных
анализ результата
сохранение результата
Multiple Choice
Please save your changes before editing any questions.
Арифметико-логическое устройство (АЛУ) состоит из .
двух регистров
схема управления операциями
Multiple Choice
Please save your changes before editing any questions.
Арифметико-логическое устройство (АЛУ) работает с .
целыми числами
вещественными числами
натуральными числами
рациональными числами
Explore all questions with a free account

Continue with Google Continue with Microsoft Continue with email Continue with phone

Записки программиста

Шпаргалка по основным инструкциям ассемблера x86/x64

12 октября 2016

В прошлой статье мы написали наше первое hello world приложение на асме, научились его компилировать и отлаживать, а также узнали, как делать системные вызовы в Linux. Сегодня же мы познакомимся непосредственно с ассемблерными инструкциями, понятием регистров, стека и вот этого всего. Ассемблеры для архитектур x86 (a.k.a i386) и x64 (a.k.a amd64) очень похожи, в связи с чем нет смысла рассматривать их в отдельных статьях. Притом акцент я постараюсь делать на x64, попутно отмечая отличия от x86, если они есть. Далее предполагается, что вы уже знаете, например, чем стек отличается от кучи, и объяснять такие вещи не требуется.

Регистры общего назначения

Регистр — это небольшой (обычно 4 или 8 байт) кусочек памяти в процессоре с чрезвычайно большой скоростью доступа. Регистры делятся на регистры специального назначения и регистры общего назначения. Нас сейчас интересуют регистры общего назначения. Как можно догадаться по названию, программа может использовать эти регистры под свои нужды, как ей вздумается.

На x86 доступно восемь 32-х битных регистров общего назначения — eax, ebx, ecx, edx, esp, ebp, esi и edi. Регистры не имеют заданного наперед типа, то есть, они могут трактоваться как знаковые или беззнаковые целые числа, указатели, булевы значения, ASCII-коды символов, и так далее. Несмотря на то, что в теории эти регистры можно использовать как угодно, на практике обычно каждый регистр используется определенным образом. Так, esp указывает на вершину стека, ecx играет роль счетчика, а в eax записывается результат выполнения операции или процедуры. Существуют 16-и битные регистры ax, bx, cx, dx, sp, bp, si и di, представляющие собой 16 младших бит соответствующих 32-х битных регистров. Также доступны и 8-и битовые регистры ah, al, bh, bl, ch, cl, dh и dl, которые представляют собой старшие и младшие байты регистров ax, bx, cx и dx соответственно.

Рассмотрим пример. Допустим, выполняются следующие три инструкции:

(gdb) x/3i $pc
=> 0x8048074: mov $0xaabbccdd,%eax
0x8048079: mov $0xee,%al
0x804807b: mov $0x1234,%ax

Значения регистров после записи в eax значения 0 x AABBCCDD:

(gdb) p/x $eax
$1 = 0xaabbccdd
(gdb) p/x $ax
$2 = 0xccdd
(gdb) p/x $ah
$3 = 0xcc
(gdb) p/x $al
$4 = 0xdd

Значения после записи в регистр al значения 0 x EE:

(gdb) p/x $eax
$5 = 0xaabbccee
(gdb) p/x $ax
$6 = 0xccee
(gdb) p/x $ah
$7 = 0xcc
(gdb) p/x $al
$8 = 0xee

Значения регистров после записи в ax числа 0 x 1234:

(gdb) p/x $eax
$9 = 0xaabb1234
(gdb) p/x $ax
$10 = 0x1234
(gdb) p/x $ah
$11 = 0x12
(gdb) p/x $al
$12 = 0x34

Как видите, ничего сложного.

Примечание: Синтаксис GAS позволяет явно указывать размеры операндов путем использования суффиксов b (байт), w (слово, 2 байта), l (длинное слово, 4 байта), q (четверное слово, 8 байт) и некоторых других. Например, вместо команды mov $ 0xEE , % al можно написать movb $ 0xEE , % al , вместо mov $ 0x1234 , % ax — movw $ 0x1234 , % ax , и так далее. В современном GAS эти суффиксы являются опциональными и я лично их не использую. Но не пугайтесь, если увидите их в чужом коде.

На x64 размер регистров был увеличен до 64-х бит. Соответствующие регистры получили название rax, rbx, и так далее. Кроме того, регистров общего назначения стало шестнадцать вместо восьми. Дополнительные регистры получили названия r8, r9, …, r15. Соответствующие им регистры, которые представляют младшие 32, 16 и 8 бит, получили название r8d, r8w, r8b, и по аналогии для регистров r9-r15. Кроме того, появились регистры, представляющие собой младшие 8 бит регистров rsi, rdi, rbp и rsp — sil, dil, bpl и spl соответственно.

Про адресацию

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

mov ( % rsp ) , % rax

Эта запись означает «прочитай 8 байт по адресу, записанному в регистре rsp, и сохрани их в регистр rax». При запуске программы rsp указывает на вершину стека, где хранится число аргументов, переданных программе (argc), указатели на эти аргументы, а также переменные окружения и кое-какая другая информация. Таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций) в rax будет записано количество аргументов, с которыми была запущена программа.

В одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:

mov 8 ( % rsp ) , % rax

Эта запись означает «возьми rsp, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в rax». Таким образом, в rax будет записан адрес строки, представляющей собой первый аргумент программы, то есть, имя исполняемого файла.

При работе с массивами бывает удобно обращаться к элементу с определенным индексом. Соответствующий синтаксис:

# инструкция xchg меняет значения местами
xchg 16 ( % rsp ,% rcx , 8 ) , % rax

Читается так: «посчитай rcx*8 + rsp + 16, и поменяй местами 8 байт (размер регистра) по получившемуся адресу и значение регистра rax». Другими словами, rsp и 16 все так же играют роль смещения, rcx играет роль индекса в массиве, а 8 — это размер элемента массива. При использовании данного синтаксиса допустимыми размерами элемента являются только 1, 2, 4 и 8. Если требуется какой-то другой размер, можно использовать инструкции умножения, бинарного сдвига и прочие, которые мы рассмотрим далее.

Наконец, следующий код тоже валиден:

.data
msg :
. ascii «Hello, world!\n»
. text

. globl _start
_start :
# обнуление rcx
xor % rcx , % rcx
mov msg ( ,% rcx , 8 ) , % al
mov msg , % ah

В смысле, что можно не указывать регистр со смещением или вообще какие-либо регистры. В результате выполнения этого кода в регистры al и ah будет записан ASCII-код буквы H, или 0 x 48.

В этом контексте хотелось бы упомянуть еще одну полезную ассемблерную инструкцию:

# rax := rcx*8 + rax + 123
lea 123 ( % rax ,% rcx , 8 ) , % rax

Инструкция lea очень удобна, так как позволяет сразу выполнить умножение и несколько сложений.

Fun fact! На x64 в байткоде инструкций никогда не используются 64-х битовые смещения. В отличие от x86, инструкции часто оперируют не абсолютными адресами, а адресами относительно адреса самой инструкции, что позволяет обращаться к ближайшим +/- 2 Гб оперативной памяти. Соответствующий синтаксис:

movb msg ( % rip ) , % al

Сравним длины опкодов «обычного» и «относительного» mov ( objdump -d ):

4000b0: 8a 0c 25 e8 00 60 00 mov 0x6000e8,%cl
4000b7: 8a 05 2b 00 20 00 mov 0x20002b(%rip),%al # 0x6000e8

Как видите, «относительный» mov еще и на один байт короче! Что это за регистр такой rip мы узнаем чуть ниже.

Для записи же полного 64-х битового значения в регистр предусмотрена специальная инструкция:

movabs $ 0x1122334455667788 , % rax

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

Арифметические операции

Рассмотрим основные арифметические операции:

# инциализируем значения регистров
mov $ 123 , % rax
mov $ 456 , % rcx

# инкремент: rax = rax + 1 = 124
inc % rax

# декремент: rax = rax — 1 = 123
dec % rax

# сложение: rax = rax + rcx = 579
add % rcx , % rax

# вычитание: rax = rax — rcx = 123
sub % rcx , % rax

# изменение знака: rcx = — rcx = -456
neg % rcx

Здесь и далее операндами могут быть не только регистры, но и участки памяти или константы. Но оба операнда не могут быть участками памяти. Это правило применимо ко всем инструкциям ассемблера x86/x64, по крайней мере, из рассмотренных в данной статье.

mov $ 100 , % al
mov $ 3 , % cl
mul % cl

В данном примере инструкция mul умножает al на cl, и сохраняет результат умножения в пару регистров al и ah. Таким образом, ax примет значение 0 x 12C или 300 в десятичной нотации. В худшем случае для сохранения результата перемножения двух N-байтовых значений может потребоваться до 2*N байт. В зависимости от размера операнда результат сохраняется в al:ah, ax:dx, eax:edx или rax:rdx. Притом в качестве множителей всегда используется первый из этих регистров и переданный инструкции аргумент.

Знаковое умножение производится точно так же при помощи инструкции imul. Кроме того, существуют варианты imul с двумя и тремя аргументами:

mov $ 123 , % rax
mov $ 456 , % rcx

# rax = rax * rcx = 56088
imul % rcx , % rax

# rcx = rax * 10 = 560880
imul $ 10 , % rax , % rcx

Инструкции div и idiv производят действия, обратные mul и imul. Например:

mov $ 0 , % rdx
mov $ 456 , % rax
mov $ 123 , % rcx

# rax = rdx:rax / rcx = 3
# rdx = rdx:rax % rcx = 87
div % rcx

Как видите, был получен результат целочисленного деления, а также остаток от деления.

Это далеко не все арифметические инструкции. Например, есть еще adc (сложение с учетом флага переноса), sbb (вычитание с учетом займа), а также соответствующие им инструкции, выставляющие и очищающие соответствующие флаги (ctc, clc), и многие другие. Но они распространены намного меньше, и потому в рамках данной статьи не рассматриваются.

Логические и битовые операции

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

Так, например, выглядит вычисление простейшего логического выражения:

mov $ 0 , % rax # a = false
mov $ 1 , % rbx # b = true
mov $ 0 , % rcx # c = false

# rdx := a || !(b && c)
mov % rcx , % rdx # rdx = c
and % rbx , % rdx # rdx &= b
not % rdx # rdx = ~ rdx
or % rax , % rdx # rdx |= a
and $ 1 , % rdx # rdx &= 1

Заметьте, что здесь мы использовали по одному младшему биту в каждом из 64-х битовых регистров. Таким образом, в старших битах образуется мусор, который мы обнуляем последней командой.

Еще одна полезная инструкция — это xor (исключающее или). В логических выражениях xor используется нечасто, однако с его помощью часто происходит обнуление регистров. Если посмотреть на опкоды инструкций, то становится понятно, почему:

4000b3: 48 31 db xor %rbx,%rbx
4000b6: 48 ff c3 inc %rbx
4000b9: 48 c7 c3 01 00 00 00 mov $0x1,%rbx

Как видите, инструкции xor и inc кодируются всего лишь тремя байтами каждая, в то время, как делающая то же самое инструкция mov занимает целых семь байт. Каждый отдельный случай, конечно, лучше бенчмаркать отдельно, но общее эвристическое правило такое — чем короче код, тем больше его помещается в кэши процессора, тем быстрее он работает.

В данном контексте также следует вспомнить инструкции побитового сдвига, тестирования битов (bit test) и сканирования битов (bit scan):

# положим что-нибудь в регистр
movabs $ 0xc0de1c0ffee2beef , % rax

# сдвиг влево на 3 бита
# rax = 0x0de1c0ffee2beef0
shl $ 4 , % rax

# сдвиг вправо на 7 бит
# rax = 0x001bc381ffdc57dd
shr $ 7 , % rax

# циклический сдвиг вправо на 5 бит
# rax = 0xe800de1c0ffee2be
ror $ 5 , % rax

# циклический сдвиг влево на 5 бит
# rax = 0x001bc381ffdc57dd
rol $ 5 , % rax

# положить в CF (см далее) значение 13-го бита
# CF = !!(0x1bc381ffdc57dd & (1 bt $ 13 , % rax

# то же самое + установить бит (bit test and set)
# rax = 0x001bc381ffdc77dd, CF = 0
bts $ 13 , % rax

# то же самое + сбросить бит (bit test and reset)
# rax = 0x001bc381ffdc57dd, CF = 1
btr $ 13 , % rax

# то же самое + инвертировать бит (bit test and complement)
# rax = 0x001bc381ffdc77dd, CF = 0
btc $ 13 , % rax

# найти самый младший ненулевой байт (bit scan forward)
# rcx = 0, ZF = 0
bsf % rax , % rcx

# найти самый старший ненулевой байт (bit scan reverse)
# rdx = 52, ZF = 0
bsr % rax , % rdx

# если все биты нулевые, ZF = 1, значение rdx неопределено
xor % rax , % rax
bsf % rax , % rdx

Еще есть битовые сдвиги со знаком (sal, sar), циклические сдвиги с флагом переноса (rcl, rcr), а также сдвиги двойной точности (shld, shrd). Но используются они не так уж часто, да и утомишься перечислять вообще все инструкции. Поэтому их изучение я оставляю вам в качестве домашнего задания.

Условные выражения и циклы

Выше несколько раз упоминались какие-то там флаги, например, флаг переноса. Под флагами понимаются биты специального регистра eflags / rflags (название на x86 и x64 соответственно). Напрямую обращаться к этому регистру при помощи инструкций mov, add и подобных нельзя, но он изменяется и используется различными инструкциями косвенно. Например, уже упомянутый флаг переноса (carry flag, CF) хранится в нулевом бите eflags / rflags и используется, например, в той же инструкции bt. Еще из часто используемых флагов можно назвать zero flag (ZF, 6-ой бит), sign flag (SF, 7-ой бит), direction flag (DF, 10-ый бит) и overflow flag (OF, 11-ый бит).

Еще из таких неявных регистров следует назвать eip / rip, хранящий адрес текущей инструкции. К нему также нельзя обращаться напрямую, но он виден в GDB вместе с eflags / rflags, если сказать info registers , и косвенно изменяется всеми инструкциям. Большинство инструкций просто увеличивают eip / rip на длину этой инструкции, но есть и исключения из этого правила. Например, инструкция jmp просто осуществляет переход по заданному адресу:

# обнуляем rax
xor % rax , % rax
jmp next
# эта инструкция будет пропущена
inc % rax
next :
inc % rax

В результате значение rax будет равно единице, так как первая инструкция inс будет пропущена. Заметьте, что адрес перехода также может быть записан в регистре:

xor % rax , % rax
mov $next , % rcx
jmp *% rcx
inc % rax
next :
inc % rax

Впрочем, на практике такого кода лучше избегать, так как он ломает предсказание переходов и потому менее эффективен.

Примечание: GAS позволяет давать меткам цифирные имена типа 1: , 2: , и так далее, и переходить к ближайшей предыдущей или следующей метке с заданным номером инструкциями вроде jmp 1b и jmp 1f . Это довольно удобно, так как иногда бывает трудно придумать меткам осмысленные имена. Подробности можно найти здесь.

Условные переходы обычно осуществляются при помощи инструкции cmp, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je, jg и подобных:

je 1f # перейти, если равны (equal)
jl 1f # перейти, если знаково меньше (less)
jb 1f # перейти, если беззнаково меньше (below)
jg 1f # перейти, если знаково больше (greater)
ja 1f # перейти, если беззнаково больше (above)

Существует также инструкции jne (перейти, если не равны), jle (перейти, если знаково меньше или равны), jna (перейти, если беззнаково не больше) и подобные. Принцип их именования, надеюсь, очевиден. Вместо je / jne часто пишут jz / jnz, так как инструкции je / jne просто проверяют значение ZF. Также есть инструкции, проверяющие другие флаги — js, jo и jp, но на практике они используются редко. Все эти инструкции вместе взятые обычно называют jcc. То есть, вместо конкретных условий пишутся две буквы «c», от «condition». Здесь можно найти хорошую сводную таблицу по всем инструкциям jcc и тому, какие флаги они проверяют.

Помимо cmp также часто используют инструкцию test:

test % rax , % rax
jz 1f # перейти, если rax == 0
js 2f # перейти, если rax < 0
1 :
# какой-то код
2 :
# какой-то еще код

Fun fact! Интересно, что cmp и test в душе являются теми же sub и and, только не изменяют своих операндов. Это знание можно использовать для одновременного выполнения sub или and и условного перехода, без дополнительных инструкций cmp или test.

Еще из инструкций, связанных с условными переходами, можно отметить следующие.

jrcxz 1f
# какой-то код
1 :

Инструкция jrcxz осуществляет переход только в том случае, если значение регистра rcx равно нулю.

cmovge % rcx , % rax

Инструкции семейства cmovcc (conditional move) работают как mov, но только при выполнении заданного условия, по аналогии с jcc.

Инструкции setcc присваивают однобайтовому регистру или байту в памяти значение 1, если заданное условие выполняется, и 0 иначе.

cmpxchg % rcx , ( % rdx )

Сравнить rax с заданным куском памяти. Если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере rcx. Иначе очистить ZF и загрузить значение из памяти в rax. Также оба операнда могут быть регистрами.

cmpxchg8b ( % rsi )
cmpxchg16b ( % rsi )

Инструкция cmpxchg8b главным образом нужна в x86. Она работает аналогично cmpxchg, только производит compare and swap сразу 8-и байт. Регистры edx:eax используются для сравнения, а регистры ecx:ebx хранят то, что мы хотим записать. Инструкция cmpxchg16b по тому же принципу производит compare and swap сразу 16-и байт на x64.

Важно! Примите во внимание, что без префикса lock все эти compare and swap инструкции не атомарны.

mov $ 10 , % rcx
1 :
# какой-то код
loop 1b
# loopz 1b
# loopnz 1b

Инструкция loop уменьшает значение регистра rcx на единицу, и если после этого rcx != 0 , осуществляет переход на заданную метку. Инструкции loopz и loopnz работают аналогично, только условия более сложные — (rcx != 0) && (ZF == 1) и (rcx != 0) && (ZF == 0) соответственно.

Не нужно быть семи пядей во лбу, чтобы изобразить при помощи этих инструкций конструкцию if-then-else или циклы for / while, поэтому двигаемся дальше.

«Строковые» операции

Рассмотрим следующий кусок кода:

mov $str1 , % rsi
mov $str2 , % edi
cld
cmpsb

В регистры rsi и rdi кладутся адреса двух строк. Командой cld очищается флаг направления (DF). Инструкция, выполняющая обратное действие, называется std. Затем в дело вступает инструкция cmpsb. Она сравнивает байты (%rsi) и (%rdi) и выставляет флаги в соответствии с результатом сравнения. Затем, если DF = 0, rsi и rdi увеличиваются на единицу (количество байт в том, что мы сравнивали), иначе — уменьшаются. Аналогичные инструкции cmpsw, cmpsl и cmpsq сравнивают слова, длинные слова и четверные слова соответственно.

Инструкции cmps интересны тем, что могут использоваться с префиксом rep, repe (repz) и repne (repnz). Например:

mov $str1 , % rsi
mov $str2 , % edi
mov $len , % rcx
cld
repe cmpsb
jne not_equal

Префикс rep повторяет инструкцию заданное в регистре rcx количество раз. Префиксы repz и repnz делают то же самое, но только после каждого выполнения инструкции дополнительно проверяется ZF. Цикл прерывается, если ZF = 0 в случае c repz и если ZF = 1 в случае с repnz. Таким образом, приведенный выше код проверяет равенство двух буферов одинакового размера.

Аналогичные инструкции movs перекладывает данные из буфера, адрес которого указан в rsi, в буфер, адрес которого указан в rdi (легко запомнить — rsi значит source, rdi значит destination). Инструкции stos заполняет буфер по адресу из регистра rdi байтами из регистра rax (или eax, или ax, или al, в зависимости от конкретной инструкции). Инструкции lods делают обратное действие — копируют байты по указанному в rsi адресу в регистр rax. Наконец, инструкции scas ищут байты из регистра rax (или соответствующих регистров меньшего размера) в буфере, адрес которого указан в rdi. Как и cmps, все эти инструкции работают с префиксами rep, repz и repnz.

На базе этих инструкций легко реализуются процедуры memcmp, memcpy, strcmp и подобные. Интересно, что, например, для обнуления памяти инженеры Intel рекомендуют использовать на современных процессорах rep stosb , то есть, обнулять побайтово, а не, скажем, четверными словами.

Работа со стеком и процедуры

Со стеком все очень просто. Инструкция push кладет свой аргумент на стек, а инструкция pop извлекает значение со стека. Например, если временно забыть про инструкцию xchg, то поменять местами значение двух регистров можно так:

push % rax
mov % rcx , % rax
pop % rcx

Существуют инструкции, помещающие на стек и извлекающие с него регистр rflags / eflags:

pushf
# делаем что-то, что меняет флаги
popf
# флаги восстановлены, самое время сделать jcc

А так, к примеру, можно получить значение флага CF:

pushf
pop % rax
and $ 1 , % rax

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

Процедуры, как правило, «создаются» при помощи инструкций call и ret. Инструкция call кладет на стек адрес следующей инструкции и передает управление по указанному в аргументе адресу. Инструкция ret читает со стека адрес возврата и передает по нему управление. Например:

someproc :
# типичный пролог процедуры
# для примера выделяем 0x10 байт на стеке под локальные переменные
# rbp — указатель на фрейм стека
push % rbp
mov % rsp , % rbp
sub $ 0x10 , % rsp

# тут типа какие-то вычисления .
mov $ 1 , % rax

# типичный эпилог процедуры
add $ 0x10 , % rsp
pop % rbp

# выход из процедуры
ret

_start :
# как и в случае с jmp, адрес перехода может быть в регистре
call someproc
test % rax , % rax
jnz error

Примечание: Аналогичный пролог и эпилог можно написать при помощи инструкций enter $ 0x10 , $ 0 и leave . Но в наше время эти инструкции используются редко, так как они выполняются медленнее из-за дополнительной поддержки вложенных процедур.

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

Для примера рассмотрим ассемблерный код, сгенерированный CLang 3.8 для простой программки на языке C под x64. Так выглядит одна из процедур:

unsigned int
hash ( const unsigned char * data , const size_t data_len ) {
unsigned int hash = 0x4841434B ;
for ( int i = 0 ; i < data_len ; i ++ ) {
hash = ( ( hash }
return hash ;
}

Дизассемблерный листинг (при компиляции с -O0 , комментарии мои):

# типичный пролог процедуры
# регистр rsp не изменяется, так как процедура не вызывает никаких
# других процедур
400950: 55 push %rbp
400951: 48 89 e5 mov %rsp,%rbp

# инициализация локальных переменных:
# -0x08(%rbp) — const unsigned char *data (8 байт)
# -0x10(%rbp) — const size_t data_len (8 байт)
# -0x14(%rbp) — unsigned int hash (4 байта)
# -0x18(%rbp) — int i (4 байта)
400954: 48 89 7d f8 mov %rdi,-0x8(%rbp)
400958: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40095c: c7 45 ec 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
400963: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)

# rax := i. если достигли data_len, выходим из цикла
40096a: 48 63 45 e8 movslq -0x18(%rbp),%rax
40096e: 48 3b 45 f0 cmp -0x10(%rbp),%rax
400972: 0f 83 28 00 00 00 jae 4009a0

# eax := (hash 400978: 8b 45 ec mov -0x14(%rbp),%eax
40097b: c1 e0 05 shl $0x5,%eax
40097e: 03 45 ec add -0x14(%rbp),%eax

# eax += data[i]
400981: 48 63 4d e8 movslq -0x18(%rbp),%rcx
400985: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400989: 0f b6 34 0a movzbl (%rdx,%rcx,1),%esi
40098d: 01 f0 add %esi,%eax

# hash := eax
40098f: 89 45 ec mov %eax,-0x14(%rbp)

# i++ и перейти к началу цикла
400992: 8b 45 e8 mov -0x18(%rbp),%eax
400995: 83 c0 01 add $0x1,%eax
400998: 89 45 e8 mov %eax,-0x18(%rbp)
40099b: e9 ca ff ff ff jmpq 40096a

# возвращаемое значение (hash) кладется в регистр eax
4009a0: 8b 45 ec mov -0x14(%rbp),%eax

# типичный эпилог
4009a3: 5d pop %rbp
4009a4: c3 retq

Здесь мы встретили две новые инструкции — movs и movz. Они работают точно так же, как mov, только расширяют один операнд до размера второго, знаково и беззнаково соответственно. Например, инструкция movzbl (%rdx,%rcx,1),%esi читайт байт (b) по адресу (%rdx,%rcx,1) , расширяет его в длинное слово (l) путем добавления в начало нулей (z) и кладет результат в регистр esi.

Как видите, два аргумента были переданы процедуре через регистры rdi и rsi. По всей видимости, используется конвенция под названием System V AMD64 ABI. Утверждается, что это стандарт де-факто под x64 на *nix системах. Я не вижу смысла пересказывать описание этой конвенции здесь, заинтересованные читатели могут ознакомиться с полным описанием по приведенной ссылке.

Заключение

Само собой разумеется, в рамках одной статьи, описать весь ассемблер x86/x64 не представляется возможным (более того, я не уверен, что сам знаю его прямо таки весь). Как минимум, за кадром остались такие темы, как операции над числами с плавающей точкой, MMX-, SSE- и AVX-инструкции, а также всякие экзотические инструкции вроде lidt, lgdt, bswap, rdtsc, cpuid, movbe, xlatb, или prefetch. Я постараюсь осветить их в следующих статьях, но ничего не обещаю. Следует также отметить, что в выводе objdump -d для большинства реальных программ вы очень редко увидите что-то помимо описанного выше.

Еще интересный топик, оставшийся за кадром — это атомарные операции, барьеры памяти, спинлоки и вот это все. Например, compare and swap часто реализуется просто как инструкция cmpxchg с префиксом lock. По аналогии реализуется атомарный инкремент, декремент, и прочее. Увы, все это тянет на тему для отдельной статьи.

В качестве источников дополнительной информации можно рекомендовать книгу Modern X86 Assembly Language Programming, и, конечно же, мануалы от Intel. Также довольно неплоха книга x86 Assembly на wikibooks.org.

Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:

  • http://ref.x86asm.net/;
  • http://www.felixcloutier.com/x86/;
  • http://x86.renejeschke.de/;
  • https://en.wikipedia.org/wiki/X86_instruction_listings;

Дополнение: В продолжение темы вас может заинтересовать пост Учебный микропроцессорный комплект УМК-80. В нем рассказывается об ассемблере 8080, 8-и битном предке ассемблера x86/x64.

Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.

Развитие процессоров Intel: 1971-1993

Чем 386-й отличается от 486-го? А Pentium от Pentium Pro? Было время, когда я мог робко ответить: «Скоростью. Чем же еще?». Вспоминая это, не могу удержаться от улыбки — мои заблуждения и «полузнания» частенько бывали смешными. Например, году в 94-м узнав что «бэкашка» — 16-битный компьютер, я решил что, приходя к дяде, я играю на 486-битном (!) компьютере. И не удивительно — тогда в нашей школе словечки «триставосемьдесятшестой» и «четыреставосемьдесятшестой» были у всех на слуху и многие дети (я!) скороговоркой выпаливали их даже не понимая толком, что все это значит. Как бы то ни было, но мой период ошибок и «полузнаний» благодаря интересу к компьютерам постепенно прошел. Думаю, что я трансформировался из любознательного чайника в слегка утомленного профи. 🙂

реклама

С самого начала я завел общую тетрадку, в которой конспектировал все то, что узнавал интересного о процессорах, операционных системах, средствах мультимедиа. Читал все подряд и без разбору: от руководства по БЕЙСИКу к моей любимой «бэкашке» до «PC Magazine» с «Компьютеррой». Именно тогда я узнал, как расшифровывается EDO и PCI, как перезагрузить компьютер одной командой MS-DOS и многое другое. Впоследствии моими источниками стали Фидо и Интернет, а журналы приняли слегка «повторительный» оттенок. Сейчас во мне появилось желание консолидировать все то, что я разузнал о процессорах Intel, и выделить самое главное в рамках этой статьи.

НАЧАЛО – ВСЕГО ЛИШЬ 4 БИТА

Так с чего же началась история процессоров Intel? Познакомьтесь, пожалуйста, с ее первенцем — микропроцессором 4004! Он был представлен публике 15 ноября 1971 года и сразу же вызвал интерес у компьютерного мира. В то время реализация всех функций большой ЭВМ на одном маленьком чипе казалась просто чудом. Правда, его возможности были скромными даже для тех лет. Он мог обрабатывать данные порциями по 4 бита, длина машинной команды составляла 8 бит, что позволило иметь 46 инструкций. Память была раздельной для кодов и данных, менее 1 Кб для данных и 4 Кб для команд. Внутри у него было шестнадцать 4-битных регистров и 4-х уровневый стек.

Знакомьтесь, Intel 4004.

Первый микропроцессор в мире!

Первый микропроцессор имел 4-битную архитектуру и состоял всего-навсего из 2300 транзисторов (в современных «пентиумах» их десятки миллионов). При этом стандартно он работал на частоте 108 КГц (отдельные экземпляры разгонялись до 740 КГц) — намного меньше мегагерца! Тогда голубой мечтой инженеров Intel было создание мегагерцового варианта, чтобы догнать по скорости IBM 1620 — дорогой, но весьма быстрый по тем временам «шкаф» эпохи 60-х. Техпроцесс по сегодняшним меркам был ужасно «толстым» — целых 10 мкм. Несмотря на то что в компьютерах этот процессор не нашел применения, он все же использовался в калькуляторах Busicom и в различных системах управления (например, уличными светофорами). Через год (1972) был выпущен 4040. Он представлял собой улучшенную версию 4004 — главным нововведением была поддержка прерываний. Кроме того, добавилось 14 новых инструкций и глубина стека была увеличена до 8 уровней, а память команд до 8 Кб.

8 БИТ – ПИОНЕРЫ 70-Х

8008 был разработан в апреле 1972 года и стал первым 8-битным микропроцессором. Он был очень похож на 4040: появившиеся 8-битные регистры и увеличенная до 16 Кб память команд — вот, пожалуй, единственные отличия. Стек по-прежнему находился внутри чипа и был ограничен 8 уровнями вложенности. Он все еще позиционировался как процессор для продвинутых калькуляторов и терминалов ввода-вывода. Однако спустя полтора года Intel создала гораздо более совершенную версию — 8080. Основные идеи архитектуры были взяты с его предшественника, 8008, — но внутренняя организация была улучшена настолько, что именно он стал стандартом де-факто для микропроцессоров того времени, а 8008 был вскоре забыт. Новый процессор имел очень развитую систему команд (78 базовых + более 200 их вариаций). Шина данных была 8-битной, а 16-разрядная адресная шина позволила ему прямо адресовать 64 Кб единой памяти, которая перестала физически разделяться на память команд и память данных. С чипом 8080 связано появление стека внешней памяти, то есть стек стал располагаться не внутри процессора, а в оперативной памяти и мог быть довольно большим. Это позволило использовать в программах алгоритмы рекурсии. «Восьмидесятка» обладала семью 8-битными регистрами (именовавшимися A — E, H и L, — причем пары BC, DE и HL могли быть скомбинированы в 16-битные регистры). Кроме того, был улучшен техпроцесс, он составил 6 мкм. Количество транзисторов: 6000. Тактовая частота достигла 2 МГц.

Помимо всего прочего 8080 послужил основой первому персональному компьютеру Альтаир-8800. Выпущенный в 1975 году фирмой MITS «Альтаир» стоил $439 ($621 в сборке). Клавиатура и монитор в стандартный комплект не входили, оперативная память составляла всего 256 байт (да-да, именно байт, а не килобайт), правда позже стали продаваться блоки расширенной памяти (от 1 до 4 Кб). Тогда же фирмой Digital Research была написана операционная система CP/M (Control Program for Microcomputers), ставшей на некоторое время стандартом, и прообразом MS-DOS. Между прочим, программисты «Альтаира» умудрялись писать даже игры, а Билл Гейтс и Пол Аллен — написали интерпретатор Бейсика. Стоит упомянуть о процессоре 8085, представленном в марте 1976 года, в котором Intel добавила две новые команды для управления прерываниями и усовершенствовала конструкцию корпуса. В этом процессоре появился сброс в начальное состояние (RESET) и использовался только один источник питания +5В (микропроцессору 8080 требовалось три источника: +12В, +5В и -5В). Одно из применений, которое нашел себе 8085 — электронные весы Toledo. Однако к середине 70-х рынок 8-битных чипов был уже переполнен. По этой причине Intel оставила попытки закрепиться на нем и сделала качественный шаг в сторону 16-битных процессоров.

реклама

ДЕБЮТ “ВОСЕМЬДЕСЯТ ШЕСТЫХ”

8086 (86-й) можно назвать прадедушкой всех современных «пентиумов». 🙂 Первый 16-битный процессор, результат усиленных разработок 1976 года. Он содержал рекордное по тому времени количество транзисторов — 29 тысяч! Именно от него ведет свое начало известная на сегодня архитектура x86. Размер его регистров по сравнению с 8080 был увеличен вдвое, что в свою очередь увеличило производительность в 10 раз. Ведь раньше чтобы сложить два больших числа, не умещавшихся в 8-битном регистре, приходилось писать специальную программу на Ассемблере, а теперь это стало доступно на аппаратном уровне. Количество базовых команд процессора 8086 возросло до 92. Кроме того, размер информационной шины был увеличен до 16 разрядов, что дало возможность вдвое увеличить скорость передачи данных на процессор и с него. Размер его адресной шины тоже был существенно увеличен — до 20 бит. Это позволило 86-му иметь 1 Мб (2 20 байт) оперативной памяти! Ближайший его конкурент, популярный в то время процессор Z80 (1976) фирмы Zilog Corporation, мог адресовать только 64 Кб. Память 8086 была также доработана: весь мегабайт оперативной памяти не представлялся единым полем, а был разделен на 16 сегментов величиной по 64 Кб. Таким образом, память 8086 можно было представить как объединенную вместе память нескольких 8080. При этом впервые в истории микропроцессоров программа перестала работать непосредственно с одним физическим адресом ячейки памяти (как это было, например, в Z80). Вместо этого, для получения физического адреса (20 бит) процессор складывал номер сегмента (16 бит), умноженный на 16, со смещением (16 бит). В действительности 8086 вместо умножения на 16 использовал содержимое регистра так, как если бы оно имело четыре дополнительных нулевых бита (см. рисунок). Такой странный на первый взгляд метод адресации обусловлен тем, что команды и данные процессора 8086 должны были располагаться в разных частях памяти, т. е. в разных сегментах.

У 86-го процессора было всего 14 регистров. Из них 4 регистра общего назначения (AX, BX, CX, DX), 2 индексных (SI, DI), 2 указательных (BP, SP), 4 сегментных (CS, SS, DS, ES), регистр следующей инструкции IP и регистр флагов (состояний процессора) FLAGS. Все они были 16-битными, причем регистры AX, BX, CX, DX состояли из пары 8-битных (AL — AH, . DL — DH соответственно). Несмотря на то, что они являлись частями целых 16-битных регистров, их можно было использовать как отдельные 8-битные. Благодаря этому, программы, разработанные под 8080, можно было с минимальной переделкой использовать и на 8086 процессоре.

Через год после презентации 8086, Intel объявила о разработке его более дешевого аналога — 8088. Он являлся близнецом 8086: 16-битные регистры, 20 адресных линий, тот же набор команд — все то же, за исключением одного, — шина данных была уменьшена до 8 бит. Заполнение 16-битных регистров извне теперь проходило в два раза медленнее. Как следствие — меньшая производительность. Его можно назвать своего рода «86SX» (тогда, правда, Intel не обозначала так свои процессоры). Однако стоимость самого чипа заметно не уменьшилась. Так для чего же Intel понадобилось делать этот шаг? Для того, чтобы угодить фирме IBM, решившей начать производство компьютеров на базе этого процессора. Желание IBM вполне объяснимо. Восьмибитная шина данных позволяла использовать имеющиеся на рынке 8-битные микросхемы памяти и сэкономить на развертывании производства 16-битных. При этом внутренняя структура процессора оставалась по-прежнему 16-битной, что давало важные преимущества по сравнению с другими микропроцессорами и задатком на будущее. На его основе в августе 1981 года фирма IBM начала массовое производство компьютеров IBM PC. Тактовая частота первых моделей была небольшой — 4.77 МГц. Однако позже были выпущены модели Turbo-XT с частотами 8, 10 и 12 МГц и встроенным жестким диском на 20 мегабайт.

В предыдущей части речь шла о ранних процессорах Intel: от 4-битного 4004 до основателя нынешней династии — процессора 8086. Они были важнейшими творениями в истории корпорации, и не будь их — думаю, не было бы сейчас «пентиумов» и «целеронов». В этой речь пойдет о процессорах 186 и 286 — последних полностью 16-битных процессорах Intel. Если бы каждый процессор содержал в себе столько новинок, сколько их пришлось на долю 286-го, то наверное сегодня шлем виртуальной реальности был бы такой же привычной «железкой» как мышь или клавиатура. 🙂 Но не буду забегать вперед, вот что последовало за 86-м чипом.

В СЕМЬЕ НЕ БЕЗ. СТО ВОСЕМЬДЕСЯТ ШЕСТОГО

Мало кто знает, что 186-й процессор вообще существовал, и еще меньше знают о нем что-нибудь подробное. Тем не менее, он был. Созданный в 1981 году и презентованный в первой половине 1982 года, он стал базовым для реализации турборежима. Почти сразу же был создан процессор 188 — преемник 8088. Эти процессоры сохранили базовую архитектуру процессоров 8086/8088, но содержали на кристалле контроллер прямого доступа к памяти (DMA), трехканальный таймер, генератор синхронизации и контроллер прерываний (а зря — «интегралки» народ не любит и по сей день). Кроме того, было добавлено около десятка новых команд и стартовая частота поднялась до 8 МГц. Затем вышли модели с частотами 10, 12.5, 16 и 20 МГц. Процессоры C186/C188 имели средства управления энергопотреблением, были и редкие модификации со встроенными последовательными портами. Кроме того, добавилась парочка новых команд, ускоряющих вызов функций — pusha (Push All) и popa (Pop All), — запись и чтение всех регистров процессора в стек, раньше приходилось возиться с каждым регистром отдельно.

У меня сложилось впечатление, что эти процессоры были созданы не столько для компьютеров, сколько для специальных устройств управления и микроконтроллеров. Но компьютеры на их основе все же были — у меня дома лежит журнал, привезенный в 1983 году мамой из Финляндии, с рекламой компьютеров Nokia на базе 186-го. 🙂 Правда широкого распространения эти компьютеры не получили. Видимо причины этого кроются в отсутствии качественных нововведений, относительной дороговизне чипа и в замаячившем на горизонте следующем процессоре Intel. Кроме того, встроенная периферия 186-го имела программный интерфейс несовместимый с IBM PC, т. е. для апгрейда писишки он не годился. Одним словом 186-й оказался тупиковой ветвью.

286-Й: ЕЩЕ НЕ СЕРВЕР, НО УЖЕ НЕ «БЭКАШКА»

В 1982 году фирма Intel сделала крупный шаг в разработке новых идей: ее следующий 16-битный чип стал первой попыткой создать процессор, который мог бы аппаратно реализовывать многозадачность. Для этого был придуман защищенный режим. Ведь основная проблема многозадачности была в том, что любые программы, работавшие под управлением прежних процессоров, имели полную свободу действий — могли по любому адресу памяти записать все что угодно. Операционная система и другие приложения при этом были не защищены: в любой момент глючная программа могла затереть эти места в памяти и компьютер, скорее всего бы просто повис, даже не выдав знакомой таблички о выполнении «недопустимой операции». Были попытки создать операционную систему, которая сама бы контролировала все действия программ. Но для этого пришлось отказаться от компиляции приложений в готовые машинные коды — они стали интерпретируемыми, а производительность упала раз в двадцать. Стало ясно, что без аппаратной акселерации контроля, т. е. без защищенного режима процессора не обойтись.

Суть работы защищенного режима была проста. Все свои команды процессор выполнял точно так же как и в реальном режиме, но программистам пришлось использовать понятие «логического адреса». Логический адрес состоял из 32 бит: селектора (16 бит) и смещения (16 бит). При этом в сегментных регистрах (CS, DS, SS, ES) теперь хранился не сегмент, а селектор. Селектор — это индекс в таблице дескрипторов (рис. 2). Запись в таблице дескрипторов содержала всю необходимую информацию о некотором блоке памяти: его базовый адрес, размер всего блока, его тип (код или данные) и сведения о приоритете программы-владельца. Таким образом, каждый дескриптор полностью описывал один сегмент программы. Размер этого дескриптора был одинаков как для 286-х, так и для 386-х машин — 64 бита или 8 байт, но у 286-го старшие 16 бит не использовались.

Существовала одна глобальная и несколько локальных таблиц. Глобальная присутствовала всегда и хранила информацию о сегментах операционной системы. Локальные таблицы были для всех остальных программ. Управление памятью в защищенном режиме всегда было связано с конкретной операционной системой и ее версией. В OS/2 2.0 каждой программе были доступны глобальная и локальная (своя) таблицы дескрипторов. Всем приложениям в Windows 3.0 давалась одна общая локальная таблица. Система защиты 286-го процессора позволила разбить всю память и программы на 4 типа (кольца защиты — рис. 3). При этом программа работала только со своими блоками. Если программа обращалась к чужому сегменту памяти, то происходило прерывание с выдачей сообщения об ошибке. Ошибка также возникала, если программа пыталась считать или записать в свой собственный сегмент кода.

реклама

Теперь о параметрах 286-го чипа. 134 000 транзисторов, техпроцесс 1.5 мкм, 68 контактных ножек, 16-битная шина данных, 24-битная адресная шина (до 16 Мб физической памяти), 19 «видимых» регистров (к 14 регистрам 86-го процессора добавилось еще 5, необходимых для реализации защищенного режима) и 6 «невидимых» (у программиста не было к ним никакого доступа, а нужны они были для эффективной работы самого процессора). Максимальный размер виртуальной памяти (файл подкачки) составил 1008 Мб (хотя на практике «двушка» никогда не имела таких больших дисков). Стартовая частота — 6 МГц, затем были созданы модели с частотами от 10 до 25 МГц. Помимо количественного повышения быстродействия было и качественное: некоторые операции 286-й научился выполнять за меньшое число тактов. Процессор 286 с тактовой частотой 12.5 МГц работал примерно в 6 раз быстрее, чем 8086 с частотой 4.77 МГц. Команда push научилась сохранять в стеке константы. Шины адреса и данных теперь стали раздельными, а не совмещенными на одном множестве ножек. Как и в 8086-м новый процессор имел 6-байтную очередь команд — что-то вроде прообраза будущих конвейеров. Команды в эту очередь загружались в последовательном порядке и параллельно с производимыми вычислениями.

Несмотря на то, что 286-й процессор был окончательно представлен уже в 1982 году, производство компьютеров на его основе началось только в 1984-м. Новые компьютеры стали называться IBM PC AT (Advanced Technology). Выпуск этой модели подхлестнул интерес и к прежней IBM PC, способствуя популяризации архитектуры x86 в целом. Появилось несколько журналов, посвященных исключительно писишкам, магазинов, торгующих программным обеспечением. Словосочетание «персональный компьютер» становилось обыденным и больше не резало слух. Для меня, поклонника группы «Queen», 1984-й год ассоциируется со словами «I Want To Break Free». 🙂 Что ж, пожалуй, для писишек этот год стал и впрямь «освободительным». От диктата больших машин и насмешек их приверженцев.

К сожалению, защищенный режим 286-го обладал и недостатками:

  • Несмотря на возможность адресовать 16 Мб памяти, максимальный размер сегмента остался по-прежнему равным 64 Кб, затрудняя программистам работу с большими массивами данных.
  • Режим работы с виртуальной памятью не был толком продуман — отсутствовал «прозрачный» для приложений способ перемещения данных операционной системой из памяти на жесткий диск — для реализации этого программам приходилось прибегать к разным ухищрениям вроде «запирания» и «отпирания» указателей на блок памяти.
  • В защищенном режиме отсутствовала совместимость с программами, написанными для реального режима MS-DOS.
  • Переход из реального режима в защищенный был односторонним, для обратного перехода требовалась перезагрузка компьютера.

Таким образом, первый многозадачный «блинчик» у Intel получился слегка «комом» и 286-е компьютеры обычно использовались только в качестве быстрых аналогов 86-го. Сумела ли Intel исправить эти ошибки и подарить миру персоналок истинную многозадачность? Да, в следующем процессоре.

17 октября 1985 года фирма Intel представила свой первый 32-битный процессор, обладавший уникальным преимуществом: в отличие от своих конкурентов, разработанных к тому времени компанией Motorola, он был аппаратно совместим с огромным парком 86-х и 286-х машин. Это был Intel 80386, сердце легендарной «трешки». Не сочтите меня стариком, но я люблю 386-е машины, люблю за все то, что они дали. Ведь именно на трешке я увидел Wolf3D и Warcraft. Познал миры легендарной Кирандии, заделался магнатом в Transport Tycoon и написал первые программки на Паскале. Даже Windows я впервые увидел на трешке (приняв ее за графический редактор со встроенным калькулятором :). 80386 — едва ли не самый революционный процессор, редко когда в чипах Intel появлялось столько новшеств сразу.

КРАТКИЙ ОБЗОР

Обладая полностью 32-битной архитектурой, 386-й процессор адресовал до 4 Гб (2 32 байт) физической оперативной памяти и до 64 Тб виртуальной. Он состоял из 275 000 транзисторов и изготавливался на основе технологии CHMOS III фирмы Intel, вобравшей в себя быстродействие технологии HMOS и низкое энергопотребление CMOS технологии. Толщина транзисторных элементов, как и у 286-го, составляла 1.5 мкм (в дальнейшем — 1 мкм), а число ножек увеличилось с 68 до 132.

Новый процессор стал мощнее своих предшественников. Умножение двух 16-битных чисел командой MUL R16 выполнялось, в зависимости от числа единичных разрядов, за 9-22 такта. Для сравнения: то же у 286-го срабатывало всегда за 21 такт, а у 86-го аж за 118-133 такта! Математический сопроцессор 80387 не остался в стороне и тоже был оптимизирован — если 287-й выполнял команду извлечение корня FSQRT за 180-186 тактов, то 387-й чип справлялся с этой задачей уже за 122-129 тактов. В среднем количество тактов, необходимых для выполнения команд, уменьшилось в полтора-два раза в сравнении с 286-м и в 3-8 раз в сравнении с оригинальным IBM PC. Просмотр всего содержимого энциклопедии Britannica с применением 386-го занимал 12,5 секунд (286-у нужно было 45). Кроме того, добавилось несколько продвинутых команд сопроцессора, например вычисление синуса, косинуса и логарифмов.

Первые процессоры стали работать с наивысшей частотой, достигнутой к тому времени для 286-го — 16 МГц, затем появилась 20 МГц модель (16 февраля 1987), а к середине 1988 года предел был отодвинут до 25 МГц (4 апреля 1988). В начале 90-х популярность приобрели 33 МГц модели (10 апреля 1989), кстати, все 386-е работали на частоте материнской платы, никаких коэффициентов умножения!

386-й под микроскопом

Набор регистров в процессоре был существенно изменен — почти все 16-битные регистры были заменены их 32-битными аналогами. Новые регистры именовались как и прежде, только с приставкой «E» (Extended) в начале слова. Так, вместо регистров общего назначения AX, BX, CX, DX появились регистры EAX, EBX, ECX, EDX, младшие части которых (левые 16 бит) использовались 86/286-инструкциями в качестве прежних регистров. Вместо 16-битных IP (указатель следующей инструкции) и FLAGS (флаги процессора) — соответственно 32-битные регистры EIP и EFLAGS. Вместо SI/DI (индексы источника/назначения) — ESI/EDI. Словом, почти все прежние регистры заменились новыми, младшая часть которых использовалась по-старому. В новом процессоре 16-битными остались только сегментные регистры (CS, SS, DS, ES) и регистр локальной таблицы дескрипторов (LDTR). Кстати, к сегментным регистрам добавилась парочка новых, поначалу редко используемых — GS и FS. Напомню, что регистр CS указывает на начало сегмента кодов (исполняемый код программы), регистр SS — на «дно» стека, а остальные 4 регистра (DS, ES, GS, FS) — на разные сегменты данных (там хранятся переменные, массивы, в общем, любые данные программы).

Между прочим Intel надолго осталась верна набору регистров 386-го, включая сопроцессор, и при создании следующих моделей (486, Pentium) ограничилась несущественным расширением (TR3-TR5 регистры, появившиеся в 486-м, управляющий регистр CR4, появившийся в Pentium). Все эти дополнительные регистры предназначались не столько для программ-приложений, сколько для операционных систем и средств отладки, т. е. выгоды от них в играх и Windows-приложениях не было никакой. Следующее увеличение числа регистров, полезных для самих приложений, произойдет, по сути дела, только в процессоре Pentium III (1999).

ГЛАВНЫЕ «ИЗЮМИНКИ»

Защищенный режим 386-го был существенно доработан. Именно тогда, 16 лет назад, Intel заложила фундамент успеха Windows 95. Модель 286-го в целом была сохранена, но добавилось три очень важных аспекта: сколь угодно большой размер сегмента, страничный режим адресации (Page Addressing) и режим виртуального 8086 (Virtual 8086 Mode). Все эти аспекты используются операционными системами Windows 95/NT и без них они работать не смогут. Кроме того, 386-й «научился» возвращаться обратно из защищенного режима в реальный без перезагрузки компьютера (в 286-м переход в защищенный режим был односторонним).

Снятие ограничения на размер сегмента (вообще-то он ограничен четырьмя гигабайтами, но по-моему для компьютерного ОЗУ это почти бесконечность) тесно связано со страничной организацией памяти. Без использования страниц ограничение на размер остается, правда уже не в 64 Кб как у 86/286, а в 1 Мб. Это объясняется тем, что для указания размера сегмента в таблице дескрипторов отведено 20 бит (у 286-го 16 бит). 2 20 дает нам 1 Мб комбинаций. Но в дескрипторе сегмента 386-й машины появился очень интересный бит — бит гранулярности. При его включении процессор начинает понимать размер сегмента не в байтах, а в страницах по 4 Кб (4096 байт). Минимальный размер сегмента в этом случае будет равен 4 Кб (1 * 4 Кб), а максимальный — 4 Гб (1 Мб * 4 Кб).

Ранее я говорил о принципах работы программ в защищенном режиме 286-го процессора. Не стоит заново все пересказывать, детали вы можете увидеть в предыдущей части, приведу только главные отличия. Вначале у трешки все почти так же — только смещение и базовый адрес в дескрипторе стали 32-битными, соответственно и результат их суммы — 32-битное число. Единственное что осталось 16-битным — селектор. Но вот дальше. Сумма базового адреса и смещения еще не физический адрес! Старшие 10 бит этой суммы, которая называется линейным адресом, задают индекс таблицы страниц в каталоге таблиц, следующие 10 бит — индекс страницы в найденной таблице страниц, оставшиеся 12 бит — смещение относительно начала этой страницы. И только когда процессор найдет в каталоге нужную таблицу, а в ней нужную страницу (потерпите, немного осталось 🙂 и прибавит к полученному адресу смещение, он получит физический адрес ячейки памяти.

Мудрено? Для чего такие сложности? Для реализации замечательной вещи: теперь процессор мог динамически менять расположение данных в памяти при постоянном логическом адресе! Программа, например WinWord, могла работать со своими документами и даже не подозревать, что секунду назад данные физически были в этих ячейках памяти, а сейчас процессор переместил их в другие (ему могла приказать операционная система — Windows частенько «перетасовывает» содержимое памяти). Логические адреса, с которыми оперирует WinWord, остаются постоянными! Операционная система может переместить часть данных в файл подкачки, сбросив при этом «бит присутствия» в дескрипторе сегмента. Если программа захочет обратиться к засвопленной странице, то процессор по нулевому значению бита присутствия обнаружит ее отсутствие в памяти и выработает соответствующее прерывание. Святая обязанность любой операционной системы на него среагировать и восстановить эти данные в какой-нибудь области памяти, после чего возобновить работу прерванной программы. При этом логические адреса не меняются, в этом вся соль!

С режимом виртуального 8086 все просто — он наконец-то позволил в защищенном режиме запускать программы, написанные для реального режима, т. е. для MS-DOS. При этом в многозадачной среде любое количество MS-DOS приложений могло работать одновременно на одном компьютере, причем каждая программка думала, что она работает в одиночку, т. е. процессор обслуживает только ее. При запуске любой MS-DOS программы Windows 95/98 создает для нее виртуальную машину MS-DOS (DOS Virtual Machine) — задачу, использующую режим виртуального 8086. Кстати этот режим не запрещает DOS-приложениям использовать новые 32-битные регистры 386-го, за исключением тех, что необходимы для корректной работы самой системы защиты процессора и операционной системы.

Модель защищенного режима трешки без изменений работает и по сей день. Современные Pentium 4 и Athlon не содержат в режимах адресации практически ничего нового. Появившиеся в Pentium странички памяти по 4 Мб и 36-битная адресация в Pentium Pro не в счет, ибо эти возможности в домашних персоналках не применяются. По крайней мере Windows 98/NT их не использует точно. Поскольку Windows ME/2000 могут (по крайней мере формально) работать и на обычном (первом) Pentium, где этих расширений просто нет, то полагаю они тоже их не используют. Работающие у вас дома «окошки» наверняка обходятся сводом правил, придуманным Intel еще в далеком 1985 году.

Но и на солнце не без пятен: история 386-го была омрачена найденной в нем ошибкой. Процессор неправильно выполнял некоторые операции с 32-битными числами, в частности, умножение. Из-за отсутствия 32-битных операционных систем, и как следствие приложений, ошибку обнаружили только через полтора года, после того как появились соответствующие программы. Intel моментально устранила ошибку и микросхемы, изготовленные после апреля 1987 года ее не имели, а уже вышедшие чипы были промаркированы «только для 16-битных операций». При попытке установить на такой компьютер Windows 95, Setup выдает сообщение об ошибке «B1». Отличительный признак исправленных процессоров — все они маркируются двойным символом «сигма».

И напоследок кое-что о процессоре 386SX. Он был представлен 16 июня 1988 года как недорогая альтернатива полноценному 80386, который с этого момента стал называться 386DX. Программно 386SX на 100% совместим со старшим братом — без замера производительности разницу между ними не обнаружить. Отличий было всего два: сокращенная до 16 бит шина данных и до 24 бит адресная шина (в DX обе шины 32-битные). Заполнение 32-битных регистров данными из оперативной памяти в SX-версии происходило вдвое медленнее. Такая же потеря скорости наблюдалась и при записи содержимого 32-битного регистра в память. Но на скорость выполнения самих арифметических операций сокращение шины не влияло. Т. е. умножение или деление одного и того же числа, если оно уже находилось в регистре процессора, происходило в SX и DX моделях за одинаковое число тактов. В результате в 16-битных приложениях 386SX отставал от старшего брата всего на 10-20%, но в 32-битных разница была существеннее — 50-70%. Поэтому Windows 95 не имело большого смысла ставить на 386SX, в то время как на DX-версию, думаю, это было вполне оправдано (в случае 8 или более мегабайт памяти). Кроме того, в SX-версию нельзя было добавить свыше 16 Мб оперативной памяти — следствие урезанной до 24 бит адресной шины. Впрочем, это несущественно, кто из нас видел трешку с большей памятью? 🙂

10 апреля 1989 года Intel познакомила общественность со своим новым процессором i486. «Четверка» стала едва ли не самым важным процессором в истории ПК, наверное даже Pentium’у не выпадала такая честь. Догадываетесь о чем я? «Мультимедиа». Волшебное слово, как по мановению палочки обернувшее скучную машину в уникальный центр развлечения, досуга и творчества. А появление мультимедиа стало по-настоящему возможным только благодаря четверкам. Они привлекли внимание миллионов, унося их в миры Myst, Doom, Warcraft. Когда лет шесть назад я увидел на CD-ROM диске мультимедиа-энциклопедию динозавров с приятными на слух комментариями гида и видеороликами (!), я был пленен «персоналками» окончательно.

Что же послужило причиной такого прорыва, превратившего очередной, по сути, бизнес-процессор в предвестника эры мультимедиа? Я постараюсь ответить на это и рассказать о ключевых достоинствах четверки. Тех «изюминках», которые отличают ее от предшественника, 386-го процессора. Дело в том, что в поисках способа повышения производительности наиболее передового в те годы 386DX-33, инженеры Intel столкнулись с серьезными проблемами. Традиционный путь уже не годился. Существовавшая в конце 80-х технология производства чипов не позволяла плотнее размещать транзисторы на кристалле. Это создавало трудности с теплоотводом при повышении частоты. Еще труднее было повысить частоту материнской платы. Нужны были качественные (и очень дорогие) модули памяти, чтобы работа на частотах свыше 33 МГц была стабильной. К примеру, самые дешевые мегабайтные модули памяти (SIMM 30-pin 100-ns) стоили тогда порядка $400. С другой стороны, оставаясь в рамках традиционной CISC-архитектуры, было трудно уменьшать количество тактов, необходимых для выполнения той или иной операции.

Эти и другие проблемы не давали двигаться обычным путем, просто наращивая частоту процессора и материнской платы. Но решения были найдены. Коротко их можно описать шестью словами: кэш, конвейер, встроенный сопроцессор, коэффициент умножения. Эти термины на 95% описывают достоинства 486-го. Остальные 5% это пара-тройка мелких нововведений, о которых я упомяну позднее. Поехали?

Немного теории: кэш-память (cache, кэш) располагается «между» процессором и оперативной памятью, и, при обращении процессора к памяти, поиск нужных данных сначала производится в ней. Поскольку время доступа к кэшу гораздо меньше, чем к обычной памяти, а в большинстве случаев необходимые данные содержаться в кэше, среднее время доступа к памяти уменьшается. Если выключить в BIOS’е Pentium’а оба кэша, то по скорости получится. 386-й. Современные Pentium III или Athlon без обоих кэшей смогут обогнать разве что 486DX2-66. Наличие кэша оказывает огромное влияние на производительность, почти принципиальное.

486-й под микроскопом

В 486-м процессоре появился внутренний кэш объемом 8 Кб, единый для данных и инструкций. Кэш имел 4-канальную наборно-ассоциативную архитектуру и работал на уровне физических адресов памяти. Он содержал 128 наборов по 4 строки размером по 16 байт. Кэш умел работать только со строками, и если процессор требовал какой-нибудь байт, отсутствующий в кэше, то кэш-контроллер загружал из ОЗУ всю 16-байтную строку, содержащую необходимый байт. Выбор строки для замещения производился по алгоритму «псевдо-LRU», для этого каждому набору строк отводилось по 3 бита статистики использования. Алгоритм LRU (Least Recently Used) основан на поиске элемента, к которому дольше всего не было обращений. При каждом обращении к строке кэш-контроллер увеличивал на 1 соответствующий счетчик LRU. Приставка «псевдо» означает лишь несовершенство механизма работы, ведь под счетчик отводилось всего 3 бита, что дает нам 8 положений счетчика (2 3 ). После 8-го обращения к строке счетчик обнулится и соответствующая строка из самой «необходимой» станет самой «не необходимой» и будет прямым кандидатом на замещение. Кэш первых 486-х работал в режиме Write Through (сквозная запись). В этом случае при записи данных тратилось дополнительное время на их запись во внешнюю память (даже если они присутствовали в кэше). Эта политика напоминает работу дискового кэша SmartDrive с настройками по умолчанию, когда ускоряется только чтение данных, а запись не ускоряется. Последовавшие процессоры (некоторые 486DX2 и все 486DX4) позволяли переключаться на политику Write Back. В этом варианте запись данных, если их старая копия уже присутствовала в кэше, производилась только в кэш, а запись в ОЗУ откладывалась.

Процессор i486 мог использовать и внешний кэш (вне микросхемы процессора). Именно 486-у мы обязаны появлением 2-х уровнего кэширования: кэш, интегрированный в ядро процессора, стал называться кэшем первого уровня (L1), а кэш наружный, находящийся на материнской плате, — кэшем второго уровня (L2). Очевидно, что кэш первого уровня функционирует быстрее второго. Даже если они оба работают на частоте процессора, потому что при чтении данных из кэша L2 процессор все равно вынужден делать несколько пустых тактов, хотя и меньше, чем при чтении из ОЗУ. Объем кэша L2 в зависимости от материнской платы составлял от 256 до 512 Кб. В системных платах 386-х моделей наружный кэш обычно не превышал 128 Кб (типичный объем — 64 Кб). В марте 1994-го Intel, выпустив 486DX4, увеличила объем кэша L1 до 16 Кб (он по-прежнему оставался общим для данных и для команд).

Что отрицательного несло в себе появление внутреннего кэша? Резкое увеличение числа элементов (486-й стал первым микропроцессором, перешагнувшим некий рубеж, — он содержал более миллиона транзисторов, и около половины их приходилось на долю кэша) повлекло за собой сильный процент брака, и как следствие высокие цены. Не только у Intel, но и у конкурентов (AMD, Cyrix). И лишь появление Pentium’а привело к падению цен на 486-е. Вторым минусом было увеличение энергопотребления и повышение температуры процессора. Именно это вынудило Intel применить новый подход — впервые на процессоры стали устанавливать радиаторы с активным охлаждением.

Конвейер — это специальное устройство, входящее в состав процессора, и реализующее такой метод обработки команд, при котором их выполнение разбивается на несколько этапов. В чем преимущество? Представьте себе часовой завод, на котором работает только один мастер, зато на все руки. Он сможет за час из пружинок и винтиков собрать работающие часы. Это и будет одна машинная команда, выполненная в течение часа. А теперь представьте, что вместо одного мастера — 5 узких специалистов. Никто из них не в состоянии в одиночку собрать часы. Но первый умеет вытачивать корпус, второй — стрелки, третий — нанизывать детали на ось, и т. д. Главное, что они работают параллельно и без задержек: сделал детальку, передал другому, берись за следующую. К тому же выполнять простые операции легче, а значит быстрее, чем сложные. В итоге эти спецы за час совместной работы изготовят не 5, а 10 или даже более часов (в нашем случае — машинных команд). Этот принцип не нов и был открыт еще пару веков назад — вспомните мануфактуры, расцвет массового производства. Помимо ускорения вычислений конвейеризация дает еще одно преимущество: каждый мини-блок, выполняющий часть операции, представляет собой менее сложное устройство, чем один исполнительный супер-блок, а значит он менее чувствителен к повышению частоты. Разбиение сложного вычислительного модуля на мелкие позволяет эффективнее разгонять процессор (это особенно актуально в свете нынешнего Pentium 4 с его 20-ступенчатым конвейером).

Конвейер процессора i486 имел 5 ступеней (рис. 5). Не буду описывать назначение каждой ступени — на рисунке все показано. Справедливости ради стоит отметить, что Intel не изобретала конвейеры, по крайней мере, не была пионером в этой области: первые процессоры с полностью конвейеризированной архитектурой появились раньше. Например, процессор Z-80000 с 6-ступенчатым конвейером был анонсирован еще в начале 1986-го, но по некоторым причинам он не прижился в персональных компьютерах (впрочем, как и вся линейка процессоров Zilog).

ВСТРОЕННЫЙ СОПРОЦЕССОР

Сопроцессор (FPU, Floating-Point Unit) помогает основному процессору выполнять математические операции над вещественными числами. Начиная с 8086-го чипа сопроцессор представлял собой отдельную микросхему, имеющую то же название, что и центральный процессор, только с семеркой на конце: 8087, 80287, 80387. Микросхема 486-го стала первой микросхемой Intel со встроенным FPU. В состав процессора был введен математический сопроцессор, программно совместимый с сопроцессором 387. Процессор 486SX, появившийся 22 апреля 1991 года, отличался тем, что не имел встроенного сопроцессора. Любопытно, но первые 486SX представляли собой отбраковку полноценных 486, с неисправным блоком FPU, который был специальным образом заблокирован для использования. Полные 486 стали называться 486DX (по аналогии с серией 386, только там разница между SX и DX версиями была в разрядностях шин данных и адреса). Интересно заметить, что существовал 487SX процессор — по сути, аналог 486DX (CPU + FPU), устанавливаемый в гнездо сопроцессора материнской платы 486SX, и отключающий «основной» SX-процессор.

КОЭФФИЦИЕНТ УМНОЖЕНИЯ

Все знают, что коэффициент умножения — это число на которое надо умножить тактовую частоту материнской платы, чтобы получить частоту работы самого процессора. Но к 486-м, появившимся в 1989 году, он не имел отношения. Эти процессоры работали на частоте системной платы, также как и 386-е. Первым процессором, функционирующим на удвоенной частоте материнской платы, стал i486DX2-50, объявленный 3 марта 1992 года. Коэффициент умножения — одно из важнейших улучшений и оно тесно переплетается с внутренним кэшем. Без кэша вводить коэффициент почти бессмысленно. Именно появление i486DX2-50 и материнских плат, поддерживающих разные частоты шины и коэффициенты умножения, заложило основу для практического «разгона» процессоров.

ЭТИ ВАЖНЫЕ МЕЛОЧИ.

В общем-то, их не так уж и мало. Во-первых, появилось 5 новых команд: XADD (обычный ADD, только шиворот-навыворот — результат суммы помещается во второй операнд, а не в первый), BSWAP (перестановка байт из порядка младший-старший в порядок старший-младший), INVD (очистка внутреннего кэша без выгрузки строк, опасно при политике Write Back), WBINVD (очистка внутреннего кэша с выгрузкой всех модифицированных строк, может потребовать значительное число тактов), CMPXCHG (сравнение и обмен). Во-вторых, была повышена производительности шины данных — введены пакетные циклы (burst), позволяющие передавать очередное слово в каждом такте шины (а не через такт, как в обычном режиме).

Кроме того, расширены средства тестирования — введены регистры TR3, TR4, TR5 для тестирования внутреннего кэша. Увеличена очередь команд до 16 байт. Добавлена защита страниц памяти на уровне супервизора. Введены: буферы отложенной записи, функции контроля выравнивания операндов, ZIF-сокеты (в более поздних моделях) и возможность аппаратного тестирования процессора извне по интерфейсу JTAG (тоже не у всех моделей). Вот, пожалуй, и все.

В завершающей части я расскажу о самом известном процессоре Intel — «Первом Пентиуме». Мне кажется, этот процессор для Intel стал тем же, что и «Bohemian Rhapsody» для Queen — волшебной ракетой, что вывела фирму на орбиту известности и миллиардных оборотов. Сегодня «Первый Пентиум» — уже история. Но согласитесь, его магическая аура присутствует по-прежнему. Благодаря чему же он так прославился? Что нового привнес в нашу жизнь? Отчего это слово в последние годы стало нарицательным, порой — даже синонимом слова «компьютер»? Знаю, об этом можно писать целую книгу, но моя цель чуточку проще: я попробую изложить в рамках статьи только самую суть.

Pentium под микроскопом

Когда в июне 1989 года Винод Дэм, инженер фирмы Intel, делал первые «наброски» Pentium’а, он и не подозревал, насколько сложной окажется предстоящая работа и что именно этот продукт станет одним из главных достижений Intel. Вскоре проект был разбит на несколько этапов. Для тестирования была придумана технология, позволившая имитировать функционирование еще не существовавшего процессора. Достигали это применением специальных устройств-блоков, объединенных на 14 платах с помощью кабелей. Когда первое тестирование было закончено, а основные ошибки — исправлены, макет начал работать. В разработке и тестирования Pentium’а принимали участие не только специалисты Intel, но и сторонние разработчики, что, наверное, немало способствовало общему успеху. В конце 1991 года макет процессора был завершен, и инженеры смогли запустить на нем программное обеспечение. Естественно, ни о какой скорости пока не было и речи: макет функционировал в полном объеме, но не быстрее калькулятора! Работа продолжалась, и когда все архитектурные тонкости уже определились, настала очередь проектировщиков: они принялись под микроскопом изучать топологию процессора и прохождение сигналов с целью ее оптимизации. Эта работа была завершена где-то к февралю 1992 года, после чего началось тестирование опытной партии. В апреле 1992 года наконец-то приняли решение о промышленном освоении. Индустриальной базой стала 5-я Орегонская фабрика, лучшая на тот момент. Примерно через год более 3 млн. транзисторов окончательно перенесли на шаблоны и запустили цикл производства. 4-х летняя история создания процессора Pentium завершилось его презентацией 22 марта 1993 года.

СУПЕРСКАЛЯРНОСТЬ

Давайте заглянем в недра — чего же там нового? Помимо традиционного повышения тактовой частоты и сокращения времени (числа тактов) выполнения инструкций, есть одно свойство, наиболее качественно отличающее его от предшественника. В главе об особенностях 486-го я красной нитью выделял появление конвейера — устройства, разбивающего процесс выполнения команды на несколько этапов. Напомню этапы конвейера 486-го: выборка команды, ее декодирование, вычисление адресов операндов, выполнение и, наконец, сохранение результата. Оказывается, конвейер Pentium содержит эти же этапы, но самих конвейеров прибавилось! Новая микросхема стала первым процессором Intel, способным выполнять за такт СРАЗУ ДВЕ инструкции. Архитектура, при которой процессор имеет несколько конвейеров, получила название «суперскалярной», в противовес старой, одноконвейерной, т.е. «скалярной» архитектуре.

Pentium имеет 2 целочисленных конвейера — u и v (рис. 6). Правда, они не равноценны: главным считается u-конвейер, он поддерживает весь набор инструкций и работает на полную катушку. v-конвейер имеет некоторые ограничения и лишь помогает основному. Он умеет обрабатывать только «простые» инструкции — целочисленная арифметика, логические команды, команды перехода. Вдобавок, он не может работать, если его действия зависят от результата выполнения команды, идущей по u-конвейеру. Условий, по которым определяется возможность «спаривания» инструкций, около десяти (если интересно — пишите, я пришлю). В моменты, не удовлетворяющие этим правилами, v-конвейер не берет новых команд, лишь продвигая по этапам те, за выполнение которых он уже взялся. Затем он простаивает, ожидая благоприятной ситуации. В остальных случаях процессор одновременно выполняет сразу 2 инструкции, точнее, учитывая конвейерную архитектуру, правильней сказать: «на различных стадиях выполнения одновременно продвигаются 2 потока инструкций».

У конвейерной архитектуры одно слабое место: при выполнении команд, чтобы выбрать из памяти следующую, иногда нужно знать результат выполнения текущей. Дело в том, что некоторые команды (например, условного перехода) нарушают порядок выполнения инструкций, приказывая процессору «перепрыгнуть» не на следующую по порядку команду, а на какую-то другую. В любой мало-мальски сложной программе это встречается сплошь и рядом. В простейшем случае (как в 486-м) конвейер будет ждать выполнения команды, определяющей адрес следующей инструкции, и лишь затем приступит к выборке. В Pentium же появилось интересное новшество branch prediction — предсказание переходов. Процессор запоминает статистику последних 256 переходов и на ее основе выдает предположение о следующем. Если переход был предсказан верно, удается избежать простоя, если нет — конвейер полностью очищается и начинает заполняться снова (на что тратится время).

Суперскалярность — особенность №1 микропроцессоров 5-го поколения. Она присуща не только Pentium, но и AMD K5, Cyrix M1, словом, всем его современникам. Кстати, помимо этой архитектуры, у Pentium еще один плюс: ступени D2 обоих конвейеров обзавелись многоканальным сумматором. Задержки, возникавшие в 486-м при декодировании сложных многокомпонентных адресов (например, «EBX + ESI + смещение»), наконец-то ушли в прошлое.

2-е по значимости отличие Pentium от 486-го — быстрый сопроцессор (FPU). Именно Pentium вылечил у некоторых программистов фобию к числам с «плавучкой». Помнится, раньше в Фидо ходила куча советов, как рассчитать трехмерную сцену, обходясь только целыми числами — получались вещественные с «фиксированной точкой». Сказывалось на точности, но для графики 320×200 вполне хватало.

С появлением Pentium стало очевидным: нет смысла соревноваться с оптимизированным сопроцессором. В результате появился целый ряд игр (Quake, MDK), сотворивших революцию в игровом мире. Благодаря новому FPU, мощность Pentium’а оказалась достаточной для показа Video CD без приобретения MPEG-карт, стоящих сотни долларов. За это отдельное спасибо Xing MPEG Player’у — родоначальнику чисто «софтового» проигрывания видео. А разве плохо слушать mp3, занимаясь при этом чем-то другим — гуляя в Интернете или набирая текст? На быстрой «четверке» mp3-файлы тоже игрались, но при работе в Word или Explorer звук начинал заикаться.

Причина ускорения сопроцессора кроется в его новой конвейерной архитектуре. Инструкции FPU сначала проходят по u-конвейеру до ступени D2 включительно, после чего сворачивают на ступени X1, X2 и WF конвейера FPU (на схеме). Причем инструкции на целочисленном конвейере могут продвигаться во время просчета длительных инструкций FPU — именно с учетом этого был оптимизирован Quake. Почему конвейер сопроцессора сам не выбирает инструкции? В основном из-за вопросов совместимости, а отчасти — чтобы не заниматься проблемами адресации в защищенном режиме, эту сложную рутину делает основной процессор.

Другая причина ускорения, пусть и не столь важная, — увеличение шины данных до 64 бит. Внешняя шина данных — это магистраль, передающая информацию между процессором и памятью. Особо заметно увеличение шины проявилось в серьезных программах, наподобие AutoCAD или Corel Draw. Почему? Небольшое отступление: в играх и MPEG-плеерах, где главное — скорость, процессор, как правило, работает с числами одинарной точности (32 бит). Это разумный компромисс между скоростью и качеством. Winamp при декодировании mp3 обходится 32-битными числами, что дает, по сравнению с 64-битными, меньшее на 30-40% потребление процессора. А разницу в звучании с NAD’ом (mp3-плеер, применяет 64-битную арифметику) замечают только завзятые аудиофилы. Но то, что оптимально в играх и мультимедиа, может быть неприемлемо в конструкторском или дизайнерском деле. В этих программах обычно применяют вещественные числа двойной точности (64 бит), иногда — даже повышенной (80 бит). Из-за 32-битной шины 486-го их приходилось загружать из памяти за несколько тактов. Pentium же, благодаря новой шине, обычно справляется за один такт. В итоге, пропускная способность была заметно увеличена — 528 Мб/с для Pentium-66 по сравнению со 160 Мб/с для 486DX-50. Из отрицательных «бонусов» 64-битной шины назову только один — потребность в специальной организации памяти. Помните правило: «SIMM’ы вставлять только парами»? Вот-вот, это оттуда.

С кэшем ситуация неоднозначная. Разумеется, он стал быстрее (в основном, благодаря объему), но появились и минусы. Сначала о главном: кэш L1 в Pentium стал раздельным и увеличился с 8 до 16 Кб — по 8 Кб на кэш команд и кэш данных. Оба кэша поддерживают политику отложенной записи (Write Back), хотя в кэш команд запись обычно не производится.

По поводу разделения кэша не могу сказать, что это однозначно хорошо. Не случайно процессоры M1 фирмы Cyrix имели общий кэш L1 и в задачах, не требующих интенсивных FPU-вычислений, показывали отличную производительность. Я могу привести примеры, когда по скорости выигрывает тот или иной вариант кэша, если интересно, к тому же его разделение приводит к росту числа транзисторов. Скорее всего, это разделение в Pentium было мотивировано упрощением схем некоторых ступеней конвейеров (ступени PF и WB).

Еще один минус: строки кэша стали 32-байтными (в 486-м строки 16-байтные). С одной стороны, это снизило число «служебных» транзисторов, а с другой — привело к небольшому падению эффективности. Напомню: все данные в кэше хранятся в строках, и в одной строке могут находиться только смежные данные, т. е. имеющие близкостоящие друг от друга адреса. Каждая строка имеет свойство «достоверности». По нему кэш-контроллер определяет, можно использовать содержащиеся в ней данные или нет. Строка не может быть «частично достоверной», она либо верна целиком, либо неверна совсем. Строка заполняется данными из памяти полностью, даже если процессору не хватает в кэше всего одного байта.

В Pentium появилось несколько новых инструкций. Перечислю: распознавание семейства и модели процессора — CPUID, сравнение и обмен сразу 8 байтов — CMPXCHG8B, чтение/запись из/в регистр, специфичный для конкретной модели (Model Specific Register) — RDMSR и WRMSR. И еще одна, RDTSC — потрясная инструкция — возвращает в EDX:EAX текущее значение внутреннего счетчика, который увеличивается на единицу при каждом цикле процессора (т.е. эта команда дает количество тактов, сделанных процессором с момента включения). При 133 МГц этого счетчика хватает на 4400 лет.

Кое-что из несущественного для домашних персоналок: в процессоре была введена трассировка инструкций и мониторинг производительности, реализован интерфейс построения двухпроцессорных систем с симметричной архитектурой (начиная со 2-го поколения Pentium, 1-е поколение позволяло строить лишь функционально-избыточную систему). Введена возможность оперирования страницами размером в 4 Мб. Расширен режим виртуального 8086 — введена виртуализация флага прерываний. Вот и все.

Не знаю как вам, а мне достижения и нововведения современных процессоров кажутся малозначимыми по сравнению с принципиальными нововведениями процессоров прошлых лет. Исключение составляет, пожалуй, только появление HyperThreading’а — интересное нововведение, заслуживающее отдельной статьи. 🙂 В заключение остается только пожелать «процессоростроителям» не ударяться в слепую гонку гигагерц и нанометров, а почаще думать о реализации качественно новых эффективных методов вычислений.

Сколько бит помещается в регистр ax в процессорах семейства intel

Издание: Системное программное обеспечение: Учебник для вузов

В рамках данного учебного пособия мы, естественно, не будем рассматривать все многообразие современных 32-разрядных микропроцессоров, используемых в ПК и иных вычислительных системах. Здесь мы ограничимся рассмотрением только архитектурных, а не технических характеристик микропроцессоров. Под обозначением i80x86 будем понимать любые 32-битовые микропроцессоры, имеющие такой же основной набор команд, как и в первом 32-битовом микропроцессоре Intel 80386, и те же архитектурные решения, что и в микропроцессорах фирмы Intel.

Реальный и защищенный режимы работы процессора

Широко известно, что первым микропроцессором, на базе которого была создана IBM PC, был Intel 8088. Этот микропроцессор отличался от первого 16-разрядного микропроцессора фирмы Intel — 8086 — прежде всего тем, что у него была 8-битовая шина данных, а не 16-битовая (как у 8086). Оба эти микропроцессора предназначались для создания вычислительных устройств, которые бы работали в однозадачном режиме, то есть специальных аппаратных средств для поддержки надежных и эффективных мультипрограммных ОС в них не было.
Однако к тому времени, когда разработчики осознали необходимость включения в микропроцессор специальной аппаратной поддержки для мультипрограммных вычислений, уже было создано очень много программных продуктов. Поэтому для совместимости с первыми компьютерами в последующих версиях микропроцессоров была реализована возможность использовать их в двух режимах — реальном (real mode — так назвали режим работы первых 16-битовых микропроцессоров) и защищенном (protected mode — означает, что параллельные вычисления могут быть защищены аппаратно-программными механизмами).
Подробно рассматривать архитектуру первых 16-битовых микропроцессоров i8086/i8088 мы не будем, поскольку этот материал должен изучаться в предыдущих дисциплинах учебного плана. Для тех же, кто с ним не знаком, можно рекомендовать такие книги, как [52, 73], и многие другие. Однако напомним, что в этих микропроцессорах (а значит, и в остальных микропроцессорах семейства i80x86 при работе их в реальном режиме) обращение к памяти с возможным адресным пространством в 1 Мбайт осуществляется посредством механизма сегментной адресации (рис. 3.1). Этот механизм был использован для увеличения количества разрядов, участвующих в указании адреса ячейки памяти, с которой в данный момент осуществляется работа, с 16 до 20 и тем самым увеличения объема памяти.
Конкретизируем задачу и ограничимся рассмотрением определения адреса команды. Для адресации операндов используется аналогичный механизм, только участвуют в этом случае другие сегментные регистры. Напомним, что для определения физического адреса команды содержимое сегментного регистра CS (code segment) умножается на 16 за счет добавления справа (к младшим битам) четырех нулей, после чего к полученному значению прибавляется содержимое указателя команд (регистр IP, instruction pointer). Получается двадцатибитовое значение, которое и позволяет указать любой байт из 2 20 .

Рис. 3.1. Схема определения физического адреса для процессора 8086

В защищенном режиме работы определение физического адреса осуществляется совершенно иначе. Прежде всего используется сегментный механизм для организации виртуальной памяти. При этом адреса задаются 32-битовыми значениями. Кроме этого, возможна страничная трансляция адресов, также с 32-битовыми значениями. Наконец, при работе в защищенном режиме, который по умолчанию предполагает 32-битовый код, возможно исполнение двоичных программ, созданных для работы микропроцессора в 16-битовом режиме. Для этого введен режим виртуальной 16-битовой машины и 20-битовые адреса реального режима транслируются с помощью страничного механизма в 32-битовые значения защищенного режима. Наконец, есть еще один режим — 16-битовый защищенный, позволяющий 32-битовым микропроцессорам выполнять защищенный 16-битовый код, который был характерен для микропроцессора 80286. Правда, следует отметить, что это последний режим практически не используется, поскольку программ, созданных для него, не так уж и много.
Для изучения этих возможностей рассмотрим сначала новые архитектурные возможности микропроцессоров i80x86.

Новые системные регистры микропроцессоров i80x86

Основные регистры микропроцессора i80x86, знание которых необходимо для понимания защищенного режима работы, приведены на рис. 3.2. Следует обратить внимание на следующее:

  • указатель команды EIP — 32 битовый регистр, младшие 16 разрядов этого регистра есть регистр IP;
  • регистр флагов EFLAGS — 32 бита, младшие 16 разрядов представляют регистр FLAGS;
  • регистры общего назначения EAX, EBX, ECX, EDX, а также ESP, EBP, ESI, EDI — 32-битовые, однако их младшие 16 разрядов представляют собой известные регистры AX, BX, CX, DX, SP, BP, SI, DI;
  • сегментные регистры CS, SS, DS, ES, FS, GS — 16-битовые. При каждом из регистров CS, SS, DS, ES, FS, GS изображены пунктиром скрытые от программистов (недоступны никому, кроме собственно микропроцессора) 64-битовые регистры, в которые загружаются дескрипторы соответствующих сегментов;
  • регистр-указатель на локальную таблицу сегментов текущей задачи— LDTR (16 битов). При этом регистре также имеется «теневой» (скрытый от программиста) 64-битовый регистр, в который микропроцессор заносит дескриптор, указывающий на таблицу дескрипторов сегментов задачи, описывающих ее локальное виртуальное адресное пространство;
  • регистр-указатель задачи TR (16 битов). Указывает на дескриптор в глобальной таблице дескрипторов, позволяющий получить доступ к дескриптору задачи TSS — информационной структуре, которую поддерживает микропроцессор для управления задачами;
  • регистр GDTR (48 битов) глобальной таблицы GDT, содержащей как дескрипторы общих сегментов, так и специальные системные дескрипторы. В частности, в GDTR находятся дескрипторы, с помощь которых можно получить доступ к сегментам TSS;
  • регистр IDTR (48 битов) таблицы дескрипторов прерываний. Содержит информацию, необходимую для доступа к «таблице прерываний» IDT;
  • управляющие регистры CR0 — CR3 (32-битовые) и некоторые другие регистры.

Управляющий регистр CR0 содержит целый ряд флагов, которые определяют режимы работы микропроцессора. Подробно об этих флагах можно прочитать в книгах [2, 22, 48]. Мы же просто ограничимся тем фактом, что самый младший бит (PE, protect enable) этого регистра определяет режим работы процессора. При PE=0 процессор функционирует в реальном режиме работы, а при единичном значении микропроцессор переключается в защищенный режим. Самый старший бит регистра CR0 (бит PG, paging) определяет, включен (PG=1) или нет (PG=0) режим страничного преобразования адресов.
Регистр CR2 предназначен для размещения в нем адреса подпрограммы обработки страничного исключения, то есть в случае использования страничного механизма отображения памяти обращение к отсутствующей странице будет вызывать переход на соответствующую подпрограмму диспетчера памяти, и для определения этой подпрограммы будет задействован регистр CR2.
Регистр CR3 содержит номер физической страницы, в которой располагается таблица каталогов таблиц страниц текущей задачи. Очевидно, что, приписав к этому номеру нули, мы попадем на начало этой страницы.

Адресация в 32-разрядных микропроцессорах i80x86 при работе в защищенном режиме

Поддержка сегментного способа организации виртуальной памяти

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

  • чтобы у каждого вычислительного процесса могло быть свое собственное (личное, локальное) адресное пространство, которое никак не может пересекаться с адресными пространствами других задач;
  • чтобы существовало общее (разделяемое) адресное пространство.
    Поэтому в микропроцессорах i80x86 реализован сегментный способ организации распределения памяти. Помимо этого, в этих микропроцессорах может быть задействована и страничная трансляция. Поскольку для каждого сегмента нужен дескриптор, устройство управления памятью поддерживает соответствующую информационную структуру. Формат дескриптора сегмента приведен на рис. 3.3.

Поля дескриптора (базовый адрес, поле предела) размещены в дескрипторе не непрерывно, а в разбивку, во-первых, из-за того, что разработчики постарались минимизировать количество перекрестных соединений в полупроводниковой структуре микропроцессора, а во-вторых — вследствие необходимости обеспечить полную совместимость микропроцессоров (предыдущий микропроцессор i80286 работал с 16-битовым кодом и тоже поддерживал сегментный механизм реализации виртуальной памяти). Необходимо заметить, что формат дескриптора сегмента, изображенный на рис. 3.3, справедлив только для случая нахождения соответствующего сегмента в оперативной памяти. Если же бит присутствия в поле прав доступа равен нулю (сегмент отсутствует в памяти), то все биты, за исключением поля прав доступа, считаются неопределенными и могут использоваться системными программистами (для указания адреса сегмента во внешней памяти) произвольным образом.

Рис. 3.3. Дескриптор сегмента

Локальное адресное пространство задачи определяется через таблицу LDT (local descriptor table). У каждой задачи может быть свое локальное адресное пространство. Общее или глобальное адресное пространство определяется через таблицу GDT (global descriptor table). Само собой, что работу с этими таблицами (их заполнение и последующую модификацию) должна осуществлять операционная система. Доступ к таблицам LDT и GDT со стороны прикладных задач должен быть исключен.
При переключении микропроцессора в защищенный режим он начинает совершенно другим образом, чем в реальном режиме, вычислять физические адреса команд и операндов. Прежде всего, содержимое сегментных регистров интерпретируется иначе: считается, что там содержится не адрес начала сегмента, а номер соответствующего сегмента. Для того чтобы подчеркнуть этот факт, сегментные регистры CS, SS, DS, ES, FS, GS в таком случае даже называются иначе — селекторами сегментов. При этом каждый селекторный регистр разбивается на следующие три поля (рис. 3.4):

  • поле индекса (index) — старшие 13 битов (3-15). Определяет собственно номер сегмента (его индекс в соответствующей таблице дескрипторов);
  • поле индикатора таблицы сегментов (table index, TI) — бит с номером 2. Определяет часть виртуального адресного пространства (общее или принадлежащее только данной задаче). Если TI=0, то Index указывает на элемент в глобальной таблице дескрипторов GDT, то есть идет обращение к общей памяти. Если TI=1, то идет обращение к локальной области памяти текущей задачи; это пространство описывается локальной таблицей дескрипторов LDT;
  • поле уровня привилегий — биты 0 и 1. Указывает запрашиваемый уровень привилегий (RPL, requested privilege level).
    Операционная система в процессе своего запуска инициализирует многие регистры и, прежде всего, GDTR. Этот регистр содержит начальный адрес глобальной таблицы дескрипторов (GDT) и ее размер. Как мы уже знаем, в GDT находятся дескрипторы глобальных сегментов и системные дескрипторы.

Для манипулирования задачами ОС имеет информационную структуру, которую мы уже определили как дескриптор задачи (см. раздел «Понятия вычислительного процесса и ресурса», глава 1). Микропроцессор поддерживает работу с наиболее важной частью дескриптора задачи, которая меньше всего зависит от операционной системы. Эта инвариантная часть дескриптора, с которой и работает микропроцессор, названа сегментом состояния задачи (task state segment, TSS). Перечень полей TSS изображен на рис. 3.5. Видно, что в основном этот сегмент содержит контекст задачи. Процессор получает доступ к этой структуре с помощью регистра задачи (task register, TR).
Регистр TR содержит индекс (селектор) элемента в GDT. Этот элемент представляет собой дескриптор сегмента TSS. Дескриптор заносится в теневую часть регистра (см. рис. 3.2). К рассмотрению TSS мы еще вернемся, а сейчас заметим, что в одном из полей TSS содержится указатель (селектор) на локальную таблицу дескрипторов данной задачи. При переходе процессора с одной задачи на другую содержимое поля LDTR заносится микропроцессором в одноименный регистр. Инициализировать регистр TR можно и явным образом.
Итак, регистр LDTR содержит селектор, указывающий на один из дескрипторов глобальной таблицы GDT. Этот дескриптор заносится микропроцессором в теневую часть регистра LDTR и описывает таблицу LDT для текущей задачи. Теперь, когда у нас определены как глобальная, так и локальная таблица дескрипторов, можно рассмотреть процесс определения линейного адреса. Для примера рассмотрим процесс получения адреса команды. Адреса операндов определяются по аналогии, но задействованы будут другие регистры.
Микропроцессор анализирует бит TI селектора кода и в зависимости от его значения, извлекает из таблицы GDT или LDT дескриптор сегмента кода с номером (индексом), который равен полю index (биты 3-15 селектора, см. рис. 3.4). Этот дескриптор заносится в теневую (скрытую) часть регистра CS. Далее микропроцессор сравнивает значение регистра EIP (extended instruction pointer) с полем размера сегмента, содержащегося в извлеченном дескрипторе, и если смещение относительно начала сегмента не превышает размера предела, то значение EIP прибавляется к значению поля начала сегмента и мы получаем искомый линейный адрес команды. Линейный адрес — это одна из форм виртуального адреса. Исходный двоичный виртуальный адрес, вычисляемый в соответствии с используемой адресацией, преобразуется в линейный. В свою очередь, линейный адрес будет либо равен физическому (если страничное преобразование отключено), либо с помощью страничной трансляции преобразуется в физический адрес. Если же смещение из регистра EIP превышает размер сегмента кода, то эта аварийная ситуация вызывает прерывание и управление должно передаваться супервизору ОС.

Рис. 3.5. Сегмент состояния задачи (TSS)

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

Рис. 3.6. Процесс получения линейного адреса команды

Поддержка страничного способа организации виртуальной памяти

При создании микропроцессора i80386 разработчики столкнулись с очень серьезной проблемой в реализации страничного механизма. Дело в том, что микропроцессор имеет широкую шину адреса — 32 бита — и возникает вопрос о разбиении всего адреса на поле страницы и поле индекса. Если большое количество битов адреса отвести под индекс, то страницы станут очень большими, что повлечет большие потери и на фрагментацию, и на операции ввода/вывода, связанные с замещением страниц. Хотя количество страниц стало бы при этом меньше, и накладные расходы на их поддержание тоже уменьшились бы. Если же размер страницы уменьшить, то большое поле номера страницы привело бы к появлению громадного количества возможных страниц и необходимо было либо вводить какие-то механизмы контроля за номером страницы (с тем, чтобы он не выходил за размеры таблицы страниц), либо создавать эти таблицы максимально возможного размера. Разработчики пошли по пути, при котором размер страницы все же небольшой (он выбран равным 2 12 =4096=4К), а поле номера страницы величиной в 20 битов, в свою очередь, разбивается на два поля и осуществляется двухэтапная (двухшаговая) страничная трансляция.
Для описания каждой страницы создается соответствующий дескриптор. Длина дескриптора выбрана равной 32 битам: 20 битов линейного адреса определяют номер страницы (по существу — ее адрес, поскольку добавление к нему (приписывание в качестве младших разрядов) 12 нулей приводит к определению начального адреса страницы), а остальные биты разбиты на следующие поля, которые изображены на рис. 3.7. Как видно, три бита дескриптора зарезервированы для использования системными программистами при разработке подсистемы организации виртуальной памяти. С этими битами микропроцессор сам не работает.

Рис. 3.7. Дескриптор страницы

Прежде всего, микропроцессор анализирует самый младший бит дескриптора — бит присутствия, ибо если поле present равно нулю, то это означает отсутствие данной страницы в оперативной памяти, и такая ситуация влечет прерывание в работе процессора с передачей управления соответствующей программе, которая должна будет загрузить затребованную страницу. Бит dirty — «грязный» — предназначен для отметки, что данную страницу модифицировали и при замещении этого страничного кадра следующим ее необходимо сохранить во внешней памяти. Бит обращения (access) свидетельствует о том, что к данной таблице или странице осуществлялся доступ. Он используется для определения страницы, которая будет участвовать в замещении при использовании дисциплин LRU или LFU. Наконец, первый и второй биты используются для защиты памяти.
Старшие 10 битов линейного адреса определяют номер таблицы страниц (page table entry, PTE), из которой посредством вторых 10 битов линейного адреса выбирается соответствующий дескриптор виртуальной страницы. И уже из этого дескриптора выбирается номер физической страницы, если данная виртуальная страница отображена сейчас на оперативную память. Эта схема определения физического адреса по линейному изображена на рис. 3.8.
Первая таблица, которую мы индексируем первыми (старшими) 10 битами линейного адреса, названа таблицей каталогов таблиц страниц (page directory entry, PDE). Ее адрес в оперативной памяти определяется старшими 20 битами управляющего регистра CR3.

Рис. 3.8. Трансляция линейного адреса в микропроцессорах i80x86

Каждая из таблиц PDE и PTE состоит из 1024 элементов (2 10 =1024). В свою очередь, каждый элемент (дескриптор страницы) имеет длину 4 байта (32 бита), поэтому размер этих таблиц как раз соответствует размеру страницы.
Оценим теперь эту двухшаговую схему трансляции с позиций расхода памяти. Каждый дескриптор описывает страницу размером 4 Кбайт. Следовательно, одна таблица страниц, содержащая 1024 дескриптора, описывает пространство памяти в 4 Мбайт. Если наша задача пользуется виртуальным адресным пространством, например, в 50 Мбайт (предположим, что речь идет о некотором графическом редакторе, который обрабатывает изображение, состоящее из большого количества пикселов), то для описания этой памяти необходимо иметь 14 страниц, содержащих таблицы PTE. Кроме этого, нам потребуется для этой задачи еще одна таблица PDE (тоже размером в одну страницу), в которой 14 дескрипторов будут указывать на местонахождение упомянутых таблиц PTE. Остальные дескрипторы PDE могут быть не задействованы. Итого, для описания 50 Мбайт адресного пространства задачи потребуется всего 15 страниц, то есть 60 Кбайт памяти, что можно считать приемлемым.
Если бы не был использован такой двухшаговый механизм трансляции, то потери памяти на описание адресного пространства могли бы составить 4(Кбайт)• 2 10 = =4 (Мбайт)! Очевидно, что это уже неприемлемое решение.
Итак, микропроцессор для каждой задачи, для которой у него есть TSS, позволяет иметь таблицу PDE и некоторое количество PTE. Поскольку это дает возможность адресоваться к любому байту из 2 32 , а шина адреса как раз и позволяет использовать физическую память с таким объемом, то можно как бы отказаться от сегментного способа адресации. Другими словами, если считать, что задача состоит из одного единственного сегмента, который, в свою очередь, разбит на страницы, то фактически мы получаем только один страничный механизм работы с виртуальной памятью. Этот подход получил название «плоской памяти». При использовании плоской модели памяти упрощается создание и операционных систем, и систем программирования. Кроме этого, уменьшаются расходы памяти для поддержки системных информационных структур. Поэтому в абсолютном большинстве современных 32-разрядных ОС, создаваемых для микропроцессоров i80x86, используется плоская модель памяти.

Режим виртуальных машин для исполнения приложений реального режима

Разработчики рассматриваемого семейства микропроцессоров в своем стремлении обеспечить максимально возможную совместимость архитектуры пошли не только на то, чтобы обеспечить возможность программам, созданным для первых 16-разрядных ПК, без проблем выполняться на компьютерах с более поздними моделями микропроцессоров за счет введения реального режима работы. Они также обеспечили возможность выполнения 16-разрядных приложений реального режима при условии, что сам процессор при этом функционирует в защищенном режиме работы и операционная система, используя соответствующие аппаратные средства микропроцессора, организует мультипрограммный (мультизадачный) режим. Другими словами, микропроцессоры i80x86 поддерживают возможность создания операционных сред реального режима при работе микропроцессора в защищенном режиме. Если условно назвать 16-разрядные приложения DOS-приложениями (поскольку в абсолютном большинстве случаев это именно так), то можно сказать, что введена поддержка для организации виртуальных DOS-машин, работающих вместе с обычными 32-битовыми приложениями защищенного режима. Это даже нашло отражение в названии режима работы микропроцессоров i80x86 — режим виртуального процессора i8086, иногда (для краткости) его называют режимом V86 или просто виртуальным режимом, — при котором в защищенном режиме работы может исполняться код DOS-приложения. Мультизадачность при выполнении нескольких программ реального режима будет поддержана аппаратными средствами защищенного режима.
Переход в виртуальный режим осуществляется посредством изменения бита VM (virtual mode) в регистре EFLAGS. Когда процессор находится в виртуальном режиме, для адресации памяти используется схема реального режима работы — (сегмент: смещение) с размером сегментов до 64 Кбайт, которые могут располагаться в адресном пространстве размером в 1 Мбайт, однако полученные адреса считаются не физическими, а линейными. В результате применения страничной трансляции осуществляется отображение виртуального адресного пространства 16-битового приложения на физическое адресное пространство. Это позволяет организовать параллельное выполнение нескольких задач, разработанных для реального режима, да еще и совместно с обычными 32-битовыми приложениями, требующих защищенного режима работы.
Естественно, что для обработки прерываний, возникающих при выполнении 16-битовых приложений в виртуальном режиме, процессор возвращается из этого режима в обычный защищенный режим. В противном случае невозможно было бы организовать полноценную виртуальную машину. Очевидно, что обработчики прерываний для виртуальной машины должны эмулировать работу подсистемы прерываний процессора i8086. Другими словами, прерывания отображаются в операционную систему, работающую в защищенном режиме, и уже основная ОС моделирует работу операционной среды выполняемого приложения.
Вопрос, связанный с операциями ввода/вывода, которые недоступны для обычных приложений (см. следующую главу), решается аналогично. При попытке выполнить недопустимые команды ввода/вывода возникают прерывания, и необходимые операции выполняются операционной системой, хотя задача об этом и «не подозревает». При выполнении команд IN, OUT, INS, OUTS, CLI, STI процессор, находящийся в виртуальном режиме и исполняющий код на уровне привилегий третьего (самого нижнего) кольца защиты, за счет возникающих вследствие этого прерываний переводится на выполнение высоко привилегированного кода операционной системы.
Таким образом, ОС может полностью виртуализировать ресурсы компьютера: и аппаратные, и программные, создавая иную полноценную операционную среду; при существовании так называемых нативных приложений, создаваемых по собственным спецификациям данной ОС. Очень важным моментом для организации полноценной виртуальной машины является реализация виртуализации не только программных, но и аппаратных ресурсов. Так, например, в ОС Windows NT эта задача выполнена явно неудачно, тогда как в OS/2 имеется полноценная виртуальная машина как для DOS-приложений, так и для приложений, работающих в среде спецификаций Win16. Правда, в последнее время это уже перестало быть актуальным, поскольку появилось большое количество приложений, работающих по спецификациям Win32 API.

Защита адресного пространства задач

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

Уровни привилегий для защиты адресного пространства задач

Для того чтобы запретить пользовательским задачам модифицировать области памяти, принадлежащие самой ОС, необходимо иметь специальные средства. Одного разграничения адресных пространств через механизм сегментов мало, ибо можно указывать различные значения адреса начала сегмента и тем самым получать доступ к чужим сегментам. Другими словами, необходимо в явном виде разграничивать системные сегменты данных и кода от сегментов, принадлежащих пользовательским программам. Поэтому были введены два основных режима работы процессора: пользователя и супервизора. Большинство современных процессоров имеют по крайней мере два этих режима. Так, в режиме супервизора программа может выполнять все действия и иметь доступ по любым адресам, тогда как в пользовательском режиме должны быть ограничения, с тем чтобы обнаруживать и пресекать запрещенные действия, перехватывая их и передавая управление супервизору ОС. Часто в пользовательском режиме запрещается выполнение команд ввода/вывода и некоторых других, чтобы гарантировать, что только ОС выполняет эти операции. Можно сказать, что эти два режима имеют разные уровни привилегий.
В микропроцессорах i80x86 имеются не два, а четыре уровня привилегий. Часто уровни привилегий называют кольцами защиты, поскольку это иногда помогает объяснить принцип действия самого механизма; поэтому говорят, что некоторый программный модуль «исполняется в кольце защиты с таким-то номером». Для указания уровня привилегий используются два бита, поэтому код 00 обозначает самый высший уровень, а код 11(2) (=3) — самый низший. Самый высокий уровень привилегий предназначен для операционной системы (прежде всего, для ядра ОС), самый низкий — для прикладных задач пользователя. Промежуточные уровни привилегий введены для большей свободы системных программистов в организации надежных вычислений при создании ОС и иного системного ПО. Предполагалось, что уровень с номером (кодом) 1 может быть использован, например, для системного сервиса — программ обслуживания аппаратуры, драйверов, работающих с портами ввода/вывода. Уровень привилегий с кодом 2 может быть использован для создания пользовательских интерфейсов, систем управления базами данных и т. п., то есть для реализации специальных системных функ ций, которые по отношению к супервизору ОС ведут себя как обычные приложения. Так, например, система OS/2 использует три уровня привилегий: с нулевым уровнем привилегий исполняется код самой ОС, на втором уровне исполняются системные процедуры подсистемы ввода/вывода, на третьем уровне исполняются прикладные задачи пользователей. Однако чаще всего на практике используются только два уровня — нулевой и третий. Таким образом, упомянутый режим супервизора для микропроцессоров i80x86 соответствует выполнению кода с уровнем привилегий 0 (его обозначают так: PL0 (privilege level). Подводя итог, можно констатировать, что именно уровень привилегий задач определяет, какие команды в них можно использовать и какое подмножество сегментов и/или страниц в их адресном пространстве они могут обрабатывать.
Основными системными объектами, которыми манипулирует процессор при работе в защищенном режиме, являются дескрипторы. Дескрипторы сегментов содержат информацию об уровне привилегии соответствующего сегмента кода или данных. Уровень привилегии исполняющейся задачи определяется значением поля привилегии, находящегося в дескрипторе ее текущего кодового сегмента. Напомним, что в каждом дескрипторе сегмента (см. рис.3.3) имеется поле DPL в байте прав доступа, которое и определяет уровень привилегии связанного с ним сегмента. Таким образом, поле DPL текущего сегмента кода становится полем CPL. При обращении к какому-нибудь сегменту в соответствующем селекторе указывается запрашиваемый уровень привилегий (requested privilege level) (см. рис. 3.4).
В пределах одной задачи используются сегменты с различным уровнем привилегии и в определенные моменты времени выполняются или обрабатываются сегменты с соответствующими им уровнями привилегии. Механизм проверки привилегий работает в ситуациях, которые можно назвать межсегментными переходами (обращениями). Это доступ к сегменту данных или стековому сегменту, межсегментные передачи управления в случае прерываний (и особых ситуаций), при использовании команд CALL, JMP, INT, IRET, RET. В таких межсегментных обращениях участвуют два сегмента: целевой сегмент (к которому мы обращаемся) и текущий сегмент кода, из которого идет обращение.
Процессор сравнивает упомянутые значения CPL, RPL, DPL и на основе понятия эффективного уровня привилегий (EPL=max(RPL,DPL)) ограничивает возможности доступа к сегментам по следующим правилам, в зависимости от того, идет ли речь об обращении к коду или к данным.
При доступе к сегментам данным проверяется условие CPL?EPL. Нарушение этого условия вызывает так называемую особую ситуацию ошибки защиты и возникает прерывание. Уровень привилегии сегмента данных, к которому осуществляется обращение, должен быть таким же, как и текущий уровень, или меньше его. Обращение к сегменту с более высоким уровнем привилегии воспринимается как ошибка, так как существует опасность изменения данных с высоким уровнем привилегий в программе с низким уровнем привилегии. Доступ к данным с меньшим уровнем привилегии разрешается.
Если целевой сегмент является сегментом стека, то правило проверки имеет вид

В случае его нарушения также возникает исключение. Поскольку стек может использоваться в каждом сегменте кода и всего имеются четыре уровня привилегий кода, то используются и четыре стека. Сегмент стека, адресуемый регистром SS, должен иметь тот же уровень привилегий, что и текущий сегмент кода.
Правила для передачи управления, то есть когда осуществляется межсегментный переход с одного сегмента кода на другой сегмент кода, несколько сложнее. Если для перехода с одного сегмента данных на другой сегмент данных считается допустимым обрабатывать менее привилегированные сегменты, то передача управления из высоко привилегированного кода на менее привилегированный код должна контролироваться дополнительно. Другими словами, код операционной системы не должен доверять коду прикладных задач. И обратно, нельзя просто так давать задачам возможность исполнять высоко привилегированный код, хотя потребность в этом всегда имеется (ведь многие функции, в том числе и функции ввода/вывода, считаются привилегированными и должны выполняться только самой ОС). Для реализации возможностей передачи управления в сегменты кода с иными уровнями привилегий введен механизм шлюзования, который мы вкратце рассмотрим ниже. Итак, если DPL=CPL, то переход в другой сегмент кода возможен. Более подробное рассмотрение затронутых вопросов по замыслу авторов выходит за рамки настоящего учебника (для получения более детальных сведений по этому и некоторым другим вопросам особенностей архитектуры микропроцессоров i80x86 рекомендуется обратиться к материалам [1, 8]). Здесь мы рассмотрим только основные идеи.

Механизм шлюзов для передачи управления на сегменты кода с другими уровнями привилегий

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

Рис. 3.9. Механизм шлюзов для перехода на другой уровень привилегий

Помимо дескрипторов сегментов системными объектами, с которыми работает микропроцессор, являются специальные системные дескрипторы, названные шлюзами или вентилями. Главное различие между дескриптором сегмента и шлюзом вызова заключается в том, что содержимое дескриптора указывает на сегмент в памяти, а шлюз обращается к дескриптору. Другими словами, если дескриптор служит механизмом отображения памяти, то шлюз служит механизмом перенаправления.
Для получения доступа к более привилегированному коду задача должна обратиться к нему не непосредственно (путем указания дескриптора этого кода), а обращением к шлюзу этого сегмента (рис. 3.10).
В этом дескрипторе вместо адреса сегмента указываются селектор, позволяющий найти дескриптор искомого сегмента кода, и адрес (смещение назначения), с которого будет выполняться подчиненный сегмент, то есть полный 32-битный адрес. Формат дескриптора шлюза приведен на рис. 3.11. Адресовать шлюз вызова можно с помощью команды CALL. По существу, дескрипторы шлюзов вызова не являются дескрипторами (сегментов), но они могут быть расположены среди обычных дескрипторов в дескрипторных таблицах процесса. Смещение, указываемое в команде перехода на другой сегмент (FAR CALL), игнорируется, и фактически осуществляется переход на команду, адрес которой определяется через смещение из шлюза вызова. Этим гарантируется попадание только на разрешенные точки входа в подчиненные сегменты.

Рис. 3.10. Переход на сегмент более привилегированного кода
Рис. 3.11. Формат дескриптора шлюза

Введены следующие правила использования шлюзов:

  • значение DPL шлюза вызова должно быть больше или равно значению текущего уровня привилегий CPL;
  • значение DPL шлюза вызова должно быть больше или равно значению поля RPL селектора шлюза;
  • значение DPL шлюза вызова должно быть больше или равно значению DPL целевого сегмента кода;
  • значение DPL целевого сегмента кода должно быть меньше или равно значению текущего уровня привилегий CPL.

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

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

Изложенный коротко аппаратный механизм защиты по привилегиям оказывается довольно сложным и жестким. Однако поскольку все практические ситуации учесть в схемах микропроцессора невозможно, то при разработке процедур операционных систем и иного высоко привилегированного кода следует придерживаться приведенных ниже рекомендаций, заимствованных из книги [22].
Основной риск связан с передачей управления через шлюз вызова более привилегированной процедуре. Нельзя предоставлять вызывающей программе никаких преимуществ, вытекающих из-за временного повышения привилегий. Это замечание особенно важно для процедур нулевого уровня привилегий (PL0-процедур).
Вызывающая программа может нарушить работу процедуры, передавая ей «плохие» параметры. Поэтому целесообразно как можно раньше проконтролировать передаваемые процедуре параметры. Шлюз вызова сам по себе не проверяет значений параметров, которые копируются в новый стек, поэтому достоверность каждого передаваемого параметра должна контролировать вызванная процедура. Вот некоторые способы контроля передаваемых параметров.

  1. Следует проверять счетчики циклов и повторений на минимальные и максимальные значения.
  2. Необходимо проверить 8- и 16-битные параметры, передаваемые в 32-битных регистрах. Когда процедуре передается короткий параметр, его следует расширить со знаком или нулем для заполнения всего 32-битного регистра.
  3. Следует стремиться свести к минимуму время работы процессора с запрещенными прерываниями. Если процедуре требуется запрещать прерывания, необходимо, чтобы вызывающая программа не могла влиять на время нахождения процессора с запрещенными прерываниями (флажок IF=0).
  4. Процедура никогда не должна воспринимать как параметр код или указатель кода.
  5. В операциях процессора следует явно задавать состояние флажка направления DF для цепочечных команд.
  6. Заключительная команда RET или RETn в процедуре должна точно соответствовать полю счетчика WC шлюза вызова; при этом n = 4x, так как счетчик задает число двойных слов, а n соответствует байтам.
  7. Не следует применять шлюзы вызовов для функций, которым передается переменное число параметров (см. рекомендацию 6). При необходимости нужно воспользоваться счетчиком и указателем параметров.
  8. Функции не могут возвращать значения в стеке (см. рекомендацию 6), так как после возврата стеки процедуры и вызывающей программы находятся точно в таком состоянии, в каком они были до вызова.
  9. В процедуре следует сохранять и восстанавливать все сегментные регистры. Иначе, если какой-либо сегментный регистр привлекался для адресации данных, недоступных вызывающей программе, процессор автоматически загрузит в него пустой селектор.

Рекомендуется контролировать все обращения к памяти. Нетрудно представить себе, что РL3-программа передаст PL0-процедуре указатель селектор:смещение и запросит считывание или запись нескольких байтов по этому адресу. Типичным примером может служить процедура дискового ввода/вывода, которая воспринимает как параметр системный номер файла, счетчик байт и адрес, по которому записываются данные с диска. Хотя PL0-процедура имеет привилегии для производства такой операции, но у РL3-программы разрешения на это может не быть.

Система прерываний 32-разрядных микропроцессоров i80x86

В микропроцессорах семейства i80x86 система прерываний построена таким образом, чтобы, с одной стороны, обеспечить возможность создавать эффективные и надежные мультипрограммные операционные системы, которые должны функционировать в защищенном режиме, а с другой стороны — обеспечить возможность выполнять программы, разработанные для реального режима. Рассмотрим коротко оба режима.

Работа системы прерываний в реальном режиме работы процессора

В реальном режиме работы система прерываний использует понятие вектора прерывания. Термин «вектор прерываний» используется потому, что для указания адреса используется не одно значение, а два, то есть мы имеем дело не со скалярной величиной, а с «векторной».
Итак, каждый вектор прерываний состоит из 4 байтов или 2 слов: первые два содержат новое значение для регистра IP, а следующие два — новое значение регистра CS. Таблица векторов прерываний занимает 1024 байта. Таким образом, в ней может быть задано 256 векторов прерываний. В процессоре i8086 эта таблица располагается на адресах 00000H003FFH. Расположение этой таблицы в процессорах i80286 и старше определяется значением регистра IDTR — Interrupt Descriptor Table Register. При включении или сбросе процессора i80x86 этот регистр обнуляется. Однако при необходимости можно в регистре IDTR указать смещение и, таким образом, перейти на новую таблицу векторов прерываний.
Таблица векторов прерываний заполняется (инициализируется) при запуске системы, но в принципе может быть изменена или перемещена.
Каждый вектор прерывания имеет свой номер, называемый номером прерывания, который указывает его место в таблице. Этот номер, помноженный на четыре (сдвиг на два разряда влево и заполнение освободившихся битов нулями), и сложенный с содержимым регистра IDTR, дает абсолютный адрес первого байта вектора в оперативной памяти.
Подобно вызову процедуры, прерывание заставляет микропроцессор сохранить в стеке информацию для последующего возврата, а затем перейти к группе команд, адрес которых определяется вектором прерывания. Таким образом, прерывание вызывает косвенный переход к своей подпрограмме обработки за счет получения ее адреса из вектора прерывания.
В IBM PC, как и в других вычислительных системах, прерывания бывают двух видов: внутренние и внешние.
Внутренние прерывания, как мы уже знаем, возникают в результате работы процессора. Они возникают в ситуациях, которые нуждаются в специальном обслуживании, или при выполнении специальных инструкций — INT или INTO. Это следующие прерывания:

  • прерывание при делении на ноль; номер прерывания — 0;
  • прерывание по флагу TF (trap flag). В этом случае прерывание обычно используется специальными программами отладки типа DEBUG. Номер прерывания — 1;
  • инструкции INT (interrupt — выполнить прерывание с соответствующим номером) и INTO (interrupt if overflow — прерывание по переполнению). Эти прерывания называются программными.

В качестве операнда команды INT указывается номер прерывания, которое нужно выполнить, например INT 10H. Программные прерывания как средство перехода на соответствующую процедуру были введены для того, чтобы выполнение этой процедуры осуществлялось в привилегированном режиме, а не в обычном пользовательском.
Внешние прерывания возникают по сигналу какого-нибудь внешнего устройства. Существуют два специальных внешних сигнала среди входных сигналов процессора, при помощи которых можно прервать выполнение текущей программы и тем самым переключить работу центрального процессора. Это сигналы NMI (no mask interrupt, немаскируемое прерывание) и INTR (interrupt request, запрос на прерывание). Соответственно, внешние прерывания подразделяются на немаскируемые и маскируемые.
Маскируемые прерывания генерируются контроллером прерываний по заявке определенных периферийных устройств. Контроллер прерываний (его обозначение — i8259A) поддерживает восемь уровней (линий) приоритета; к каждому уровню «привязано» одно периферийное устройство. Маскируемые прерывания часто называют еще аппаратными прерываниями. В ПК, начиная с IBM PC AT, построенных на базе микропроцессора i80286, используются два контроллера прерываний i8259A; они соединяются каскадным образом. Схема последовательного соединения этих контроллеров изображена на рис. 3.12.
Таким образом, на IBM PC AT предусмотрено 15 линий IRQ (Interrupt Request), часть которых используется внутренними контроллерами системной платы, а остальные заняты стандартными адаптерами либо не используются. Ниже перечислены линии запроса на прерывание, которые мы приводим потому, что каждый специалист по вычислительной технике должен знать основные стандарты ПК. Итак, линии IRQ:

  • 0 — системный таймер;
  • 1 — контроллер клавиатуры;
  • 2 — сигнал возврата по кадру (EGA/VGA), на AT соединен с IRQ 9;
  • 3 — обычно COM2/COM4;
  • 4 — обычно COM1/COM3;
  • 5 — контроллер HDD (на первых компьютерах IBM PC XT), обычно свободен на IBM PC AT и используется звуковой картой;
  • 6 — контроллер FDD;
  • 7 — LPT1, многими LPT-контpоллеpами не используется;
  • 8 — часы реального времени с автономным питанием (RTC — real time clock);
  • 9 — параллельна IRQ 2;
  • 10 — не используется, то есть свободно;
  • 11 — свободно;
  • 12 — обычно контроллер мыши типа PS/2;
  • 13 — математический сопроцессор;
  • 14 — обычно контроллер IDE0 (первый канал);
  • 15 — обычно контроллер IDE1 (второй канал).

Как известно, прерывания могут быть инициированы внешним устройством ПК или специальной командой прерывания из программы. В любом случае, если прерывания разрешены, то выполняется следующая процедура:

  1. В стек помещается регистр флагов PSW.
  2. Флаг включения/выключения прерываний IF и флаг трассировки TF, находящиеся в регистре PSW, обнуляются для блокировки других маскируемых прерываний и исключения пошагового режима исполнения команд.
  3. Значения регистров CS и IP сохраняются в стеке вслед за PSW.
  4. Вычисляется адрес вектора прерывания, и из вектора, соответствующего номеру прерывания, загружаются новые значения IP и CS.

Когда системная подпрограмма принимает управление, она может снова разрешить маскируемые прерывания командой STI (set interrupt flag, установить флаг прерываний), которая переводит флаг IF в состояние 1, что разрешает микропроцессору вновь реагировать на прерывания, инициируемые внешними устройствами, поскольку стековая организация позволяет вложение прерываний друг в друга.
Закончив работу, подпрограмма обработки прерывания должна выполнить инструкцию IRET (interrupt return), которая извлекает из стека три 16-битовых значения и загружает их в указатель команд IP, регистр сегмента команд CS и регистр PSW соответственно. Таким образом, процессор сможет продолжить работу с того места, где он был прерван.
В случае внешних прерываний процедура перехода на подпрограмму обработки прерывания дополняется следующими шагами:

  1. Контроллер прерываний получает заявку от определенного периферийного устройства и, соблюдая схему приоритетов, генерирует сигнал INTR (interrupt request), который является входным для микропроцессора.
  2. Микропроцессор проверяет флаг IF в регистре PSW. Если он установлен в 1, то переходим к шагу 3. В противном случае работа процессора не прерывается. Часто говорят, что прерывания замаскированы, хотя правильнее говорить, что они отключены. Маскируются (запрещаются) отдельные линии запроса на прерывания посредством программирования контроллера прерываний.
  3. Микропроцессор генерирует сигнал INTA (подтверждение прерывания). В ответ на этот сигнал контроллер прерывания посылает по шине данных номер прерывания. После этого выполняется описанная нами ранее процедура передачи управления соответствующей программе обработки прерывания.

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

Работа системы прерываний в защищенном режиме работы процессора

В защищенном режиме работы система прерываний действует совершенно иначе. Прежде всего, система прерываний микропроцессора i80x86 при работе в защищенном режиме вместо таблицы векторов, о которой мы говорили выше, имеет дело с таблицей дескрипторов прерываний (IDT, interrupt descriptor table). Дело здесь не столько в названии таблицы, сколько в том, что таблица IDT представляет собой не таблицу с адресами обработчиков прерываний, а таблицу со специальными системными структурами данных (дескрипторами), доступ к которой со стороны пользовательских (прикладных) программ невозможен. Только сам микропроцессор (его система прерываний) и код операционной системы могут получить доступ к этой таблице, которая представляет собой специальный сегмент, адрес и длина которого содержатся в регистре IDTR (см. рис. 3.2). Этот регистр аналогичен регистру GDTR в том отношении, что он инициализируется один раз при загрузке системы. Интересно заметить, что в реальном режиме работы регистр IDTR так же указывает адрес таблицы прерываний, но при этом, как и в процессоре i8086, каждый элемент таблицы прерываний (вектор) занимает всего 4 байта и содержит 32-битный адрес в формате селектор:смещение (CS:IP). Начальное значение этого регистра равно нулю, но в него можно занести и другое значение. В этом случае таблица векторов прерываний будет находиться в другом месте оперативной памяти. Естественно, что перед тем, как это сделать (занести в регистр IDTR новое значение), необходимо подготовить саму таблицу векторов. В защищенном режиме работы загрузку регистра IDTR может произвести только код с максимальным уровнем привилегий.
Каждый элемент в таблице дескрипторов прерываний, о которой мы говорим уже в защищенном режиме, представляет собой 8-байтовую структуру, более похожую на дескриптор шлюза (gate), нежели на дескриптор сегмента.
Как мы уже знаем, в зависимости от причины прерывания процессор автоматически индексирует таблицу прерываний и выбирает соответствующий элемент, с помощью которого и осуществляется перенаправление в исполнении кода, то есть передача управления на обработчик прерывания. Однако таблица IDT содержит только шлюзы, а не дескрипторы сегментов кода, поэтому фактически получается косвенная адресация, но с использованием рассмотренного ранее механизма защиты с помощью уровней привилегии. Благодаря этому пользователи уже не могут сами изменить обработку прерываний, которая предопределяется системным программным обеспечением.
Дескриптор прерываний может принадлежать к одному из трех типов:

  • коммутатор прерывания (interrupt gate);
  • коммутатор перехвата (trap gate);
  • коммутатор задачи (task gate).

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

Обработка прерываний в контексте текущей задачи

Рассмотрим рис. 3.13, поясняющий обработку прерывания в контексте текущей задачи. При возникновении прерывания процессор по номеру прерывания индексирует таблицу IDT, то есть адрес соответствующего коммутатора определяется путем сложения содержимого поля адреса в регистре IDTR и номера прерывания, умноженного на 8 (справа к номеру прерывания добавляются три нуля). Полученный дескриптор анализируется, и если его тип соответствует коммутатору trap gate или коммутатору interrupt gate, то выполняются следующие действия.

  1. В стек на уровне привилегий текущего сегмента кода помещаются:
    • значения SS и SP, если уровень привилегий в коммутаторе выше уровня привилегий ранее исполнявшегося кода;
    • регистр флагов EFLAGS;
    • регистры CS и IP.
  2. Если рассматриваемому прерыванию соответствовал коммутатор interrupt gate, то запрещаются прерывания (флаг IF:=0 в регистре EFLAGS). В случае коммутатора trap gate флаг прерываний не сбрасывается и обработка новых прерываний на период обработки текущего прерывания тем самым не запрещается.
  3. Поле селектора из дескриптора прерываний используется для индексирования таблицы дескрипторов задачи. Дескриптор сегмента заносится в теневой регистр, а смещение относительно начала нового сегмента кода определяется полем смещения из дескриптора прерывания.

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

Обработка прерываний с переключением на новую задачу

Совершенно иначе осуществляется обработка прерываний в случае, если дескриптором прерываний является коммутатор задачи. Формат коммутатора задачи (task gate) отличается от коммутаторов interrupt gate и trap gate, прежде всего, тем, что в нем вместо селектора сегмента кода, на который передается управление, указывается селектор сегмента состояния задачи (TSS). Это иллюстрируется с помощью рис. 3.14. В результате осуществляется процедура перехода на новую задачу с полной сменой контекста, ибо сегмент состояния задачи полностью определяет новое виртуальное пространство и адрес начала программы, а текущее состояние прерываемой задачи аппаратно (по микропрограмме микропроцессора) сохраняется в ее собственном TSS.

Рис. 3.14. Схема передачи управления при прерывании с переключением на новую задачу

При этом происходит полное переключение на новую задачу с вложением, то есть выполняются следующие действия:

  1. Сохраняются все рабочие регистры процессора в текущем сегменте TSS, базовый адрес этого сегмента берется в регистре TR (см. раздел «Адресация в 32-разрядных микропроцессорах i80x86 при работе в защищенном режиме»).
  2. Текущая задача отмечается как занятая.
  3. По селектору из Task Gate выбирается новый TSS (поле селектора помещается в регистр TR) и загружается состояние новой задачи. Напомним, что загружаются значения регистра LDTR, EFLAGS, восемь регистров общего назначения, указатель команды регистр EIP и шесть сегментных регистров.
  4. Устанавливается бит NT (next task).
  5. В поле обратной связи TSS помещается селектор прерванной задачи.
  6. Значения CS:IP, взятые из нового TSS, позволяют найти и выполнить первую команду обработчика прерывания.

Таким образом, коммутатор task gate дает указание процессору произвести переключение задачи, и обработка прерывания осуществляется под контролем отдельной внешней задачи. В каждом сегменте TSS имеется селектор локальной дескрипторной таблицы LDT, поэтому при переключении задачи процессор загружает в регистр LDTR новое значение. Это позволяет обратиться к сегментам кода, которые абсолютно не пересекаются с сегментами кода любых других задач, поскольку именно локальные дескрипторные таблицы обеспечивают эффективное изолирование виртуальных адресных пространств. Новая задача начинает свое выполнение на уровне привилегий, определяемом полем RPL нового содержимого регистра CS, которое загружается из сегмента TSS. Достоинством этого коммутатора является то, что он позволяет сохранить все регистры процессора с помощью механизма переключения задач, тогда как коммутаторы trap gate и interrupt gate сохраняют только содержимое регистров IFLAGS, CS и IP и сохранение других регистров возлагается на программиста, разрабатывающего соответствующую программу обработки прерывания.
Справедливости ради следует признать, что, несмотря на возможности коммутатора task gate, разработчики современных операционных систем достаточно редко его используют, поскольку переключение на другую задачу требует существенно больших затрат времени, а полное сохранение всех рабочих регистров часто и не требуется. В основном обработку прерываний осуществляют в контексте текущей задачи, так как это приводит к меньшим накладным расходам и повышает быстродействие системы.

Контрольные вопросы и задачи

Вопросы для проверки
  1. Как в реальном режиме работы микропроцессоров i80x86 осуществляется преобразование виртуального адреса в физический?
  2. Какие механизмы виртуальной памяти используются в защищенном режиме работы микропроцессоров i80x86?
  3. Для чего в микропроцессоры i80x86 введен регистр-указатель задачи TR? Какой он разрядности?
  4. Как в микропроцессорах i80x86 реализована поддержка сегментного способа организации виртуальной памяти?
  5. Что понимается под термином «линейный адрес»? Как осуществляется преобразование линейного адреса в физический? А может ли линейный адрес быть равным физическому?
  6. Что дало введение двухшаговой страничной трансляции в механизме страничного способа реализации виртуальной памяти? Как разработчики микропроцессора i80386 решили проблему замедления доступа к памяти, которое при двухшаговом преобразовании адресов очень существенно?
  7. Что дало введение виртуального режима? Как в этом режиме осуществляется вычисление физического адреса?
  8. Что имеется в микропроцессорах i80x86 для обеспечения защиты адресного пространства задач?
  9. Что такое «уровень привилигий»? Сколько уровней привилегий имеется в микропроцессорах i80x86? Для каких целей введено такое количество уровней привилегий?
  10. Что такое текущий уровень привилегий? Что такое эффективный уровень привилегий?
  11. Объясните правила работы с уровнями привилегий для различных типов сегментов.
  12. Поясните механизм шлюзования: для чего он предназначен, как осуществляется передача управления на сегменты кода с другими уровнями привилегий.
  13. Расскажите о работе системы прерываний микропроцессоров i80x86 в реальном режиме.
  14. В чем заключаются основные принципиальные отличия работы системы прерываний микропроцессоров i80x86 в защищенном режиме по сравнению с реальным режимом?
  15. Как осуществляется переход на программу обработки прерываний, если дескриптор прерываний является коммутатором прерываний?
  16. Как осуществляется переход на программу обработки прерываний, если дескриптор прерываний является коммутатором перехвата?
  17. Как осуществляется переход на программу обработки прерываний, если дескриптор прерываний является коммутатором задачи?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *