§ Новые регистры

Как и всегда, начнем с того, чтобы объявить сразу же необходимые регистры, которые будут уже 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, но это довольно сложная схема, на самом деле
Некоторые регистры, например DR0-7 я пока делать не хочу, но если все остальное смогу сделать как надо, то их тоже добавлю.
Еще хочу сказать про то, что код получится чрезвычайно сложным и, возможно, никогда на ПЛИС запускаться не будет, потому что вряд ли он сгенерируется нормально.
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.
В префиксной архитектуре x86 есть такая особенность. При выбранном size=1, будут вместо 8-битных операндов использоваться 16-битные, но, если будет еще и opsize=1, то тогда вместо 16 битных будут 32 битные, однако, при size=0, всегда будут 8 бит, даже если opsize=1.
Вообще, я ранее говорил, но 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.
Файлы проекта