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

Уж сколько раз я делал этот код, что иногда мне кажется, что могу написать его с закрытыми глазами. И так и сяк делал, вообще как только не разрабатывал и перерабатывал его. И пришло время документально об этом сказать с нотариально заверенным скриншотом :-P
Первым делом, надо поправить кое-какие параметры при сбросе процессора:
1cs          <= 16'hF800; // 16'hF000
2eip         <= 16'h0000; // 16'hFFF0
3__segment   <= 80'h0000;
На самом деле, уже потом вместо cs:eip будут значения F000:FFF0, но, поскольку инструкция JMP segment:offset еще пока что далеко не реализована, то переход будет происходить сразу же к биосу, который будет 32 килобайтного размера, а не 64К, как это делают. Мне кажется, для биоса это будет вполне себе нормально, даже простой Бейсик поместится.
При декодировании инструкции, в fetch, нелишним будет добавить вот это:
1regn    <= in[2:0];
2t_next  <= fetch;
Где regn потребуется для будущих инструкции INC/DEC/PUSH r16 и других им подобных, а t_next будет необходим для того, чтобы процессор знал, куда переходить после исполнения очередной процедуры. По умолчанию, переход будет на fetch — на считывание новой инструкции.
Теперь, для того, чтобы процессор знал, куда перенаправлять исполнение, нужно дописать условие в fetch:
1casex (in)
28'b00xx_x0xx: begin t <= fetch_modrm; end
3endcase
Это — маска для опкодов, которые используются АЛУ, работающие с modrm-байтом. После получения этого опкода, процессор выполняет переход к фазе fetch_modrm.
1fetch_modrm: case (fn2)
20: begin
3
4    modrm   <= in;
5    eip     <= eip_next;
6    ea      <= 1'b0;
7
8    // Левый операнд
9    case (dir ? in[5:3] : in[2:0])
10    0: op1 <= size ? (opsize ? eax : eax[15:0]) : eax[ 7:0];
11    1: op1 <= size ? (opsize ? ecx : ecx[15:0]) : ecx[ 7:0];
12    2: op1 <= size ? (opsize ? edx : edx[15:0]) : edx[ 7:0];
13    3: op1 <= size ? (opsize ? ebx : ebx[15:0]) : ebx[ 7:0];
14    4: op1 <= size ? (opsize ? esp : esp[15:0]) : eax[15:8];
15    5: op1 <= size ? (opsize ? ebp : ebp[15:0]) : ecx[15:8];
16    6: op1 <= size ? (opsize ? esi : esi[15:0]) : edx[15:8];
17    7: op1 <= size ? (opsize ? edi : edi[15:0]) : ebx[15:8];
18    endcase
19
20    // Правый операнд
21    case (dir ? in[2:0] : in[5:3])
22    0: op2 <= size ? (opsize ? eax : eax[15:0]) : eax[ 7:0];
23    1: op2 <= size ? (opsize ? ecx : ecx[15:0]) : ecx[ 7:0];
24    2: op2 <= size ? (opsize ? edx : edx[15:0]) : edx[ 7:0];
25    3: op2 <= size ? (opsize ? ebx : ebx[15:0]) : ebx[ 7:0];
26    4: op2 <= size ? (opsize ? esp : esp[15:0]) : eax[15:8];
27    5: op2 <= size ? (opsize ? ebp : ebp[15:0]) : ecx[15:8];
28    6: op2 <= size ? (opsize ? esi : esi[15:0]) : edx[15:8];
29    7: op2 <= size ? (opsize ? edi : edi[15:0]) : ebx[15:8];
30    endcase
31
32    // 32-bit MODRM
33    if (adsize) begin /* Пока что ничего */ end
34    // 16-bit MODRM
35    else begin
36
37        case (in[2:0])
38        3'b000: ea[15:0] <= ebx + esi;
39        3'b001: ea[15:0] <= ebx + edi;
40        3'b010: ea[15:0] <= ebp + esi;
41        3'b011: ea[15:0] <= ebp + edi;
42        3'b100: ea[15:0] <= esi;
43        3'b101: ea[15:0] <= edi;
44        3'b110: ea[15:0] <= ^in[7:6] ? ebp : 1'b0;
45        3'b111: ea[15:0] <= ebx;
46        endcase
47
48        // Выбор сегмента по умолчанию
49        if (!override && (in[2:1] == 2'b01 || (^in[7:6] && in[2:0] == 3'b110)))
50            segment <= ss;
51
52        // Выбор решения
53        case (in[7:6])
54        2'b00: begin
55
56            // Читать +disp16
57            if (in[2:0] == 3'b110) fn2 <= 1;
58            // Сразу читать операнды из памяти
59            else begin
60
61                fn2 <= 4;
62                src <= 1'b1;
63                if (ignoreo) begin t <= exec; fn2 <= 0; end
64
65            end
66
67        end
68        2'b01: fn2 <= 3; // 8 bit
69        2'b10: fn2 <= 1; // 16 bit
70        2'b11: begin fn2 <= 0; t <= exec; end
71        endcase
72
73    end
74
75end
76endcase
Здесь достаточно крупный кусок кода, но он все еще не полный, потому что нет условия декодирования 32-х битного ModRM, о чем будет дальше рассказано.
Примерно такой же код рассматривался ранее, но здесь есть некоторые отличия, в первую очередь, при считывании операндов в виде регистров, теперь учитывается не просто 16-битные регистры, но и добавлены 32-х битные. Они активируются при наличии как size, так и opsize.
Первичное вычисление эффективного адреса никак не изменилось, разве что только при сложении используются младшие 16 бит для 32-х битных регистров, но по факту, это ничего не поменяет.
Выбор сегмента ss по умолчанию точно такой же, но только тут различие в том, что сегмент теперь 80-битный, а не 16-битный и копируется также его скрытая часть, которая потом понадобится для вычисления смещений в памяти в защищенном режиме, и в целом, в старших 64 битах хранится загруженная копия дескриптора из памяти GDT.
  • Когда в mod=2'b00, и если rm=3'b110, то тогда переход к чтению 16-битного смещения, иначе — переход 1) чтению операнда из памяти, 2) при установленном ignoreo, сразу к исполнению инструкции. Этот параметр нужен иногда, чтобы не читать лишние данные из памяти.
  • При mod=2'b01, переход к чтению 8-битного смещения (-128..127)
  • При mod=2'b10, переход к чтению 16-битного смещения (-32768..32767)
  • Ну и при mod=2'b11, сразу переход к исполнению инструкции, потому что из памяти ничего не будет загружаться
