§ Чтение 16-битного modrm

Уж сколько раз я делал этот код, что иногда мне кажется, что могу написать его с закрытыми глазами. И так и сяк делал, вообще как только не разрабатывал и перерабатывал его. И пришло время документально об этом сказать с нотариально заверенным скриншотом :-P
Первым делом, надо поправить кое-какие параметры при сбросе процессора:
cs          <= 16'hF800; // 16'hF000
eip         <= 16'h0000; // 16'hFFF0
__segment   <= 80'h0000;
На самом деле, уже потом вместо cs:eip будут значения F000:FFF0, но, поскольку инструкция JMP segment:offset еще пока что далеко не реализована, то переход будет происходить сразу же к биосу, который будет 32 килобайтного размера, а не 64К, как это делают. Мне кажется, для биоса это будет вполне себе нормально, даже простой Бейсик поместится.
При декодировании инструкции, в fetch, нелишним будет добавить вот это:
regn    <= in[2:0];
t_next  <= fetch;
Где regn потребуется для будущих инструкции INC/DEC/PUSH r16 и других им подобных, а t_next будет необходим для того, чтобы процессор знал, куда переходить после исполнения очередной процедуры. По умолчанию, переход будет на fetch — на считывание новой инструкции.
Теперь, для того, чтобы процессор знал, куда перенаправлять исполнение, нужно дописать условие в fetch:
casex (in)
8'b00xx_x0xx: begin t <= fetch_modrm; end
endcase
Это — маска для опкодов, которые используются АЛУ, работающие с modrm-байтом. После получения этого опкода, процессор выполняет переход к фазе fetch_modrm.
fetch_modrm: case (fn2)
0: begin

    modrm   <= in;
    eip     <= eip_next;
    ea      <= 1'b0;

    // Левый операнд
    case (dir ? in[5:3] : in[2:0])
    0: op1 <= size ? (opsize ? eax : eax[15:0]) : eax[ 7:0];
    1: op1 <= size ? (opsize ? ecx : ecx[15:0]) : ecx[ 7:0];
    2: op1 <= size ? (opsize ? edx : edx[15:0]) : edx[ 7:0];
    3: op1 <= size ? (opsize ? ebx : ebx[15:0]) : ebx[ 7:0];
    4: op1 <= size ? (opsize ? esp : esp[15:0]) : eax[15:8];
    5: op1 <= size ? (opsize ? ebp : ebp[15:0]) : ecx[15:8];
    6: op1 <= size ? (opsize ? esi : esi[15:0]) : edx[15:8];
    7: op1 <= size ? (opsize ? edi : edi[15:0]) : ebx[15:8];
    endcase

    // Правый операнд
    case (dir ? in[2:0] : in[5:3])
    0: op2 <= size ? (opsize ? eax : eax[15:0]) : eax[ 7:0];
    1: op2 <= size ? (opsize ? ecx : ecx[15:0]) : ecx[ 7:0];
    2: op2 <= size ? (opsize ? edx : edx[15:0]) : edx[ 7:0];
    3: op2 <= size ? (opsize ? ebx : ebx[15:0]) : ebx[ 7:0];
    4: op2 <= size ? (opsize ? esp : esp[15:0]) : eax[15:8];
    5: op2 <= size ? (opsize ? ebp : ebp[15:0]) : ecx[15:8];
    6: op2 <= size ? (opsize ? esi : esi[15:0]) : edx[15:8];
    7: op2 <= size ? (opsize ? edi : edi[15:0]) : ebx[15:8];
    endcase

    // 32-bit MODRM
    if (adsize) begin /* Пока что ничего */ end
    // 16-bit MODRM
    else begin

        case (in[2:0])
        3'b000: ea[15:0] <= ebx + esi;
        3'b001: ea[15:0] <= ebx + edi;
        3'b010: ea[15:0] <= ebp + esi;
        3'b011: ea[15:0] <= ebp + edi;
        3'b100: ea[15:0] <= esi;
        3'b101: ea[15:0] <= edi;
        3'b110: ea[15:0] <= ^in[7:6] ? ebp : 1'b0;
        3'b111: ea[15:0] <= ebx;
        endcase

        // Выбор сегмента по умолчанию
        if (!override && (in[2:1] == 2'b01 || (^in[7:6] && in[2:0] == 3'b110)))
            segment <= ss;

        // Выбор решения
        case (in[7:6])
        2'b00: begin

            // Читать +disp16
            if (in[2:0] == 3'b110) fn2 <= 1;
            // Сразу читать операнды из памяти
            else begin

                fn2 <= 4;
                src <= 1'b1;
                if (ignoreo) begin t <= exec; fn2 <= 0; end

            end

        end
        2'b01: fn2 <= 3; // 8 bit
        2'b10: fn2 <= 1; // 16 bit
        2'b11: begin fn2 <= 0; t <= exec; end
        endcase

    end

end
endcase
Здесь достаточно крупный кусок кода, но он все еще не полный, потому что нет условия декодирования 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, сразу переход к исполнению инструкции, потому что из памяти ничего не будет загружаться
Чтение 16-битного смещения:
1: begin fn2 <= 2; ea <= ea + in; eip <= eip_next; end
2: begin

    fn2      <=  adsize ? 8 : 4;
    src      <= !adsize;
    ea[31:8] <= ea[31:8] + in;
    eip      <= eip_next;

    if (ignoreo && !adsize) begin t <= exec; fn2 <= 0; end

end
Первым делом, из шины данных считывается 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, то чтение операндов из памяти производится не будет.
8: begin fn2 <= 9; ea[31:16] <= ea[31:16] + in; eip <= eip_next; end
9: begin

    fn2         <= 4;
    ea[31:24]   <= ea[31:24] + in;
    src         <= 1'b1;
    eip         <= eip_next;

    if (ignoreo) begin t <= exec; fn2 <= 0; end

end
В этом коде дочитываются старшие 2 байта для эффективного адреса, если все-таки, был переход с adsize=1.
И остался только случай считывания 8-битного смещения:
3: begin

    fn2 <= 4;
    ea  <= ea + {{24{in[7]}}, in};
    src <= 1'b1;
    eip <= eip_next;

    if (ignoreo) begin t <= exec; fn2 <= 0; end

end
Здесь все просто. При считывании байта из шины, он знакорасширяется до 32-х битного ea и либо переходит к чтению операнда, либо выходит к exec при установленном бите ignoreo.

§ Считывание операндов

Но получить эффективный адрес недостаточно, нужно прочитать то значение, которое находится по заданному эффективному адресу. В зависимости от заданного size и opsize, будут читаться разное количество байт во временную переменную wb:
  • size=0, 1 байт
  • size=1, opsize=0, 2 байта
  • size=1, opsize=1, 4 байта
Чтение первого байта:
4: begin

    if (dir) op2 <= in; else op1 <= in;
    if (size) begin fn2 <= 5; ea <= ea + 1; end
    else      begin fn2 <= 0; t  <= exec; end

end
Принцип работы.
  • Если dir=1, то результат из памяти записывается в правый операнд op2, иначе в левый op1. Старшая часть [31:8] очищается в 0.
  • Если size=1, то переходит к чтению 2-го байта, ea++; иначе выход к исполнению инструкции
5: begin

    if (dir) op2[15:8] <= in; else op1[15:8] <= in;
    if (opsize) begin fn2 <= 6; ea <= ea + 1; end
    else        begin fn2 <= 0; ea <= ea - 1; t <= exec;  end

end
Читается 2-й байт, запись в op1/op2 аналогично предыдущему.
  • При opsize=1, переход к дочитывания 3-го байта
  • Если нет, то тогда переход к исполнению, но ea--, чтобы снова указывал на то же место, где был изначально вычислен
И при size=1, opsize=1, происходит дочитывание 3-го и 4-го байта.
// OPERAND-23:16
6: begin

    fn2 <= 7; ea <= ea + 1;
    if (dir) op2[23:16] <= in; else op1[23:16] <= in;

end

// OPERAND-31:24
7: begin

    t  <= exec;
    fn2 <= 0; ea <= ea - 3;
    if (dir) op2[31:24] <= in; else op1[31:24] <= in;

end
В 7-м такте переход осуществляется без всякого выбора, сразу к exec, и ea откатывается назад на 3 байта, возвращаясь в изначальное положение.

§ Чтение 32-битного modrm+sib

Вот это одна из довольно сложных вещей, с которыми мне пришлось сталкиваться при разработке эмулятора. Это новый вид адресации, который включается префиксом 67h (устанавливается adsize=1), и с помощью него можно адресовать 32-х битную память. Причем, вычисление адреса теперь намного более широкое и использовать можно все регистры, а не некоторые. Например:
mov al, [eax]
mov ax, [ebx+eax]
mov eax, [esi+edi*2]
mov sp, [esp-1]
32-х битный адрес состоит из 3 компонентов — регистра базы, индекса и смещения. Индекс можно умножать на 0,1,2,4 и 8. Смещение как 8-битное, так 32-х битное.
Новый метод адресации состоит из 2-х компонентов, байта modrm, как и ранее, в r/m части указывается либо смещение в память, либо регистр, и sib, который расширяет возможности адресации и позволяет как раз и использовать сумму базы + индекса.
Начнем разработку. В стадии fetch_modrm, где у меня был пропущен код, теперь его добавим:
case (in[2:0])
3'b000: ea <= eax;
3'b001: ea <= ecx;
3'b010: ea <= edx;
3'b011: ea <= ebx;
3'b100: ea <= 0;
3'b101: ea <= ^in[7:6] ? ebp : 0;
3'b110: ea <= esi;
3'b111: ea <= edi;
endcase
В этом фрагменте кода высчитывается первичный эффективный адрес, который можно задать из байт modrm. Как видно из кода, можно обратиться к адресу по eax, ecx, edx, ebx, ebp, esi и edi, кроме esp, поскольку, если его указать, будет дочитан байт SIB. Так что, в случае если нам нужен простой адрес + смещение, то его можно закодировать только лишь одним байтом modrm, без sib. Например такие:
mov ax, [ebx]
mov [esi-1], al
В следующем коде будет разбираться, что делать далее, основываясь на двух битах mod:
case (in[7:6])
2'b00: begin

    if      (in[2:0] == 3'b101) fn2 <= 1;  // DISP32
    else if (in[2:0] == 3'b100) fn2 <= 10; // SIB
    else begin

        fn2 <= 4;
        src <= 1'b1;
        if (ignoreo) begin t <= exec; fn2 <= 0; end

    end

end
2'b01: fn2 <= in[2:0] == 3'b100 ? 10 : 3; // 8     bit | SIB
2'b10: fn2 <= in[2:0] == 3'b100 ? 10 : 1; // 16/32 bit | SIB
2'b11: begin fn2 <= 0; t <= exec; end
endcase
Рассмотрим каждый случай:
  • 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, ну а тут был прочтен регистр и ничего не происходит, кроме выхода к исполнению опкода
if (!override && (^in[7:6] && in[2:0] == 3'b101))
    segment <= ss;
Собственно, выбор сегмента по умолчанию простой, для mod=01 или mod=10, и при r/m=101 (ebp), выбирается сегмент ss.

§ Байт SIB

И вот пришло время перейти к чтению и разбору байта SIB. Начнем с того, что прочтем базу и индекс:
10: begin

case (in[5:3])
3'b000: ea <= sib_base + (eax << in[7:6]);
3'b001: ea <= sib_base + (ecx << in[7:6]);
3'b010: ea <= sib_base + (edx << in[7:6]);
3'b011: ea <= sib_base + (ebx << in[7:6]);
3'b100: ea <= sib_base;
3'b101: ea <= sib_base + (ebp << in[7:6]);
3'b110: ea <= sib_base + (esi << in[7:6]);
3'b111: ea <= sib_base + (edi << in[7:6]);
endcase

end
При этом не забываем увеличить eip++
eip <= 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?
wire [31:0] sib_base =
    in[2:0] == 3'b000 ? eax :
    in[2:0] == 3'b001 ? ecx :
    in[2:0] == 3'b010 ? edx :
    in[2:0] == 3'b011 ? ebx :
    in[2:0] == 3'b100 ? esp :
    in[2:0] == 3'b101 ? (^modrm[7:6] ? ebp : 1'b0) :
    in[2:0] == 3'b110 ? esi : edi;
Он "висит" на проводе и выбирается из [2:0] бита байта SIB. В качестве базы можно указать регистр esp.
Здесь можно отметить, что выбор зависит от того, что было в mod части байта modrm. Если в mod=01 или mod=10, то в качестве базы выбирается регистр ebp, иначе 0. Получается так, что таким образом можно задавать просто индекс в таком виде например:
mov eax, [ebx*2+1234]
То есть, есть индексный регистр, но без указания базы.
С вычислением базы и индексного регистра все, но к ним еще можно дочитать смещения:
case (modrm[7:6])
2'b00: if (in[2:0] == 3'b101)
       begin fn2 <= 1; end
else   begin fn2 <= 4; src <= 1'b1; end
2'b01: begin fn2 <= 3; end // disp8
2'b10: begin fn2 <= 1; end // disp32
2'b11: begin fn2 <= 0; t <= exec; end
endcase
Как обычно, рассмотрим каждый случай.
  • mod=00, при выборе в качестве базы 101, будет прочитано 32-х битное смещение, а в любом другом случае сразу же перейдет к чтению операндов из памяти
  • mod=01 и mod=10 читает 8-битное или 32-х битное смещение соответственно
  • mod=11 здесь вообще не будет никогда исполнен, но на всякий случай написал, что переходит к выполнению опкода
Поскольку в адресации могут использоваться ebp, то надо еще проверить выбор сегмента ss:
if (!override && ((^modrm[7:6] && in[2:0] == 3'b101) || (in[5:3] == 3'b101)))
    segment <= ss;
Сегмент будет выбран, если mod=01 или 10 и при этом база [2:0] будет равна 101 (ebp), либо если индексный регистр [5:3] равен ebp.
if (modrm[7:6] == 2'b00 && in[2:0] != 3'b101 && ignoreo) begin t <= exec; fn2 <= 0; end
При ignoreo=1, не допустить переход к чтению операнда и выйти сразу к исполнению инструкции.
На этом пока что все. Я рассмотрел чтение байта modrm+sib и операндов, но в следующем материале начнем исполнять код.
Файлы проекта

12 мая, 2022
© 2007-2022 Джерри штырит все права