§ Чтение 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, сразу переход к исполнению инструкции, потому что из памяти ничего не будет загружаться
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--, чтобы снова указывал на то же место, где был изначально вычислен
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 здесь вообще не будет никогда исполнен, но на всякий случай написал, что переходит к выполнению опкода
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 и операндов, но в следующем материале начнем исполнять код.
Файлы проекта
[<< Предыдущая] [Оглавление]