§ Новые регистры
Как и всегда, начнем с того, чтобы объявить сразу же необходимые регистры, которые будут уже 32-х битными.1// 8 x 32 битных регистров общего назначения 2reg [31:0] eax = 32'h0000_1234; 3reg [31:0] ebx = 32'h0000_AABC; 4reg [31:0] ecx = 32'h0000_0000; 5reg [31:0] edx = 32'h0000_0000; 6reg [31:0] esp = 32'h0000_0000; 7reg [31:0] ebp = 32'h0000_0000; 8reg [31:0] esi = 32'h0000_0000; 9reg [31:0] edi = 32'h0000_0000; 10 11// VR Nio ODIT SZ A P1C 12reg [17:0] eflags = 18'b00_0000_0000_00000010; 13reg [31:0] eip = 32'h0000_0000;Регистры общего назначения теперь 32-х битные, в том числе и регистр
eip
— extended instruction pointer, также увеличился размер регистра eflags, который теперь занимает аж 18 бит. Новые флаги:- IOPL — располагается в битах [13:12], это флаг для определения привилегии для инструкции ввода-вывода, позволяет защищать не только данные, но и еще ввод и вывод, эти два бита определяют "кольцо защиты", например, если текущий селектор cs находится в кольце защиты 3, но пытается сделать OUT/IN при IOPL=1, то это вызовет исключение общей защиты, потому что более старшее кольцо не имеет доступа к более меньшему
- NT — Nested Task (бит 14), флаг вложенной задачи, используется в защищенном режиме, ставится, когда системная задача вызвала другую задачу через call инструкцию, не jmp
- RF — (бит 16) для регистров отладки DR6 и DR7, позволяет отключать некоторые исключения при отладке
- VM — (бит 17) включение виртуального 8086, который позволяет из защищенного режима использовать предыдущий режим 8086, но это довольно сложная схема, на самом деле
Еще хочу сказать про то, что код получится чрезвычайно сложным и, возможно, никогда на ПЛИС запускаться не будет, потому что вряд ли он сгенерируется нормально.
1// | ДЕСКРИПТОР | СЕЛЕКТОР 2reg [79:0] es = 80'h0000_0000_0000_0000_____0000; 3reg [79:0] cs = 80'h0000_0000_0000_0000_____F000; 4reg [79:0] ss = 80'h0000_0000_0000_0000_____0000; 5reg [79:0] ds = 80'h0000_0000_0000_0000_____0000; 6reg [79:0] fs = 80'h0000_0000_0000_0000_____0000; 7reg [79:0] gs = 80'h0000_0000_0000_0000_____0000;Объявляются новые сегментные регистры, причем тут старшая часть - 64 битная, это на самом деле будет копия дескриптора из памяти, а младшая часть - 16 битная, это либо сегмент (в режиме реальных адресов или vm8086), либо селектор в защищенном режиме.
§ Кнопка сброса
Это довольно важный момент, потому что старт начинается именно со сброса. Когда процессор только начинает работать, сначала устанавливается reset_n=0, которое сигнализирует о сбросе его внутренних регистров исполнения.1always @(posedge clock) 2if (locked) begin // Процессор работает 3if (reset_n == 1'b0) begin 4 5 t <= fetch; 6 cs <= 80'h0000_0080_0000_FFFF__F800; // P=1,LIMIT=65535 7 eip <= 16'hFFF0; 8 eflags <= 2'h2; 9 __segment <= 80'hF800; 10 __adsize <= 1'b0; 11 __opsize <= 1'b0; 12 __override <= 1'b0; 13 __rep <= 1'b0; 14 __opext <= 1'b0; 15 src <= 1'b0; // Указатель на CS:EIP 16 prot <= 1'b0; // Процессор работает в REAL MODE 17 psize <= 1'b0; 18 trace_ff <= 1'b0; 19 cr0 <= 1'b0; 20 21end 22endВ отличии от предыдущего варианта, который я ранее делал, считывание операнда происходит за 1 такт, и потому существуют такие регистры, как
adsize, opsize, override, rep
, в которых будет сохраняться временное значение префиксов.Суть в чем. Когда фаза исполнения опкода находится в t=fetch, пользуясь случаем, как раз таблицу приведу:
1localparam 2 fetch = 0, 3 fetch_modrm = 1, 4 exec = 2, 5 modrm_wb = 3, 6 fetch_imm16 = 4, 7 loadseg = 5, 8 exception = 6, 9 push = 7, 10 pop = 8, 11 shift = 9, 12 interrupt = 10, 13 divide = 11, 14 portin = 12, 15 portout = 13;То тогда на первом такте могут получатся префиксы. Именно с помощью таких вот "теневых" регистров, приходя на первый такт, набираются необходимые данные для дальнейшего исполнения инструкции.
По идее префиксы это некие коды, которые предшествуют основному опкоду. Далее я приведу объявление некоторых регистров, с которыми будем работать:
1reg [3:0] t = 1'b0; // Фаза исполнения 2reg [4:0] fn = 1'b0; // Фаза exec 3reg [3:0] fn2 = 1'b0; // Фаза процедур 4reg [7:0] opcode = 1'b0; // Сохраненный опкод 5reg [7:0] modrm = 1'b0; // Сохраненный modrm 6reg [7:0] sib = 1'b0; // И SIB-байт 7reg src = 1'b0; // Источник адреса segment:ea; cs:eip 8reg [79:0] segment = 1'b0; // Рабочий сегмент 9reg [31:0] ea = 1'b0; // Эффективный адрес 10reg prot = 1'b0; // =1 Защищенный режим 11reg adsize = 1'b0; // =1 32х битная адресация 12reg opsize = 1'b0; // =1 32х битный операнд 13reg override = 1'b0; // =1 Сегмент префиксирован 14reg ignoreo = 1'b0; // Игнорировать чтение из памяти modrm 15reg [1:0] rep = 1'b0; // Режим REPNZ/REPZ 16reg [2:0] alu = 3'h0; // Режим АЛУ 17reg size = 1'b0; // =1 16/32 битный операнд 18reg dir = 1'b0; // =0 rm,r; =1 r,rm modrm 19reg [31:0] op1 = 32'h0; // Левый операнд 20reg [31:0] op2 = 32'h0; // Правый операнд 21reg [31:0] wb = 32'h0; // Значение для записиИ теневые регистры:
1reg __adsize = 1'b0; 2reg __opsize = 1'b0; 3reg __override = 1'b0; 4reg [1:0] __rep = 2'b00; 5reg [79:0] __segment = 16'h0000;Регистр src указывает, откуда брать данные из памяти:
1assign address = src ? {segment[15:0], 4'h0} + (adsize ? ea : ea[15:0]): // Если segment:ea 2 { cs[15:0], 4'h0} + eip[15:0]; // Иначе cs:eipВ данный момент режим работы Protected Mode не реализован, потому рассматриваются только 16-битные смещения + сегментная модель. При реализации защищенного режима там все на порядки усложняется.
Надо не забыть установить выходные пины:
1initial begin we = 1'b0; out = 1'b0; end
§ Считывание опкода
Итак, подходим к первому, что делает процессор после сброса — это считыванию опкода. Стоит учесть, что первым может идти префикс. Что происходит после того, как процессор сбросился? Он устанавливает src=0, а значит, к следующему такту на шине данныхin
будет значение из address = cs*16 + ip, и, в зависимости от того, что это за байт, будет решаться, что делать с ним дальше.Если пришел байт префикса — то просто передвигаем ip++, и читаем заново, предварительно записав в теневой регистр нужное значение, а если пришел опкод — то тогда все теневые префиксы перенести в видимые (override, rep, и т.п.), а сами же эти теневые префиксы заново установить для следующей команды.
1always @(posedge clock) 2... 3end else case (t) 4fetch: begin 5 6 eip <= eip_next; 7 opcode <= in; 8 size <= in[0]; 9 dir <= in[1]; 10 alu <= in[5:3]; 11 fn <= 1'b0; 12 fn2 <= 1'b0; 13 ignoreo <= 1'b0; 14 15 case (in) 16 // Сегментные префиксы, 80-разрядные 17 8'h26: begin __segment <= es; __override <= 1'b1; end 18 8'h2E: begin __segment <= cs; __override <= 1'b1; end 19 8'h36: begin __segment <= ss; __override <= 1'b1; end 20 8'h3E: begin __segment <= ds; __override <= 1'b1; end 21 8'h64: begin __segment <= fs; __override <= 1'b1; end 22 8'h65: begin __segment <= gs; __override <= 1'b1; end 23 8'h66: begin __opsize <= ~__opsize; end 24 8'h67: begin __adsize <= ~__adsize; end 25 8'hF0, 8'h9B, 8'h90: begin /* fwait, lock, nop */ end 26 8'hF2, 8'hF3: __rep <= in[1:0]; 27 // Исполнение опкода 28 default: begin 29 30 t <= exec; 31 32 // Защелкивание префиксов 33 rep <= __rep; __rep <= 2'b00; 34 override <= __override; __override <= 1'b0; 35 opsize <= __opsize; __opsize <= defsize; 36 adsize <= __adsize; __adsize <= defsize; 37 segment <= __segment; __segment <= ds; 38 39 // ... 40 41 end 42 endcase 43endКода довольно много, теперь разберем что тут происходит.
- Для каждого считанного байта всегда увеличивается eip++ (eip <= eip_next)
- Каждый раз записывается новый opcode, хотя это может быть и префикс. В случае префикса, opcode будет записан следующим
- size, dir, alu устанавливаются по умолчанию, потому что большинство операции происходит именно в этих правилах
- fn, fn2 сбрасываются в 0, это фазы исполнения либо основного exec, либо процедур
- ignoreo необходим для некоторых случаев, когда не требуется чтение операнда из памяти, например для инструкции mov [bx], ax или lea.
Вообще, я ранее говорил, но dir указывает то, как будет прочитан байт modrm. При dir=0, порядок операндов такой —
r/m, reg
, то есть, слева операнд будет либо регистров, либо в памяти, а справа всегда регистр, а при dir=1 порядок операндов меняется на reg, r/m
.Как видно, когда в in появляются 26h,2Eh,36h,3Eh,64h,65h — то записывается в segment (80-битный) значение из сегментного регистра, включая его "теневую" часть. Префикс 66h и 67h меняют признак opsize/adsize на противоположный. Это значит, что если, например, opsize был 1, то он станет 0, и еще один такой же префикс отменяет 1 и он становится снова 0. Такой подход позволяет менять с 32-х битных на 16-битные методы адресации или размера операнда, если текущий сегмент cs — 32х битный, и наоборот.
Коды F0h, 9Bh, 90h — вообще ничего не делают, и пропускаются, потому что LOCK: ни к чему, FWAIT бесполезен, а NOP сам по себе No Operation.
Для F2h и F3h в регистр rep[1] будет установлен 1, и если это префикс REPNZ, то в rep[0]=0, иначе, при REPZ, будет rep[0]=1.
Итак, когда все префиксы считаны, выполняется код переброса "теневых" префиксов на новые: override, rep, opsize, adsize, и установкой новых "теневых" префиксов, причем, важная деталь в том, что в opsize/adsize будет установлен defsize, который определяется так:
1wire defsize = cs[54+16] & prot;Этот провод будет иметь значение =1, если 1) включен protected mode, 2) установлен бит 54 - Default в дескрипторе. Этот бит отвечает за текущую разрядность сегмента cs. Если там установлен =1, то по умолчанию, операнды и память будут именно 32х битными.
В связи с тем, что могут быть разные режимы работы, то работает приращение EIP разными методами:
1wire [15:0] ipnext1 = eip[15:0] + 1'b1; 2wire [15:0] ipnext2 = eip[15:0] + 2'h2; 3wire [31:0] eip_next = defsize ? eip + 1'b1 : {eip[31:16], ipnext1}; 4wire [31:0] eip_next2 = defsize ? eip + 2'h2 : {eip[31:16], ipnext2};Если сегмент cs — 32-х битный, и находится в защищенном режиме, то прибавляется к 32-х битному eip, иначе — к младшим 16 битам регистра eip.
§ АЛУ и условные переходы
Чтобы не повторяться, то ранее я уже писал про эту тему, но в данном материале я просто приведу новый код с пояснениями по изменениям.1wire [7:0] branches = { 2 3 /*7*/ (eflags[SF] ^ eflags[OF]) | eflags[ZF], 4 /*6*/ (eflags[SF] ^ eflags[OF]), 5 /*5*/ eflags[PF], 6 /*4*/ eflags[SF], 7 /*3*/ eflags[CF] | eflags[ZF], 8 /*2*/ eflags[ZF], 9 /*1*/ eflags[CF], 10 /*0*/ eflags[OF] 11};В вычислениях условий ничего особо и не поменялось, только регистр flags стал eflags. Все остальные объявления флагов те же самые, кроме некоторых новых.
1localparam 2 CF = 0, PF = 2, AF = 4, ZF = 6, SF = 7, 3 TF = 8, IF = 9, DF = 10, OF = 11, 4 IOPL0 = 12, 5 IOPL1 = 13, 6 NT = 14, RF = 16, VM = 17; 7 8localparam 9 alu_add = 3'h0, alu_or = 3'h1, 10 alu_adc = 3'h2, alu_sbb = 3'h3, 11 alu_and = 3'h4, alu_sub = 3'h5, 12 alu_xor = 3'h6, alu_cmp = 3'h7;Модуль АЛУ выглядит точно так же, но с той лишь разницей, что теперь 32-х битный результат вместо 16-битного.
1wire [32:0] alu_r = 2 3 alu == alu_add ? op1 + op2 : 4 alu == alu_or ? op1 | op2 : 5 alu == alu_adc ? op1 + op2 + eflags[CF] : 6 alu == alu_sbb ? op1 - op2 - eflags[CF] : 7 alu == alu_and ? op1 & op2: 8 alu == alu_xor ? op1 ^ op2: 9 op1 - op2; // sub, cmpА также поменялась стратегия выбора старшего бита
1wire [ 4:0] alu_top = size ? (opsize ? 31 : 15) : 7;Но во всем остальном — полная копия прежнего кода 16-битного АЛУ.
1wire is_add = alu == alu_add || alu == alu_adc; 2wire is_lgc = alu == alu_xor || alu == alu_and || alu == alu_or; 3wire alu_cf = alu_r[alu_top + 1'b1]; 4wire alu_af = op1[4] ^ op2[4] ^ alu_r[4]; 5wire alu_sf = alu_r[alu_top]; 6wire alu_zf = size ? (opsize ? ~|alu_r[31:0] : ~|alu_r[15:0]) : ~|alu_r[7:0]; 7wire alu_pf = ~^alu_r[7:0]; 8wire alu_of = (op1[alu_top] ^ op2[alu_top] ^ is_add) & (op1[alu_top] ^ alu_r[alu_top]); 9 10wire [17:0] alu_f = { 11 12 /* .. */ eflags[17:12], 13 /* OF */ alu_of & ~is_lgc, 14 /* DIT */ eflags[10:8], 15 /* SF */ alu_sf, 16 /* ZF */ alu_zf, 17 /* 5 */ 1'b0, 18 /* AF */ alu_af & ~is_lgc, 19 /* 3 */ 1'b0, 20 /* PF */ alu_pf, 21 /* 1 */ 1'b1, 22 /* CF */ alu_cf & ~is_lgc 23};Кроме разве что новых флагов eflags[17:12], которые просто копируются из регистра eflags.
Файлы проекта