§ Описание флагов
Что такое компьютер? Само по себе слово "компьютер" произошло от слова "compute" — вычислять, а значит, основная задача компьютера является вычисление, и, на самом деле, так оно и есть. Основная нагрузка происходит как раз на АЛУ — арифметико-логическое устройство.Любые, даже самые сложные, операции, вроде нахождения косинуса или арктангенса, можно разбить на серию более мелких, сведя вообще все операции вычисления к сложению. Да, это слишком примитивно, но вместе с тем, основными элементарными операциями в x86 являются как раз 8 функции:
- ADD, ADC - сложение двух операндов без переноса и с учетом переноса
- SUB, SBB - вычитание одного из другого с переносом (sbb) и без переноса (sub)
- CMP - аналогично SUB, но результат вычисления не сохраняется, только обновляются флаги
- XOR - логическое битовое исключающее или
- AND - логическое битовое "и"
- OR - логическое битовое "или"
11 10 9 8 | 7 6 5 4 3 2 1 0
O D I T | S Z - A - P - C
В регистрах 5 и 3 находятся нули, вероятно, в силу исторических причин, поскольку флаги расположены почти что идентично тем, что в z80 (хотя я в этом не уверен, что из-за этого), а в бите 1 регистра флага всегда установлена 1.Что означают каждый флаг?
- C — carry, флаг, который устанавливается в 1, когда результат предыдущей инструкции превысил максимальное значение. К примеру, если был добавлен 255+1 в 8-битном регистре, что дает результат 0, но флаг C=1 говорит о том, что только что был перенос
- P — parity, флаг четности, который устанавливается в случае, если младшие 8 бит результата — четные, или количество единиц — четно, флаг этот весьма устаревший и почти никогда нигде не применяется и нужен лишь для обеспечения совместимости с возможным старым ПО. Говорят, что он был нужен для приема данных с внешнего мира, считать биты четности. Но это давно уже все устарело
- A — aux, полуперенос из разряда 3 в разряд 4, аналогично предыдущему флагу P, он устарел, применяясь только для инструкции коррекции DAA, DAS, AAA, AAS. Применялся для двоично-десятичной логики, но сейчас это стало настолько неактуально, что сами инструкции, которые работают с этим флагом, вообще удалили из x86-64
- Z — zero, ставится в 1, если результат выполнения АЛУ был нуль
- S — sign, всегда копируется из старшего бита результата, 7-го, 15-го, 31-го или 63-го, в зависимости от того, с какой разрядностью была исполнена инструкция
- T — trap, если установлен в 1, то после исполнения каждой инструкции вызывается int 1, нужен был раньше для отладчиков, сейчас уже устарел
- I — interrupt, при установленном =1, разрешается вызов прерываний во время исполнения кода
- D — direction, нужен для работы строковых инструкции, если D=1, то строка идет вниз, иначе вверх, то есть по инкременту
- O — overflow, самый сложный флаг из всех, который ставится в 1, если произошло знаковое переполнение, к примеру, если вычесть из -128 единицу (в 8-битном числе), то должно получиться -129, но на самом деле получится +127. Поскольку получится неправильный результат со знаками, говорят, что произошло знаковое переполнение.
§ Разработка АЛУ
Приступаем к созданию кода на верилоге. Считаю, что будет нелишним сначала объявить именованные константы для положения флагов в регистре флагов:1localparam 2 CF = 0, PF = 2, AF = 4, ZF = 6, SF = 7, 3 TF = 8, IF = 9, DF = 10, OF = 11;Например, CF находится в позиции 0, OF - в 11. В этой статье константы не понадобятся, но пускай будут объявлены. А теперь нужно объявить константы номеров функции АЛУ:
1localparam 2 alu_add = 3'h0, alu_or = 3'h1, 3 alu_adc = 3'h2, alu_sbb = 3'h3, 4 alu_and = 3'h4, alu_sub = 3'h5, 5 alu_xor = 3'h6, alu_cmp = 3'h7;Именно в таком порядке и никаком ином находятся номера функции АЛУ. Это касается не только положения в опкодах, но и специальных случаях с участием байта modrm.
Объявления сделаны, а теперь и сами вычисления:
1wire [16:0] alu_r = 2 3 alu == alu_add ? op1 + op2 : 4 alu == alu_or ? op1 | op2 : 5 alu == alu_adc ? op1 + op2 + flags[CF] : 6 alu == alu_sbb ? op1 - op2 - flags[CF] : 7 alu == alu_and ? op1 & op2: 8 alu == alu_xor ? op1 ^ op2: 9 op1 - op2; // sub, cmpЗдесь реализован мультиплексор, на вход которого подаются различные операции (16 битные вычисления), и в качестве провода выбора выступает 3-х битный регистр alu. Поскольку sub и cmp выполняют одинаковую функцию, здесь они объединены.
То что alu_r не является 16-битным, а 17-битным, важно, потому что именно в 17 бит и находится тот самый флаг CF при сложении и вычитании. Создадим провод
alu_top
, он будет содержать старший бит, который зависит от текущего size, потому что флаги будут меняться в зависимости от того, какая в данный момент битность операндов, 8 или 16 бит.1wire [ 3:0] alu_top = size ? 4'hF : 3'h7;Важным также является выбор группы для некоторых функции:
1wire is_add = alu == alu_add || alu == alu_adc; 2wire is_lgc = alu == alu_xor || alu == alu_and || alu == alu_or;Если is_add=1, то текущая функция АЛУ — сложение, соответственно, тогда is_lgc=0. Если же is_add=0 и is_lgc = 1, это значит, что в данный момент выбрана группа логических операциях, ну и при is_add=0 и is_lgc=0 — группа вычитаний.
Рассмотрим теперь вычисления флага CF:
1wire alu_cf = alu_r[alu_top + 1'b1];Флаг будет равен либо 9-му биту alu_r[8], либо 17 alu_r[16], для этого как раз и используется провод alu_top.
1wire alu_af = op1[4] ^ op2[4] ^ alu_r[4];Почему флаг AF равен XOR между 4-ми битами первого, второго операнда и результат, можно узнать из этой статьи.
1wire alu_sf = alu_r[alu_top];Флаг SF устанавливается тривиально, либо копируя бит alu_r[7], либо alu_r[15] — то есть, знаковый бит.
1wire alu_zf = size ? ~|alu_r[15:0] : ~|alu_r[7:0];Вот тут интересная хитрость. Когда size=1, то производится побитовое "ИЛИ" над всеми младшими 16 битами результата. Если там 0, то с помощью ~ это становится 1, сигнализируя о наличии нуля, и если хоть одна единица появится в одном из битов, то статус разрушается и zf превращается в 0. Аналогично с 8-битным результатом при size=0.
1wire alu_pf = ~^alu_r[7:0];С четностью так же как и с zero, но только примеряется не "OR", а "XOR". Эта операция проводит пере-xor-ивание между всеми битами, и при четном количестве единиц, получает 0, соответственно, с помощью инверсии ~ переворачивая в 1, и наоборот.
Пришло время для самого сложного вычисления overflow:
1wire alu_of = (op1[alu_top] ^ op2[alu_top] ^ is_add) & (op1[alu_top] ^ alu_r[alu_top]);Знающие люди скажут, что ничего сложного нет и что логическая его схема проста, но смотря на эту конструкцию, сомневаешься.
Существуют 2 случая, если is_add = 0 (вычитание) и is_add = 1 (сложение). Алгоритм вычисления этого флага меняется в зависимости от того, после какой инструкции был он рассчитан. Если после ADD, ADC, то результат
op1[alu_top] ^ op2[alu_top]
инвертируется, если после SUB,SBB,CMP, то нет.И теперь, зная все флаги, можно сформировать их в конечном итоге в такую конструкцию:
1wire [11:0] alu_f = { 2 3 /* OF */ alu_of & ~is_lgc, 4 /* DIT */ flags[10:8], 5 /* SF */ alu_sf, 6 /* ZF */ alu_zf, 7 /* 5 */ 1'b0, 8 /* AF */ alu_af & ~is_lgc, 9 /* 3 */ 1'b0, 10 /* PF */ alu_pf, 11 /* 1 */ 1'b1, 12 /* CF */ alu_cf & ~is_lgc 13};Флаги DIT при АЛУ-операциях не затрагиваются и потому просто копируются, биты 1,3,5 ставится 1,0,0 соответственно, а PF, ZF и SF записывается из результата непосредственно решение, а вот с OF, AF и CF не все так просто. С ними так — если вычисляется именно арифметическая инструкция, то is_lgc равен 0, то есть, alu_cf/pf/of присваиваются результату. В случае, если выполняется логическая инструкция (is_lgc=1), то cf, af и of устанавливаются в 0.
Как ни удивительно, но на этом моменте с АЛУ пока что все.
§ Исполнение инструкции
В этом материале я рассмотрю алгоритм работы инструкции АЛУ из набора опкодов 00-3F. Маски опкодов будут выбраны следующие:-
8'b00_xxx_0xx
АЛУ с байтом modrm -
8'b00_xxx_10x
с непосредственным операндом 8 или 16 бит
exec
процессор пришел с правильными значениями op1, op2, alu и size. Раз все правильно, то будет исполнена та схема, которую я описал выше, то есть, будут доступны результата alu_r и alu_f.1// <ALU> modrm 28'b00_xxx_0xx: begin 3 4 flags <= alu_f; 5 wb <= alu_r; 6 t <= alu == alu_cmp ? init : wback; 7 8endИтак, во флаги будет записано новое вычисленное значение alu_f (в любом случае), в регистр
wb
будет записан результат. Если номер функции АЛУ равен CMP, то инструкция завершает свою работу и выходит к фазе init, иначе, переходит к фазе wback — записи результата в левый операнд байта modrm.Регистр wb объявляется так:
1reg [15:0] wb = 16'h0000;То есть, это некий 16-разрядный регистр, который нужен для записи обратно в память или регистр. После того, как были вычислены операнды из байта modrm, в левый операнд и будет записано новое полученное значение. Дополним новые фазы:
1localparam 2 init = 1'b0, // Стадия подготовки инструкции 3... 4 wback = 4'hA, // Запись [wb,size,dir,modrm] 5 wback2 = 4'hB, 6 wback3 = 4'hC;Фаза wback отвечает за сохранение в регистр или в память (младший байт); wback2 записывает в память старший байт регистр wb; wback3 останавливает запись, выключает we и переходит либо к фазе init, либо указанной ранее фазе в регистре tnext:
1reg [ 5:0] tnext = 1'b0; 2reg [ 2:0] m = 1'b0;Здесь m - регистр для обозначения номер строки внутри фазы exec, поскольку инструкции обычно у меня не выполняются за один такт. Значение tnext всегда сбрасывается в init:
1init: begin 2 ... 3 tnext <= init;Приведу код фазы
wback
:1// Запись wb [size,dir,modrm] 2wback: begin 3 4 // Запись в регистр (reg) или reg:r/m-часть 5 if (dir || modrm[7:6] == 2'b11) begin 6 7 case (dir ? modrm[5:3] : modrm[2:0]) 8 9 3'b000: if (size) ax <= wb; else ax[ 7:0] <= wb[7:0]; 10 3'b001: if (size) cx <= wb; else cx[ 7:0] <= wb[7:0]; 11 3'b010: if (size) dx <= wb; else dx[ 7:0] <= wb[7:0]; 12 3'b011: if (size) bx <= wb; else bx[ 7:0] <= wb[7:0]; 13 3'b100: if (size) sp <= wb; else ax[15:8] <= wb[7:0]; 14 3'b101: if (size) bp <= wb; else cx[15:8] <= wb[7:0]; 15 3'b110: if (size) si <= wb; else dx[15:8] <= wb[7:0]; 16 3'b111: if (size) di <= wb; else bx[15:8] <= wb[7:0]; 17 18 endcase 19 20 t <= tnext; 21 22 end 23 // Если modrm указывает на память, записать первые 8 бит 24 else begin 25 26 t <= size ? wback2 : wback3; 27 o_data <= wb[7:0]; 28 we <= 1'b1; 29 30 end 31 32endЗдесь довольно много кода, но всего лишь 2 случая.
- Случай, в котором dir=1, а это значит, что левый операнд — заведомо регистр, номер регистра записан в modrm[5:3]
- Также, если dir=0, но mod=11, то это тоже регистр, но номер регистра записан в modrm[2:0] — что как раз и выбирается в case (dir ? modrm[5:3] : modrm[2:0])
- И второй случай, когда операнд находится в памяти. Если это так, то в любом случае на o_data ставится младший байт регистра wb, и ставится we=1
- Если size=0, то после записи операнда происходит переход на завершение записи в память:
1wback3: begin we <= 1'b0; t <= tnext; endИначе, при 16-битном операнде, переходит к wback2:
1// Запись старшего байта 2wback2: begin 3 4 ea <= ea + 1; 5 o_data <= wb[15:8]; 6 t <= wback3; 7 8endТам же записывается старший байт wb, сдвигается ea+1 и после этой микро-операции переходит к wback3, который и завершает работу фазы записи в память.
Таким образом, мы сделали сразу аж 32 инструкции! 8 функции на 4 разных типов операндов. Но поскольку будет очень удобно доделать АЛУ в этой секции опкодов в диапазоне 00-3F, то я доделаю.
18'b00_xxx_10x: case (m) 2 3 // Чтение 8 bit 4 0: begin 5 6 m <= size ? 1 : 2; 7 op1 <= ax; 8 op2 <= i_data; 9 ip <= ip + 1; 10 11 end 12 13 // Чтение 16 bit 14 1: begin op2[15:8] <= i_data; ip <= ip + 1; m <= 2; end 15 16 // Сохранение в AL/AX 17 2: begin 18 19 t <= init; 20 flags <= alu_f; 21 22 if (alu != alu_cmp) 23 begin if (size) ax <= alu_r; else ax[7:0] <= alu_r[7:0]; end 24 25 end 26 27endcaseЭто АЛУ, но с той разницей, что левый операнд либо регистр AL, либо AX, а правый - непосредственный операнд, идущий следом за опкодом.
Инструкция разделена на 3 строки. Как мы видим, при попадании в m=0 (линия 0), происходит формирование операндов op1, op2, второй операнд равен следующему байту после опкода. В зависимости от size, переход происходит либо на 1-ю линию (size=1), либо на 2-ю (size=0).
В линии 1 дочитывается старший байт в op2.
В линии 2 уже записывается результат во флаги и в al/ax, что зависит от size. Как и обычно, если функция равна CMP, то никакой результат в аккумулятор ax не пишется.
Вот теперь на этом можно сказать, все. В прикрепленном архиве я также теперь буду прикреплять файл map.html, в котором будет показано, в каком прогрессе находится код, какие инструкции были сделаны.