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

Как и всегда, начнем с того, чтобы объявить сразу же необходимые регистры, которые будут уже 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, но это довольно сложная схема, на самом деле
Некоторые регистры, например DR0-7 я пока делать не хочу, но если все остальное смогу сделать как надо, то их тоже добавлю.
Еще хочу сказать про то, что код получится чрезвычайно сложным и, возможно, никогда на ПЛИС запускаться не будет, потому что вряд ли он сгенерируется нормально.
//                   | ДЕСКРИПТОР        | СЕЛЕКТОР
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.
В префиксной архитектуре 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, который определяется так:
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.
Файлы проекта

12 мая, 2022
© 2007-2022 Простая фигня шла