§ Что такое стек
Стек (stack) - это одна из ключевых технологии для любого процессора. Стек позволяет сохранять временные данные для процедур и не только. Стек организован приблизительно говоря, как гора посуды - первую тарелку положишь, последней возьмешь, то есть если положить тарелку, потом на нее еще тарелку, и еще, то если тарелку надо будет забрать, то будет взята именно последняя положенная тарелка, потом предпоследняя, а потом первая. Такой принцип называется LIFO (Last In First Out - первым пришел, последним ушел).В процессоре стек организован в виде указателя на память в каком-то регистре. В процессоре x86 это регистр SP (или расширенный для 32 бит ESP, 64 бит RSP). Он указывает на последний элемент, а конкретно 2/4/8 байта (машинное слово). При вставке значения в стек SP уменьшается, а при извлечении он увеличивается.
1// Вставить в стек 2void push(uint16_t data) { 3 4 regs16[REG_SP] -= 2; 5 wr(SEGREG(REG_SS, regs16[REG_SP]), data, 2); 6} 7 8// Извлечь из стека 9uint16_t pop() { 10 11 uint16_t tw = rd(SEGREG(REG_SS, regs16[REG_SP]), 2); 12 regs16[REG_SP] += 2; 13 return tw; 14}В процедуре
push
, перед тем, как данные будут записаны в память по адресу SS:SP, сам указатель стека уменьшается на 2 (word), и уже потом записывается. А pop
же наоборот, сначала читаются данные из указателя SS:SP, а потом к SP добавляется 2.§ Реализация PUSH и POP
В процессоре есть такие инструкции, как PUSH и POP. Они обычно работают с 16 разрядными регистрами. Рассмотрю сначала самые простые наборы инструкции.1// PUSH r16 2case 0x50: case 0x51: case 0x52: case 0x53: 3case 0x54: case 0x55: case 0x56: case 0x57: 4 5 push(regs16[opcode_id & 7]); 6 break; 7 8// POP r16 9case 0x58: case 0x59: case 0x5A: case 0x5B: 10case 0x5C: case 0x5D: case 0x5E: case 0x5F: 11 12 regs16[opcode_id & 7] = pop(); 13 break;Этот код очень простой: здесь выбирается 16 битный регистр и либо из него пишется в стек, либо извлекается из стека в регистр. Аналогично для сегментных регистров:
1// PUSH sreg 2case 0x06: case 0x0E: case 0x16: case 0x1E: 3 4 push(regs16[REG_ES + ((opcode_id >> 3) & 3)]); 5 break; 6 7// POP sreg 8case 0x07: case 0x17: case 0x1F: 9 10 regs16[REG_ES + ((opcode_id >> 3) & 3)] = pop(); 11 break;Единственное отличие, что номер сегментного регистра находится в битах 4:3 (всего 4 регистра), поэтому выбор опкода
(opcode>>3)&3
такой необычный.Реализуем, пожалуй, еще 2 инструкции, не принадлежащих к 8086
1// PUSH imm16 2case 0x68: push(fetch(2)); break; 3 4// PUSH imm8 5case 0x6A: i_tmp = fetch(1); push(i_tmp & 0x80 ? i_tmp | 0xFF00 : i_tmp); break;Второй PUSH i8 требуется пояснить. Читается байт из непосредственного операнда. Если старший бит равен 1, то этот байт расширяется - в старших 8 битах тоже устанавливается все единицы, иначе там остаются нули. Тем самым образом это может позволить сэкономить на занимаемом программой месте.
И еще есть возможность выполнить инструкцию POP через modrm:
1// POP rm 2case 0x8F: i_tmp = pop(); put_rm(1, i_tmp); break;Извлекается число из стека, и записывается в регистр или память. А вот инструкция PUSH rm находится уже в групповой инструкции FFh под номером i_reg = 6:
1// Групповая инструкция FFh 2case 6: push(get_rm(1)); break;Она тоже очень простая. Просто извлекается из rm-части операнд и записывается в стек. Вот как по мне, сделано плохо. Лучше бы вместо POP rm использовали i_reg=7, который сейчас undefined instruction и освободили бы место для инструкции 0x8F, поскольку там modrm, у которого i_reg часть вообще никакой роли не играет. Это явно недоработка инженегрии Интола.
§ PUSHA, POPA
Эти инструкции не для 8086, но я все равно сделаю их реализацию, тем более это и несложно. Удивительный факт - они работают только для процессоров старше 186, но в 64-х битном режиме их уже нет.Приведу код для PUSHA:
1case 0x60: 2 3 i_tmp = regs16[REG_SP]; 4 for (int i = 0; i < 8; i++) 5 push(i == REG_SP ? i_tmp : regs16[i]); 6 7 break;Перед операцией сохраняется в i_tmp значение регистра SP, что обязательно, а потом последовательно вставляются в стек 8 регистров общего назначения. Если номер регистра REG_SP, то вставляется предыдущее значение SP.
Код POPA немного посложнее:
1case 0x61: 2 3 for (int i = 7; i >= 0; i--) { 4 if (i == REG_SP) i_tmp = pop(); 5 else regs16[i] = pop(); 6 } 7 regs16[REG_SP] = i_tmp; 8 break;Регистры извлекаются в обратном порядке. При извлечении регистра SP, он записывается в i_tmp, и потом, после операции, уже записывается в SP.
§ PUSHF, POPF
Сами по себе эти инструкции работают тривиально1// PUSHF 2case 0x9C: push(get_flags()); break; 3 4// POPF 5case 0x9D: set_flags(pop()); break;Но были написаны 2 процедуры для корректного получения флагов
1uint16_t get_flags() { 2 3 return 4 /* 0 */ (!!flags.c) | 5 /* 1 */ 0x02 | 6 /* 2 */ (!!flags.p<<2) | 7 /* 3 */ 0 | 8 /* 4 */ (!!flags.a<<4) | 9 /* 5 */ 0 | 10 /* 6 */ (!!flags.z<<6) | 11 /* 7 */ (!!flags.s<<7) | 12 /* 8 */ (!!flags.t<<8) | 13 /* 9 */ (!!flags.i<<9) | 14 /* 10 */ (!!flags.d<<10) | 15 /* 11 */ (!!flags.o<<11); 16}И корректного сохранения данных во флаги
1void set_flags(uint16_t data) { 2 3 // basic 4 flags.c = !!(data & 0x01); 5 flags.p = !!(data & 0x04); 6 flags.a = !!(data & 0x10); 7 flags.z = !!(data & 0x40); 8 flags.s = !!(data & 0x80); 9 // extend 10 flags.t = !!(data & 0x100); 11 flags.i = !!(data & 0x200); 12 flags.d = !!(data & 0x400); 13 flags.o = !!(data & 0x800); 14}Исходные коды прикреплены в архиве.
Следующий материал