Лисья Нора

Оглавление


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

Уж сколько раз я делал этот код, что иногда мне кажется, что могу написать его с закрытыми глазами. И так и сяк делал, вообще как только не разрабатывал и перерабатывал его. И пришло время документально об этом сказать с нотариально заверенным скриншотом :-P
Первым делом, надо поправить кое-какие параметры при сбросе процессора:
cs <= 16'hF800; // 16'hF000
eip <= 16'h0000; // 16'hFFF0
__segment <= 80'h0000;
На самом деле, уже потом вместо cs:eip будут значения F000:FFF0, но, поскольку инструкция JMP segment:offset еще пока что далеко не реализована, то переход будет происходить сразу же к биосу, который будет 32 килобайтного размера, а не 64К, как это делают. Мне кажется, для биоса это будет вполне себе нормально, даже простой Бейсик поместится.
При декодировании инструкции, в fetch, нелишним будет добавить вот это:
regn <= in[2:0];
t_next <= fetch;
Где regn потребуется для будущих инструкции INC/DEC/PUSH r16 и других им подобных, а t_next будет необходим для того, чтобы процессор знал, куда переходить после исполнения очередной процедуры. По умолчанию, переход будет на fetch – на считывание новой инструкции.
Теперь, для того, чтобы процессор знал, куда перенаправлять исполнение, нужно дописать условие в fetch:
casex (in)
8'b00xx_x0xx: begin t <= fetch_modrm; end
endcase
Это – маска для опкодов, которые используются АЛУ, работающие с modrm-байтом. После получения этого опкода, процессор выполняет переход к фазе fetch_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
endcase
Здесь достаточно крупный кусок кода, но он все еще не полный, потому что нет условия декодирования 32-х битного ModRM, о чем будет дальше рассказано.
Примерно такой же код рассматривался ранее, но здесь есть некоторые отличия, в первую очередь, при считывании операндов в виде регистров, теперь учитывается не просто 16-битные регистры, но и добавлены 32-х битные. Они активируются при наличии как size, так и opsize.
Первичное вычисление эффективного адреса никак не изменилось, разве что только при сложении используются младшие 16 бит для 32-х битных регистров, но по факту, это ничего не поменяет.
Выбор сегмента ss по умолчанию точно такой же, но только тут различие в том, что сегмент теперь 80-битный, а не 16-битный и копируется также его скрытая часть, которая потом понадобится для вычисления смещений в памяти в защищенном режиме, и в целом, в старших 64 битах хранится загруженная копия дескриптора из памяти GDT.
Чтение 16-битного смещения:
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:
Чтение первого байта:
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
Принцип работы.
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 аналогично предыдущему.
И при size=1, opsize=1, происходит дочитывание 3-го и 4-го байта.
// 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
Рассмотрим каждый случай:
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
Как обычно, рассмотрим каждый случай.
Поскольку в адресации могут использоваться ebp, то надо еще проверить выбор сегмента 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 и операндов.