§ Чтение 16-битного modrm
Уж сколько раз я делал этот код, что иногда мне кажется, что могу написать его с закрытыми глазами. И так и сяк делал, вообще как только не разрабатывал и перерабатывал его. И пришло время документально об этом сказать с нотариально заверенным скриншотом :-PПервым делом, надо поправить кое-какие параметры при сбросе процессора:
16'hF800; // 16'hF000 eip <= 16'h0000; // 16'hFFF0 __segment <= 80'h0000;cs <= На самом деле, уже потом вместо cs:eip будут значения F000:FFF0, но, поскольку инструкция JMP segment:offset еще пока что далеко не реализована, то переход будет происходить сразу же к биосу, который будет 32 килобайтного размера, а не 64К, как это делают. Мне кажется, для биоса это будет вполне себе нормально, даже простой Бейсик поместится.
При декодировании инструкции, в fetch, нелишним будет добавить вот это:
2:0]; t_next <= fetch;regn <= in[Где
regn
потребуется для будущих инструкции INC/DEC/PUSH r16
и других им подобных, а t_next будет необходим для того, чтобы процессор знал, куда переходить после исполнения очередной процедуры. По умолчанию, переход будет на fetch — на считывание новой инструкции.Теперь, для того, чтобы процессор знал, куда перенаправлять исполнение, нужно дописать условие в fetch:
casex (in) 8'b00xx_x0xx: begin t <= fetch_modrm; end endcaseЭто — маска для опкодов, которые используются АЛУ, работающие с modrm-байтом. После получения этого опкода, процессор выполняет переход к фазе fetch_modrm.
case (fn2) 0: begin modrm <= in; eip <= eip_next; ea <= 1'b0; // Левый операнд case (dir ? in[5:3] : in[2:0]) 0: op1 <= size ? (opsize ? eax : eax[15:0]) : eax[ 7:0]; 1: op1 <= size ? (opsize ? ecx : ecx[15:0]) : ecx[ 7:0]; 2: op1 <= size ? (opsize ? edx : edx[15:0]) : edx[ 7:0]; 3: op1 <= size ? (opsize ? ebx : ebx[15:0]) : ebx[ 7:0]; 4: op1 <= size ? (opsize ? esp : esp[15:0]) : eax[15:8]; 5: op1 <= size ? (opsize ? ebp : ebp[15:0]) : ecx[15:8]; 6: op1 <= size ? (opsize ? esi : esi[15:0]) : edx[15:8]; 7: op1 <= size ? (opsize ? edi : edi[15:0]) : ebx[15:8]; endcase // Правый операнд case (dir ? in[2:0] : in[5:3]) 0: op2 <= size ? (opsize ? eax : eax[15:0]) : eax[ 7:0]; 1: op2 <= size ? (opsize ? ecx : ecx[15:0]) : ecx[ 7:0]; 2: op2 <= size ? (opsize ? edx : edx[15:0]) : edx[ 7:0]; 3: op2 <= size ? (opsize ? ebx : ebx[15:0]) : ebx[ 7:0]; 4: op2 <= size ? (opsize ? esp : esp[15:0]) : eax[15:8]; 5: op2 <= size ? (opsize ? ebp : ebp[15:0]) : ecx[15:8]; 6: op2 <= size ? (opsize ? esi : esi[15:0]) : edx[15:8]; 7: op2 <= size ? (opsize ? edi : edi[15:0]) : ebx[15:8]; endcase // 32-bit MODRM if (adsize) begin /* Пока что ничего */ end // 16-bit MODRM else begin case (in[2:0]) 3'b000: ea[15:0] <= ebx + esi; 3'b001: ea[15:0] <= ebx + edi; 3'b010: ea[15:0] <= ebp + esi; 3'b011: ea[15:0] <= ebp + edi; 3'b100: ea[15:0] <= esi; 3'b101: ea[15:0] <= edi; 3'b110: ea[15:0] <= ^in[7:6] ? ebp : 1'b0; 3'b111: ea[15:0] <= ebx; endcase // Выбор сегмента по умолчанию if (!override && (in[2:1] == 2'b01 || (^in[7:6] && in[2:0] == 3'b110))) segment <= ss; // Выбор решения case (in[7:6]) 2'b00: begin // Читать +disp16 if (in[2:0] == 3'b110) fn2 <= 1; // Сразу читать операнды из памяти else begin fn2 <= 4; src <= 1'b1; if (ignoreo) begin t <= exec; fn2 <= 0; end end end 2'b01: fn2 <= 3; // 8 bit 2'b10: fn2 <= 1; // 16 bit 2'b11: begin fn2 <= 0; t <= exec; end endcase end end endcasefetch_modrm: Здесь достаточно крупный кусок кода, но он все еще не полный, потому что нет условия декодирования 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, сразу переход к исполнению инструкции, потому что из памяти ничего не будет загружаться
1: begin fn2 <= 2; ea <= ea + in; eip <= eip_next; end 2: begin fn2 <= adsize ? 8 : 4; src <= !adsize; ea[31:8] <= ea[31:8] + in; eip <= eip_next; if (ignoreo && !adsize) begin t <= exec; fn2 <= 0; end endПервым делом, из шины данных считывается 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, то чтение операндов из памяти производится не будет.
8: begin fn2 <= 9; ea[31:16] <= ea[31:16] + in; eip <= eip_next; end 9: begin fn2 <= 4; ea[31:24] <= ea[31:24] + in; src <= 1'b1; eip <= eip_next; if (ignoreo) begin t <= exec; fn2 <= 0; end endВ этом коде дочитываются старшие 2 байта для эффективного адреса, если все-таки, был переход с adsize=1.
И остался только случай считывания 8-битного смещения:
3: begin fn2 <= 4; ea <= ea + {{24{in[7]}}, in}; src <= 1'b1; eip <= eip_next; if (ignoreo) begin t <= exec; fn2 <= 0; end endЗдесь все просто. При считывании байта из шины, он знакорасширяется до 32-х битного ea и либо переходит к чтению операнда, либо выходит к exec при установленном бите ignoreo.
§ Считывание операндов
Но получить эффективный адрес недостаточно, нужно прочитать то значение, которое находится по заданному эффективному адресу. В зависимости от заданного size и opsize, будут читаться разное количество байт во временную переменную wb:- size=0, 1 байт
- size=1, opsize=0, 2 байта
- size=1, opsize=1, 4 байта
4: begin if (dir) op2 <= in; else op1 <= in; if (size) begin fn2 <= 5; ea <= ea + 1; end else begin fn2 <= 0; t <= exec; end endПринцип работы.
- Если dir=1, то результат из памяти записывается в правый операнд op2, иначе в левый op1. Старшая часть [31:8] очищается в 0.
- Если size=1, то переходит к чтению 2-го байта, ea++; иначе выход к исполнению инструкции
5: begin if (dir) op2[15:8] <= in; else op1[15:8] <= in; if (opsize) begin fn2 <= 6; ea <= ea + 1; end else begin fn2 <= 0; ea <= ea - 1; t <= exec; end endЧитается 2-й байт, запись в op1/op2 аналогично предыдущему.
- При opsize=1, переход к дочитывания 3-го байта
- Если нет, то тогда переход к исполнению, но ea--, чтобы снова указывал на то же место, где был изначально вычислен
// OPERAND-23:16 6: begin fn2 <= 7; ea <= ea + 1; if (dir) op2[23:16] <= in; else op1[23:16] <= in; end // OPERAND-31:24 7: begin t <= exec; fn2 <= 0; ea <= ea - 3; if (dir) op2[31:24] <= in; else op1[31:24] <= in; endВ 7-м такте переход осуществляется без всякого выбора, сразу к exec, и ea откатывается назад на 3 байта, возвращаясь в изначальное положение.
§ Чтение 32-битного modrm+sib
Вот это одна из довольно сложных вещей, с которыми мне пришлось сталкиваться при разработке эмулятора. Это новый вид адресации, который включается префиксом 67h (устанавливается adsize=1), и с помощью него можно адресовать 32-х битную память. Причем, вычисление адреса теперь намного более широкое и использовать можно все регистры, а не некоторые. Например:mov al, [eax] mov ax, [ebx+eax] mov eax, [esi+edi*2] mov sp, [esp-1]32-х битный адрес состоит из 3 компонентов — регистра базы, индекса и смещения. Индекс можно умножать на 0,1,2,4 и 8. Смещение как 8-битное, так 32-х битное.
Новый метод адресации состоит из 2-х компонентов, байта modrm, как и ранее, в r/m части указывается либо смещение в память, либо регистр, и sib, который расширяет возможности адресации и позволяет как раз и использовать сумму базы + индекса.
Начнем разработку. В стадии fetch_modrm, где у меня был пропущен код, теперь его добавим:
case (in[2:0]) 3'b000: ea <= eax; 3'b001: ea <= ecx; 3'b010: ea <= edx; 3'b011: ea <= ebx; 3'b100: ea <= 0; 3'b101: ea <= ^in[7:6] ? ebp : 0; 3'b110: ea <= esi; 3'b111: ea <= edi; endcaseВ этом фрагменте кода высчитывается первичный эффективный адрес, который можно задать из байт modrm. Как видно из кода, можно обратиться к адресу по eax, ecx, edx, ebx, ebp, esi и edi, кроме esp, поскольку, если его указать, будет дочитан байт SIB. Так что, в случае если нам нужен простой адрес + смещение, то его можно закодировать только лишь одним байтом modrm, без sib. Например такие:
mov ax, [ebx] mov [esi-1], alВ следующем коде будет разбираться, что делать далее, основываясь на двух битах mod:
case (in[7:6]) 2'b00: begin if (in[2:0] == 3'b101) fn2 <= 1; // DISP32 else if (in[2:0] == 3'b100) fn2 <= 10; // SIB else begin fn2 <= 4; src <= 1'b1; if (ignoreo) begin t <= exec; fn2 <= 0; end end end 2'b01: fn2 <= in[2:0] == 3'b100 ? 10 : 3; // 8 bit | SIB 2'b10: fn2 <= in[2:0] == 3'b100 ? 10 : 1; // 16/32 bit | SIB 2'b11: begin fn2 <= 0; t <= exec; end endcaseРассмотрим каждый случай:
-
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
, ну а тут был прочтен регистр и ничего не происходит, кроме выхода к исполнению опкода
if (!override && (^in[7:6] && in[2:0] == 3'b101)) segment <= ss;Собственно, выбор сегмента по умолчанию простой, для mod=01 или mod=10, и при r/m=101 (ebp), выбирается сегмент
ss
.§ Байт SIB
И вот пришло время перейти к чтению и разбору байта SIB. Начнем с того, что прочтем базу и индекс:10: begin case (in[5:3]) 3'b000: ea <= sib_base + (eax << in[7:6]); 3'b001: ea <= sib_base + (ecx << in[7:6]); 3'b010: ea <= sib_base + (edx << in[7:6]); 3'b011: ea <= sib_base + (ebx << in[7:6]); 3'b100: ea <= sib_base; 3'b101: ea <= sib_base + (ebp << in[7:6]); 3'b110: ea <= sib_base + (esi << in[7:6]); 3'b111: ea <= sib_base + (edi << in[7:6]); endcase endПри этом не забываем увеличить eip++
eip <= 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?
wire [31:0] sib_base = in[2:0] == 3'b000 ? eax : in[2:0] == 3'b001 ? ecx : in[2:0] == 3'b010 ? edx : in[2:0] == 3'b011 ? ebx : in[2:0] == 3'b100 ? esp : in[2:0] == 3'b101 ? (^modrm[7:6] ? ebp : 1'b0) : in[2:0] == 3'b110 ? esi : edi;Он "висит" на проводе и выбирается из [2:0] бита байта SIB. В качестве базы можно указать регистр esp.
Здесь можно отметить, что выбор зависит от того, что было в mod части байта modrm. Если в mod=01 или mod=10, то в качестве базы выбирается регистр ebp, иначе 0. Получается так, что таким образом можно задавать просто индекс в таком виде например:
mov eax, [ebx*2+1234]То есть, есть индексный регистр, но без указания базы.
С вычислением базы и индексного регистра все, но к ним еще можно дочитать смещения:
case (modrm[7:6]) 2'b00: if (in[2:0] == 3'b101) begin fn2 <= 1; end else begin fn2 <= 4; src <= 1'b1; end 2'b01: begin fn2 <= 3; end // disp8 2'b10: begin fn2 <= 1; end // disp32 2'b11: begin fn2 <= 0; t <= exec; end endcaseКак обычно, рассмотрим каждый случай.
- mod=00, при выборе в качестве базы 101, будет прочитано 32-х битное смещение, а в любом другом случае сразу же перейдет к чтению операндов из памяти
- mod=01 и mod=10 читает 8-битное или 32-х битное смещение соответственно
- mod=11 здесь вообще не будет никогда исполнен, но на всякий случай написал, что переходит к выполнению опкода
ss
:if (!override && ((^modrm[7:6] && in[2:0] == 3'b101) || (in[5:3] == 3'b101))) segment <= ss;Сегмент будет выбран, если mod=01 или 10 и при этом база [2:0] будет равна 101 (ebp), либо если индексный регистр [5:3] равен ebp.
if (modrm[7:6] == 2'b00 && in[2:0] != 3'b101 && ignoreo) begin t <= exec; fn2 <= 0; endПри ignoreo=1, не допустить переход к чтению операнда и выйти сразу к исполнению инструкции.
На этом пока что все. Я рассмотрел чтение байта modrm+sib и операндов, но в следующем материале начнем исполнять код.
Файлы проекта
[<< Предыдущая] [Оглавление]