Чтение 16-битного смещения:
11: begin fn2 <= 2; ea <= ea + in; eip <= eip_next; end
22: begin
3
4    fn2      <=  adsize ? 8 : 4;
5    src      <= !adsize;
6    ea[31:8] <= ea[31:8] + in;
7    eip      <= eip_next;
8
9    if (ignoreo && !adsize) begin t <= exec; fn2 <= 0; end
10
11end
Первым делом, из шины данных считывается in и добавляется к ea. Стоит заметить, что добавляется прямо ко всему 32-х битному ea, потому что если ea был равен FFh, то при добавлении 1, он будет уже равен 100h — то есть, разряд перенесется из 7-го и 8-й бит.
Второй так дочитывает еще один байт, и добавляет к старшим 24-м битам, как и в прошлом такте, к 32-х битному числу.
Если adsize=1, то есть, выбрано 32-х битное смещение, то начиная с fn2=8, к ea дочитается еще 2 байта, при этом src останется равным 0, а не 1. Также, если задан параметр ignoreo и adsize=0, то чтение операндов из памяти производится не будет.
18: begin fn2 <= 9; ea[31:16] <= ea[31:16] + in; eip <= eip_next; end
29: begin
3
4    fn2         <= 4;
5    ea[31:24]   <= ea[31:24] + in;
6    src         <= 1'b1;
7    eip         <= eip_next;
8
9    if (ignoreo) begin t <= exec; fn2 <= 0; end
10
11end
В этом коде дочитываются старшие 2 байта для эффективного адреса, если все-таки, был переход с adsize=1.
И остался только случай считывания 8-битного смещения:
13: begin
2
3    fn2 <= 4;
4    ea  <= ea + {{24{in[7]}}, in};
5    src <= 1'b1;
6    eip <= eip_next;
7
8    if (ignoreo) begin t <= exec; fn2 <= 0; end
9
10end
Здесь все просто. При считывании байта из шины, он знакорасширяется до 32-х битного ea и либо переходит к чтению операнда, либо выходит к exec при установленном бите ignoreo.

§ Считывание операндов

Но получить эффективный адрес недостаточно, нужно прочитать то значение, которое находится по заданному эффективному адресу. В зависимости от заданного size и opsize, будут читаться разное количество байт во временную переменную wb:
  • size=0, 1 байт
  • size=1, opsize=0, 2 байта
  • size=1, opsize=1, 4 байта
