§ Что такое стек

Стек (stack) - это одна из ключевых технологии для любого процессора. Стек позволяет сохранять временные данные для процедур и не только. Стек организован приблизительно говоря, как гора посуды - первую тарелку положишь, последней возьмешь, то есть если положить тарелку, потом на нее еще тарелку, и еще, то если тарелку надо будет забрать, то будет взята именно последняя положенная тарелка, потом предпоследняя, а потом первая. Такой принцип называется LIFO (Last In First Out - первым пришел, последним ушел).
В процессоре стек организован в виде указателя на память в каком-то регистре. В процессоре x86 это регистр SP (или расширенный для 32 бит ESP, 64 бит RSP). Он указывает на последний элемент, а конкретно 2/4/8 байта (машинное слово). При вставке значения в стек SP уменьшается, а при извлечении он увеличивается.
// Вставить в стек
void push(uint16_t data) {

    regs16[REG_SP] -= 2;
    wr(SEGREG(REG_SS, regs16[REG_SP]), data, 2);
}

// Извлечь из стека
uint16_t pop() {

    uint16_t tw = rd(SEGREG(REG_SS, regs16[REG_SP]), 2);
    regs16[REG_SP] += 2;
    return tw;
}
В процедуре push, перед тем, как данные будут записаны в память по адресу SS:SP, сам указатель стека уменьшается на 2 (word), и уже потом записывается. А pop же наоборот, сначала читаются данные из указателя SS:SP, а потом к SP добавляется 2.

§ Реализация PUSH и POP

В процессоре есть такие инструкции, как PUSH и POP. Они обычно работают с 16 разрядными регистрами. Рассмотрю сначала самые простые наборы инструкции.
// PUSH r16
case 0x50: case 0x51: case 0x52: case 0x53:
case 0x54: case 0x55: case 0x56: case 0x57:

    push(regs16[opcode_id & 7]);
    break;

// POP r16
case 0x58: case 0x59: case 0x5A: case 0x5B:
case 0x5C: case 0x5D: case 0x5E: case 0x5F:

    regs16[opcode_id & 7] = pop();
    break;
Этот код очень простой: здесь выбирается 16 битный регистр и либо из него пишется в стек, либо извлекается из стека в регистр. Аналогично для сегментных регистров:
// PUSH sreg
case 0x06: case 0x0E: case 0x16: case 0x1E:

    push(regs16[REG_ES + ((opcode_id >> 3) & 3)]);
    break;

// POP sreg
case 0x07: case 0x17: case 0x1F:

    regs16[REG_ES + ((opcode_id >> 3) & 3)] = pop();
    break;
Единственное отличие, что номер сегментного регистра находится в битах 4:3 (всего 4 регистра), поэтому выбор опкода (opcode>>3)&3 такой необычный.
Реализуем, пожалуй, еще 2 инструкции, не принадлежащих к 8086
// PUSH imm16
case 0x68: push(fetch(2)); break;

// PUSH imm8
case 0x6A: i_tmp = fetch(1); push(i_tmp & 0x80 ? i_tmp | 0xFF00 : i_tmp); break;
Второй PUSH i8 требуется пояснить. Читается байт из непосредственного операнда. Если старший бит равен 1, то этот байт расширяется - в старших 8 битах тоже устанавливается все единицы, иначе там остаются нули. Тем самым образом это может позволить сэкономить на занимаемом программой месте.
И еще есть возможность выполнить инструкцию POP через modrm:
// POP rm
case 0x8F: i_tmp = pop(); put_rm(1, i_tmp); break;
Извлекается число из стека, и записывается в регистр или память. А вот инструкция PUSH rm находится уже в групповой инструкции FFh под номером i_reg = 6:
// Групповая инструкция FFh
case 6: push(get_rm(1)); break;
Она тоже очень простая. Просто извлекается из rm-части операнд и записывается в стек. Вот как по мне, сделано плохо. Лучше бы вместо POP rm использовали i_reg=7, который сейчас undefined instruction и освободили бы место для инструкции 0x8F, поскольку там modrm, у которого i_reg часть вообще никакой роли не играет. Это явно недоработка инженегрии Интола.

§ PUSHA, POPA

Эти инструкции не для 8086, но я все равно сделаю их реализацию, тем более это и несложно. Удивительный факт - они работают только для процессоров старше 186, но в 64-х битном режиме их уже нет.
Приведу код для PUSHA:
case 0x60:

    i_tmp = regs16[REG_SP];
    for (int i = 0; i < 8; i++)
        push(i == REG_SP ? i_tmp : regs16[i]);

    break;
Перед операцией сохраняется в i_tmp значение регистра SP, что обязательно, а потом последовательно вставляются в стек 8 регистров общего назначения. Если номер регистра REG_SP, то вставляется предыдущее значение SP.
Код POPA немного посложнее:
case 0x61:

    for (int i = 7; i >= 0; i--) {
        if (i == REG_SP) i_tmp = pop();
        else regs16[i] = pop();
    }
    regs16[REG_SP] = i_tmp;
    break;
Регистры извлекаются в обратном порядке. При извлечении регистра SP, он записывается в i_tmp, и потом, после операции, уже записывается в SP.

§ PUSHF, POPF

Сами по себе эти инструкции работают тривиально
// PUSHF
case 0x9C: push(get_flags()); break;

// POPF
case 0x9D: set_flags(pop()); break;
Но были написаны 2 процедуры для корректного получения флагов
uint16_t get_flags() {

    return
    /* 0 */ (!!flags.c) |
    /* 1 */ 0x02 |
    /* 2 */ (!!flags.p<<2) |
    /* 3 */ 0 |
    /* 4 */ (!!flags.a<<4) |
    /* 5 */ 0 |
    /* 6 */ (!!flags.z<<6) |
    /* 7 */ (!!flags.s<<7) |
    /* 8 */ (!!flags.t<<8) |
    /* 9 */ (!!flags.i<<9) |
    /* 10 */ (!!flags.d<<10) |
    /* 11 */ (!!flags.o<<11);
}
И корректного сохранения данных во флаги
void set_flags(uint16_t data) {

    // basic
    flags.c = !!(data & 0x01);
    flags.p = !!(data & 0x04);
    flags.a = !!(data & 0x10);
    flags.z = !!(data & 0x40);
    flags.s = !!(data & 0x80);
    // extend
    flags.t = !!(data & 0x100);
    flags.i = !!(data & 0x200);
    flags.d = !!(data & 0x400);
    flags.o = !!(data & 0x800);
}
Исходные коды прикреплены в архиве.
Следующий материал