§ Что это такое
Этот байт идет за опкодом, сразу же. Он присутствует не во всех опкодах, а в тех, в котором требуются указать операнды. К примеру, он нужен для инструкции 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, причем тут может быть не только регистр общего назначения.
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 и располагается. Это не обычный биос, а именно для этого эмулятора. Создание кода для биос рассматривается в другом разделе.
Исходные коды по ссылке.
Следующий материал