Чтение первого байта:
14: begin
2
3    if (dir) op2 <= in; else op1 <= in;
4    if (size) begin fn2 <= 5; ea <= ea + 1; end
5    else      begin fn2 <= 0; t  <= exec; end
6
7end
Принцип работы.
  • Если dir=1, то результат из памяти записывается в правый операнд op2, иначе в левый op1. Старшая часть [31:8] очищается в 0.
  • Если size=1, то переходит к чтению 2-го байта, ea++; иначе выход к исполнению инструкции
15: begin
2
3    if (dir) op2[15:8] <= in; else op1[15:8] <= in;
4    if (opsize) begin fn2 <= 6; ea <= ea + 1; end
5    else        begin fn2 <= 0; ea <= ea - 1; t <= exec;  end
6
7end
Читается 2-й байт, запись в op1/op2 аналогично предыдущему.
  • При opsize=1, переход к дочитывания 3-го байта
  • Если нет, то тогда переход к исполнению, но ea--, чтобы снова указывал на то же место, где был изначально вычислен
И при size=1, opsize=1, происходит дочитывание 3-го и 4-го байта.
1// OPERAND-23:16
26: begin
3
4    fn2 <= 7; ea <= ea + 1;
5    if (dir) op2[23:16] <= in; else op1[23:16] <= in;
6
7end
8
9// OPERAND-31:24
107: begin
11
12    t  <= exec;
13    fn2 <= 0; ea <= ea - 3;
14    if (dir) op2[31:24] <= in; else op1[31:24] <= in;
15
16end
В 7-м такте переход осуществляется без всякого выбора, сразу к exec, и ea откатывается назад на 3 байта, возвращаясь в изначальное положение.

§ Чтение 32-битного modrm+sib

