Лисья Нора

Оглавление


§ Подготовка

Ранее я рассказал, как получить опкод, но перед тем, как получить и считать байт modrm и операнды, решил сделать небольшие правки:
fetch: begin
 
ip <= ip + 1'b1;
opcode <= i_data;
size <= i_data[0];
dir <= i_data[1];
 
/* verilator lint_off CASEX */
casex (i_data)
 
// Обработка префиксов
...
// Основное АЛУ
8'b00_xxx_0xx: begin
 
t <= tmodrm;
alu <= i_data[5:3];
 
end
// Запись опкода или частичное исполнение
default: t <= exec;
 
endcase
end
Что здесь было изменено.
В данном случае, происходит запись в регистр alu значения из битов опкода [5:3], это важно для того, чтобы вычислять через АЛУ полученные опкоды, но этот вопрос я разберу в следующий раз.
Как мы видим, появились некоторые новые регистры. Кстати, еще момент, появился регистр src, который отвечает за выбор того, откуда будут браться из памяти данные. Он обнуляется в init секции:
src <= 1'b0;
Когда src=0, то указатель в память указывает на cs:ip, иначе на segment:ea, где ea – вычисленный эффективный адрес, который получается из как раз байта modrm.
assign address = src? {segment, 4'h0} + ea : {cs,4'h0} + ip;
Новые регистры:
reg src = 1'b0;
reg dir = 1'b0;
reg size = 1'b0;
reg [15:0] ea = 16'h0000;
reg [ 2:0] alu = 1'b0;
reg [ 7:0] modrm = 8'h00;
reg [15:0] op1 = 16'h0000;
reg [15:0] op2 = 16'h0000;
И еще, сразу же добавим новый список констант-фаз выполнения инструкции:
localparam
init = 1'b0, // Стадия подготовки инструкции
fetch = 1'b1, // Считывание опкода и префиксов
exec = 2'h2, // Исполнение инструкции
// Для считывания ModRM
tmodrm = 2'h3, // Считывание байта modrm
tmodrm1 = 3'h4, // +d8
tmodrm2 = 3'h5, // +d16 low
tmodrmh = 3'h6, // +d16 hi
tmodrmf = 4'h8, // Чтение операндов
tmodrmf2 = 4'h9; // Чтение операндов (high)

§ Считывание modrm байта

На первом такте при получении байта modrm на входе в i_data, происходит его первичная расшифровка:
tmodrm: begin
 
modrm <= i_data;
ip <= ip + 1'b1;
 
// Левый операнд (op1) регистр
case (dir ? i_data[5:3] : i_data[2:0])
 
3'b000: op1 <= size ? ax : ax[ 7:0];
3'b001: op1 <= size ? cx : cx[ 7:0];
3'b010: op1 <= size ? dx : dx[ 7:0];
3'b011: op1 <= size ? bx : bx[ 7:0];
3'b100: op1 <= size ? sp : ax[15:8];
3'b101: op1 <= size ? bp : cx[15:8];
3'b110: op1 <= size ? si : dx[15:8];
3'b111: op1 <= size ? di : bx[15:8];
 
endcase
 
// Правый операнд (op2) регистр
case (dir ? i_data[2:0] : i_data[5:3])
 
3'b000: op2 <= size ? ax : ax[ 7:0];
3'b001: op2 <= size ? cx : cx[ 7:0];
3'b010: op2 <= size ? dx : dx[ 7:0];
3'b011: op2 <= size ? bx : bx[ 7:0];
3'b100: op2 <= size ? sp : ax[15:8];
3'b101: op2 <= size ? bp : cx[15:8];
3'b110: op2 <= size ? si : dx[15:8];
3'b111: op2 <= size ? di : bx[15:8];
 
endcase
end
Что здесь по сути, делается:
Здесь reg/mem часть занимает биты [2:0], а reg-часть занимает биты [5:3].
Байт modrm состоит из 3 частей:
7 6 5 4 3 2 1 0
[ mod ] [ r/m ] [ reg ]
Если mod=11, то тогда в качестве операнда в r/m части выступает регистр.
Номер регистра указывается в 3-х битах. В зависимости от выбранного size, регистры будут разными
0 1 2 3 4 5 6 7
size=0 al cl dl bl ah ch dh bh
size=1 ax cx dx bx sp bp si di
В случае dir=0 левый операнд будет располагаться в [2:0] битах modrm, правый в [5:3], и наоборот.
Приведу пример. Необходимо закодировать операнды mov ax,sp. Выбираем dir=0, size=1. Байт modrm будет таким 11_100_000, поскольку слева будет 000 (ax), а справа 100 (sp).
Второй пример. Тот же самый порядок операндов можно закодировать следующим образом. Выбираем dir=1, size=1. Байт modrm 11_000_100. То есть, теперь левый операнд будет 000, и правый 100 – что аналогично ранее приведенному примеру.
Вычисление эффективного адреса происходит, основываясь только на r/m-части.
case (i_data[2:0])
 
3'b000: ea <= bx + si;
3'b001: ea <= bx + di;
3'b010: ea <= bp + si;
3'b011: ea <= bp + di;
3'b100: ea <= si;
3'b101: ea <= di;
3'b110: ea <= ^i_data[7:6] ? bp : 1'b0;
3'b111: ea <= bx;
 
