§ Что это такое

Этот байт идет за опкодом, сразу же. Он присутствует не во всех опкодах, а в тех, в котором требуются указать операнды. К примеру, он нужен для инструкции ADD, SUB и так далее. Такие инструкции получают на вход 2 операнда - destination и source (обычно пишется слева dst и справа src), над ними делаются вычисления и записывается в операнд destination. При этом операнд может указывать в том числе на память, два операнда могут быть регистрами. Есть исключение в том, что оба операнда не могут указывать на память.
Пример: ADD ax, bx. Здесь указываем destination = ax, source = bx, и получим AX = AX + BX.
Общий формат байта modrm:
  • Биты 7..6 (2 бита) отвечают за MOD, модификацию операнда R/M
  • Биты 5..3 (3 бита) ответственны за указания номера регистра от 0 до 7. Это может быть не только регистр, в некоторых инструкциях вместо REG-части байта MODRM указывается инструкция, например ADD, OR, XOR и прочее.
  • Бит 2..0 (3 бита) указывают на память, либо регистр, это зависит от того, какой MOD. Если MOD=0, то к указателю в памяти ничего не будет добавлено, если MOD=1, то будет добавлен еще 1 байт (знаковый), которое называется смещением. MOD=2 будет добавлять 2 байта (тут могут быть и 4 и 8, в зависимости от префиксов), а MOD=3 вместо указателя на память использует регистр от 0 до 7, причем тут может быть не только регистр общего назначения.
Приведу массив, в котором будут указаны те опкоды, в которых есть байт modrm:
1// Карта наличия байта MODRM
2static const unsigned char opcodemap_modrm[512] = {
3
4    // Базовый опкод
5    1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, /* 00 */
6    1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, /* 10 */
7    1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, /* 20 */
8    1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, /* 30 */
9    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 40 */
10    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 50 */
11    0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, /* 60 */
12    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 70 */
13    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 80 */
14    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 90 */
15    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* A0 */
16    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* B0 */
17    1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, /* C0 */
18    1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, /* D0 */
19    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* E0 */
20    0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, /* F0 */
21
22    // Расширенный опкод
23    1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, /* 100 */
24    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 110 */
25    1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, /* 120 */
26    0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, /* 130 */
27    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 140 */
28    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 150 */
29    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 160 */
30    1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 1, /* 170 */
31    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 180 */
32    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 190 */
33    0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, /* 1A0 */
34    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 1B0 */
35    1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, /* 1C0 */
36    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 1D0 */
37    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 1E0 */
38    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, /* 1F0 */
39};
Здесь число 1 означает, что modrm байт присутствует.

§ Код разбора байта modrm