Вот это одна из довольно сложных вещей, с которыми мне пришлось сталкиваться при разработке эмулятора. Это новый вид адресации, который включается префиксом 67h (устанавливается adsize=1), и с помощью него можно адресовать 32-х битную память. Причем, вычисление адреса теперь намного более широкое и использовать можно все регистры, а не некоторые. Например:
1mov al, [eax]
2mov ax, [ebx+eax]
3mov eax, [esi+edi*2]
4mov sp, [esp-1]
32-х битный адрес состоит из 3 компонентов — регистра базы, индекса и смещения. Индекс можно умножать на 0,1,2,4 и 8. Смещение как 8-битное, так 32-х битное.
Новый метод адресации состоит из 2-х компонентов, байта modrm, как и ранее, в r/m части указывается либо смещение в память, либо регистр, и sib, который расширяет возможности адресации и позволяет как раз и использовать сумму базы + индекса.
Начнем разработку. В стадии fetch_modrm, где у меня был пропущен код, теперь его добавим:
1case (in[2:0])
23'b000: ea <= eax;
33'b001: ea <= ecx;
43'b010: ea <= edx;
53'b011: ea <= ebx;
63'b100: ea <= 0;
73'b101: ea <= ^in[7:6] ? ebp : 0;
83'b110: ea <= esi;
93'b111: ea <= edi;
10endcase
В этом фрагменте кода высчитывается первичный эффективный адрес, который можно задать из байт modrm. Как видно из кода, можно обратиться к адресу по eax, ecx, edx, ebx, ebp, esi и edi, кроме esp, поскольку, если его указать, будет дочитан байт SIB. Так что, в случае если нам нужен простой адрес + смещение, то его можно закодировать только лишь одним байтом modrm, без sib. Например такие:
1mov ax, [ebx]
2mov [esi-1], al
В следующем коде будет разбираться, что делать далее, основываясь на двух битах mod:
1case (in[7:6])
22'b00: begin
3
4    if      (in[2:0] == 3'b101) fn2 <= 1;  // DISP32
5    else if (in[2:0] == 3'b100) fn2 <= 10; // SIB
6    else begin
7
8        fn2 <= 4;
9        src <= 1'b1;
10        if (ignoreo) begin t <= exec; fn2 <= 0; end
11
12    end
13
14end
152'b01: fn2 <= in[2:0] == 3'b100 ? 10 : 3; // 8     bit | SIB
162'b10: fn2 <= in[2:0] == 3'b100 ? 10 : 1; // 16/32 bit | SIB
172'b11: begin fn2 <= 0; t <= exec; end
18endcase
Рассмотрим каждый случай:
  • mod=00, r/m=101, это переход к чтению 32-х битного смещения. При просмотре блока кода, ранее написанного, при mod=00, r/m=101 эффективный адрес ea устанавливался в 0, так что смещение будет прочитано без какой-либо базы, это прямое указание в память
  • mod=00, r/m=100, переходим к чтению байта SIB
  • mod=00, во всех остальных случаях, если ignoreo=0, сразу же переходит к чтению операнда из памяти, на который указывает ea
  • mod=01, r/m=100, читается SIB, иначе считывается 8-битное смещение, а оттуда сразу переход к чтению операнда из памяти
  • mod=10, r/m=100, читается SIB, иначе 32-х битное смещение
  • mod=11, ну а тут был прочтен регистр и ничего не происходит, кроме выхода к исполнению опкода
1if (!override && (^in[7:6] && in[2:0] == 3'b101))
2    segment <= ss;
Собственно, выбор сегмента по умолчанию простой, для mod=01 или mod=10, и при r/m=101 (ebp), выбирается сегмент ss.

§ Байт SIB

И вот пришло время перейти к чтению и разбору байта SIB. Начнем с того, что прочтем базу и индекс:
110: begin
2
3case (in[5:3])
43'b000: ea <= sib_base + (eax << in[7:6]);
53'b001: ea <= sib_base + (ecx << in[7:6]);
63'b010: ea <= sib_base + (edx << in[7:6]);
73'b011: ea <= sib_base + (ebx << in[7:6]);
83'b100: ea <= sib_base;
93'b101: ea <= sib_base + (ebp << in[7:6]);
103'b110: ea <= sib_base + (esi << in[7:6]);
113'b111: ea <= sib_base + (edi << in[7:6]);
12endcase
13
14end
При этом не забываем увеличить eip++
1eip <= eip_next;
Как видно из кода, в [7:6] находится параметр scale, который сдвигает на 0,1,2,3 бита влево. Сдвиг на 0 битов равноценно умножению на 1, на 1 бит - на 2, на 2 бита — умножение на 4 и 3 бита — это умножение на 8.
В битах [5:3] находится номер индексного регистра. Отметим, что индексный регистр в 100 — не существует, там по идее должен быть esp, но его там нет, поскольку esp может быть только в sib_base.
А где же определяется sib_base?
1wire [31:0] sib_base =
2    in[2:0] == 3'b000 ? eax :
3    in[2:0] == 3'b001 ? ecx :
4    in[2:0] == 3'b010 ? edx :
5    in[2:0] == 3'b011 ? ebx :
6    in[2:0] == 3'b100 ? esp :
7    in[2:0] == 3'b101 ? (^modrm[7:6] ? ebp : 1'b0) :
8    in[2:0] == 3'b110 ? esi : edi;
Он "висит" на проводе и выбирается из [2:0] бита байта SIB. В качестве базы можно указать регистр esp.
Здесь можно отметить, что выбор зависит от того, что было в mod части байта modrm. Если в mod=01 или mod=10, то в качестве базы выбирается регистр ebp, иначе 0. Получается так, что таким образом можно задавать просто индекс в таком виде например:
1mov eax, [ebx*2+1234]
То есть, есть индексный регистр, но без указания базы.
С вычислением базы и индексного регистра все, но к ним еще можно дочитать смещения:
1case (modrm[7:6])
22'b00: if (in[2:0] == 3'b101)
3       begin fn2 <= 1; end
4else   begin fn2 <= 4; src <= 1'b1; end
52'b01: begin fn2 <= 3; end // disp8
62'b10: begin fn2 <= 1; end // disp32
72'b11: begin fn2 <= 0; t <= exec; end
8endcase
Как обычно, рассмотрим каждый случай.
  • mod=00, при выборе в качестве базы 101, будет прочитано 32-х битное смещение, а в любом другом случае сразу же перейдет к чтению операндов из памяти
  • mod=01 и mod=10 читает 8-битное или 32-х битное смещение соответственно
  • mod=11 здесь вообще не будет никогда исполнен, но на всякий случай написал, что переходит к выполнению опкода
Поскольку в адресации могут использоваться ebp, то надо еще проверить выбор сегмента ss:
1if (!override && ((^modrm[7:6] && in[2:0] == 3'b101) || (in[5:3] == 3'b101)))
2    segment <= ss;
Сегмент будет выбран, если mod=01 или 10 и при этом база [2:0] будет равна 101 (ebp), либо если индексный регистр [5:3] равен ebp.
1if (modrm[7:6] == 2'b00 && in[2:0] != 3'b101 && ignoreo) begin t <= exec; fn2 <= 0; end
При ignoreo=1, не допустить переход к чтению операнда и выйти сразу к исполнению инструкции.
На этом пока что все. Я рассмотрел чтение байта modrm+sib и операндов, но в следующем материале начнем исполнять код.
Файлы проекта