§ Подготовка

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

§ Такт 1. Считывание modrm байта

На первом такте при получении байта modrm на входе в i_data, происходит его первичная расшифровка:
1tmodrm: begin
2
3    modrm   <= i_data;
4    ip      <= ip + 1'b1;
5
6    // Левый операнд (op1) регистр
7    case (dir ? i_data[5:3] : i_data[2:0])
8
9        3'b000: op1 <= size ? ax : ax[ 7:0];
10        3'b001: op1 <= size ? cx : cx[ 7:0];
11        3'b010: op1 <= size ? dx : dx[ 7:0];
12        3'b011: op1 <= size ? bx : bx[ 7:0];
13        3'b100: op1 <= size ? sp : ax[15:8];
14        3'b101: op1 <= size ? bp : cx[15:8];
15        3'b110: op1 <= size ? si : dx[15:8];
16        3'b111: op1 <= size ? di : bx[15:8];
17
18    endcase
19
20    // Правый операнд (op2) регистр
21    case (dir ? i_data[2:0] : i_data[5:3])
22
23        3'b000: op2 <= size ? ax : ax[ 7:0];
24        3'b001: op2 <= size ? cx : cx[ 7:0];
25        3'b010: op2 <= size ? dx : dx[ 7:0];
26        3'b011: op2 <= size ? bx : bx[ 7:0];
27        3'b100: op2 <= size ? sp : ax[15:8];
28        3'b101: op2 <= size ? bp : cx[15:8];
29        3'b110: op2 <= size ? si : dx[15:8];
30        3'b111: op2 <= size ? di : bx[15:8];
31
32    endcase
33end
Что здесь по сути, делается:
  • Читается i_data и записывается в регистр modrm, который потребуется позже для различных действий.
  • Поскольку байт прочитан, увеличивается ip++
  • Читается левый операнд, регистровый, даже если далее он будет операндом из памяти. Если dir=0, то операнды считываются в порядке reg/mem, reg; если dir=1, то в порядке reg, reg/mem. В качестве reg/mem выступает либо регистр, либо память, это зависит от того, какой будет mod (биты [7:6] в байте), ну а в качестве reg - только регистр
Здесь 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-части.
1case (i_data[2:0])
2
3    3'b000: ea <= bx + si;
4    3'b001: ea <= bx + di;
5    3'b010: ea <= bp + si;
6    3'b011: ea <= bp + di;
7    3'b100: ea <= si;
8    3'b101: ea <= di;
9    3'b110: ea <= ^i_data[7:6] ? bp : 1'b0;
10    3'b111: ea <= bx;
11
12endcase
Эффективный адрес — это указатель в память, откуда необходимо прочитать операнд (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].
1if (!segover && (i_data[2:1] == 2'b01 || (^i_data[7:6] && i_data[2:0] == 3'b110)))
2    segment <= ss;
Довольно важный момент, кстати. Когда опкод не префиксирован сегментным префиксом ранее (segover == 0), то если для индексного регистра используется bp, это значит, что данные по умолчанию будут прочитаны с сегмента ss: а не ds:, будет выбран сегмент по умолчанию.
Таких вариантов всего 2:
  • Когда [2:0]=010 или 011, что сокращается до i_data[2:1] == 2'b01
  • И когда mod=01/10 и [2:0]=110
Почему не mod=00? Потому что там занято считыванием непосредственного адреса, который по умолчанию берется как сегмент ds:.
Продвигаемся дальше, ведь обработка первого такта все еще не закончена. Теперь необходимо принять решение о том, куда двинется, к какой фазе исполнения, выполнение инструкции.
1case (i_data[7:6])
2
3    2'b00: begin
4
5        // Читать +disp16
6        if (i_data[2:0] == 3'b110) t <= tmodrm2;
7        // Сразу читать операнды из памяти
8        else begin t <= tmodrmf; src <= 1'b1; end
9
10    end
11    2'b01: t <= tmodrm1;
12    2'b10: t <= tmodrm2;
13    2'b11: t <= exec;
14
15endcase
Рассмотрим каждый.
  • mod=00, если читаем 16-битное значение, то переход к фазе tmodrm2 (чтение и прибавление +2 байта)
  • mod=00, в любом другом случае, кроме 110, переходим сразу к чтению операнда в op1/op2 (фаза tmodrmf), при этом выбирая новый источник памяти src
  • mod=01, переход к чтение 8-битного смещения (знаковое 8-битное число)
  • mod=10, и тоже переход к чтению 16 битного смещения, при этом в ea будет не 0, а некоторый регистр или сумма двух регистров, которые являются индексом, указателем в память, или, эффективным адресом

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

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

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

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

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

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

Исходные коды к этой статье находятся по ссылке.