В первую очередь, необходимо определить новые переменные:
1unsigned short i_ea;
2int i_modrm, i_mod, i_reg, i_rm;
Они будут нужны для дальнейшей работы. В i_modrm будет сам байт modrm, i_mod будет равен 0..2 (MOD-часть), i_reg и i_mem равны 0..7 и отвечают за определенные биты в modrm-байте.
1// Прочитать эффективный адрес i_ea и параметры modrm
2void get_modrm() {
3
4    i_modrm =  fetch(1);
5    i_mod   =  i_modrm >> 6;
6    i_reg   = (i_modrm >> 3) & 7;
7    i_rm    =  i_modrm & 7;
8    i_ea    =  0;
9
10    // Расчет индекса
11    switch (i_rm) {
12
13        case 0: i_ea = (regs16[REG_BX] + regs16[REG_SI]); break;
14        case 1: i_ea = (regs16[REG_BX] + regs16[REG_DI]); break;
15        case 2: i_ea = (regs16[REG_BP] + regs16[REG_SI]); break;
16        case 3: i_ea = (regs16[REG_BP] + regs16[REG_DI]); break;
17        case 4: i_ea =  regs16[REG_SI]; break;
18        case 5: i_ea =  regs16[REG_DI]; break;
19        case 6: i_ea =  regs16[REG_BP]; break;
20        case 7: i_ea =  regs16[REG_BX]; break;
21    }
22
23    // В случае если не segment override
24    if (!segment_over_en) {
25
26        if ((i_rm == 6 && i_mod) || (i_rm == 2) || (i_rm == 3))
27            segment_id = REG_SS;
28    }
29
30    // Модифицирующие биты modrm
31    switch (i_mod) {
32
33        case 0: if (i_rm == 6) i_ea = fetch(2); break;
34        case 1: i_ea += (signed char) fetch(1); break;
35        case 2: i_ea += fetch(2); break;
36        case 3: i_ea = 0; break;
37    }
38}
Можно считать, что эта процедура - основная для разбора эффективного адреса. Именно эффективный адрес, а также segment_id здесь и высчитываются.
Первым делом, считывается сам байт modrm и разбирается по частям. После этого берется значение i_rm и рассчитывается базовый 16-битный адрес на его основе. В случае если нет segment prefix (segment_over_en == 0), то тогда префикс сегмента будет равен SS тогда если i_mod != 0 и i_rm == 6 или i_rm равны 2 или 3. Почему так? Потому что там указывается регистр BP, который вместо того, чтобы указывать на DS-сегмент, будет указывать на SS. Естественно, при перегрузке префиксом будет указываться именно тот префикс, который ранее и был при чтении опкода инструкции.
Потом, на основе i_mod делается выбор того, что надо сделать далее, а именно какое смещение выбрать (или не выбирать его совсем):
  • i_mod = 0, смещение добавлять не надо, кроме случая когда i_rm = 6, в этом случае надо загрузить 2 байта, и это будет указателем в память
  • i_mod = 1, смещение у нас будет равно 1 байту, а этот байт не простой, а знаковый, поэтому мы и делаем преобразование (signed char), чтобы компилятор Си прибавил именно знаковый байт
  • i_mod = 2, смещение 2 байта, здесь просто добавляется +2 байта к уже ранее вычисленному адресу, а компилятор, поскольку i_ea у нас типа unsigned short, сам срежет лишние биты сверху
  • i_mod = 3, это указатель, что память не используется для операнда, и вместо этого используется регистр. Ясное дело, что i_ea будет равно 0. Хотя тут не имеет никакой разницы, сколько будет равно i_ea - все равно не учтётся.

§ Чтение и запись из/в R/M

После того, как байт modrm был успешно разобран, необходимо будет получать оттуда операнды. Поскольку то, откуда операнды будут получены - из памяти или из регистра, надо будет написать процедуру, которая также учитывает еще и разрядность получаемых операндов (8 или 16 бит).
1// Получение R/M части; i_w = 1 (word), i_w = 0 (byte)
2unsigned int get_rm(int i_w) {
3
4    if (i_mod == 3) {
5        return i_w ? regs16[i_rm] : regs[REG8(i_rm)];
6    } else {
7        return rd(16*regs16[segment_id] + i_ea, i_w + 1);
8    }
9}
В этом коде мы прямо указываем разрядность i_w. Если i_w=0, то будет получено 8 битное число, если i_w=1, то 16 бит (или 32, 64, это от старших архитектур зависит). Если i_mod = 3, то значит, извлекать эти операнды будут из регистров. Если нет - то из памяти. Для этого как раз и потребуется сегмент segment_id, который был вычислен при разборе modrm, а также смещение i_ea.
Также стоит отметить, что при сохранении 8-битного числа, поскольку порядок следования байтов другой, то надо будет преобразовывать его с помощью следующего макроса:
1#define REG8(x) ((x & 4) >> 2) | ((x & 3) << 1)
Процедура записи аналогична процедуре чтения
1// Сохранение данных в R/M
2void put_rm(int i_w, unsigned short data) {
3
4    if (i_mod == 3) {
5        if (i_w) regs16[i_rm] = data;
6        else regs[REG8(i_rm)] = data;
7    } else {
8        wr(16*regs16[segment_id] + i_ea, data, i_w + 1);
9    }
10}
Здесь все так же, меняется лишь только направление, то есть, будет запись либо 8, либо 16 битного числа.

§ Загрузка BIOS в память

Немного не в тему, но очень нужная вещь, без которой потом ничего работать не будет
1// Загрузка bios в память
2int bios_rom = open("bios.rom", 32898);
3if (bios_rom < 0) { printf("No bios.rom present"); return 1; }
4(void) read(bios_rom, RAM + 0xF0100, 0xFF00);
В этом коде делается попытка открыть bios.rom через функцию open, и если такого файла не найдено, то выходит с ошибкой. Если все хорошо, то читается файл bios.rom а адресу 0xF0100, где обычно BIOS и располагается. Это не обычный биос, а именно для этого эмулятора. Создание кода для биос рассматривается в другом разделе.
Исходные коды по ссылке.
Следующий материал