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