endcase
Эффективный адрес – это указатель в память, откуда необходимо прочитать операнд (1 или 2 байта). Размер операнда задается так же, через size.
Этот адрес реально используется только тогда, когда mod != 11, то есть, вместо r/m части будет указатель на память. К примеру, mov [bx],cx будет кодироваться как dir=0, size=1 и байт modrm=00_001_111, где в битах [2:0] находится индексный регистр bx. Для mov cx,[bx] байт modrm останется точно таким же, но dir=1. Другими словами, в качестве операндов может приниматься либо 2 регистра, либо регистр и память. Оба операнда, указывающие на память, невозможны.
Теперь же к интересным особенностям. Как видно из кода сверху, для modrm[2:0]=110 эффективный адрес будет принимать значение bp в том случае, если mod=10 или mod=01 (кодируется через ^i_data[7:6] – обычный xor над двумя битами). В случае если mod=00 или mod=11 (все равно не используется), то эффективный адрес будет равен 0.
На самом деле же, эффективный адрес равен нулю только на первом такте, ибо для такого случая будет прочитан disp16, который добавляет +2 байта к нулю. Это получается прямое обращение к памяти, например mov ax,[$1234].
if (!segover && (i_data[2:1] == 2'b01 || (^i_data[7:6] && i_data[2:0] == 3'b110)))
segment <= ss;
Довольно важный момент, кстати. Когда опкод не префиксирован сегментным префиксом ранее (segover == 0), то если для индексного регистра используется bp, это значит, что данные по умолчанию будут прочитаны с сегмента ss: а не ds:, будет выбран сегмент по умолчанию.
Таких вариантов всего 2:
Почему не mod=00? Потому что там занято считыванием непосредственного адреса, который по умолчанию берется как сегмент ds:.
Продвигаемся дальше, ведь обработка первого такта все еще не закончена. Теперь необходимо принять решение о том, куда двинется, к какой фазе исполнения, выполнение инструкции.
case (i_data[7:6])
 
2'b00: begin
 
// Читать +disp16
if (i_data[2:0] == 3'b110) t <= tmodrm2;
// Сразу читать операнды из памяти
else begin t <= tmodrmf; src <= 1'b1; end
 
end
2'b01: t <= tmodrm1;
2'b10: t <= tmodrm2;
2'b11: t <= exec;
 
endcase
Рассмотрим каждый.

§ Чтение 8-битного disp

Основной и самый сложный код в чтении байта modrm, пройден. Теперь остались достаточно простые и детерминированные микрооперации.
tmodrm1: begin
 
t <= tmodrmf;
ip <= ip + 1'b1;
ea <= ea + {{8{i_data[7]}}, i_data[7:0]};
src <= 1'b1;
 
end
Фаза чтения 8-битного смещения, которая добавляет к ранее вычисленному ea знаковое значение. Заметим новую конструкцию {8{i_data[7]}}, которая означает копирование бита 7 из i_data на 8 разрядов, что по сути и является знакорасширением для итогового 16-битного сумматора. После определения конечного эффективного адреса, переключаем контекст src=1, прибавляем ip++ и переходим к чтению операндов в фазе tmodrmf.

§ Чтение 16-битного disp

В данном случае, на чтение 16-битного смещения потребуется 2 такта. Первый из них читает байт и добавляет его к эффективному адресу ea, причем, добавляет 8-битное значение к 16-битному, чтобы обеспечить перенос из бита 7 в бит 8, если он там будет.
tmodrm2: begin
 
t <= tmodrmh;
ip <= ip + 1'b1;
ea <= ea + i_data;
 
end
Второй такт добавляет уже непосредственно к старшему байту ea, тем самым образом за два такта получая 2-байтное смещение и складывая его.
tmodrmh: begin
 
t <= tmodrmf;
ip <= ip + 1'b1;
src <= 1'b1;
ea[15:8] <= ea[15:8] + i_data;
 
end
После считывания адреса, происходит переход к чтению операнда.

§ Чтение операнда

Это последний этап в выполнении считывания данных с байта modrm. Считывание операнда зависит от того, какой размер операнда выбран. Если выбран size=0, то прочитается в op1/op2 только 1 байт, иначе будет дочитан 2-й байт. Так выглядит первый такт считывания.
tmodrmf: begin
 
if (dir) op2 <= i_data; else op1 <= i_data;
if (size)
begin t <= tmodrmf2; ea <= ea + 1'b1; end
else begin t <= exec; end
 
end
При dir=1, как я и говорил ранее по тексту, в правый операнд (op2) записывается 8-битное значение, попутно очищая старший байт, поскольку что op1, что op2 – 16 битные операнды.
При size=1, будет дочитан старший байт, который находится в следующем байте после ea, то есть, ea+1. Если же size=0, то здесь делать нечего, надо выходить к exec – исполнению инструкции.
tmodrmf2: begin
 
if (dir) op2[15:8] <= i_data; else op1[15:8] <= i_data;
 
ea <= ea - 1'b1;
t <= exec;
 
end
А вот и самый последний из методов. Как и в прошлом методе, дочитываем старший байт в op1/op2, в зависимости от dir, но также вычитается из ea-1, чтобы в будущем он указывал на правильную позицию. Это потребуется для того, если инструкция будет записывать результат обратно в память (пример add [bx+$1234],ax).
Вот на этом можно сказать, все. Но без скриншота, конечно, не оставлю.
ch3_1.png