§ Как работают процедуры

Я считаю, что без процедур процессор работать будет и программы писать можно, но это крайне неудобно. Но в x86 архитектуре процедуры есть, как и во всех уважающих себя архитектурах, поэтому беспокоиться не о чем. Существуют несколько видов вызова процедур, их и опишу. Но сначала я должен рассмотреть основной принцип работы процедур. А он простой. Когда происходит вызов процедуры (CALL), в стек записывается адрес, который находится за инструкцией вызова. Процедура, отработав полностью, использует инструкцию возврат из процедуры (RET), и как раз извлекает из стека последний адрес.

§ CALL near

Для начала рассмотрим простой случай вызова процедуры:
1case 0xE8:
2
3    i_tmp = fetch(2);
4    push(reg_ip);
5    reg_ip += i_tmp;
6    break;
Вначале сканируется 2-х байтное смещение, после чего в стек записывается новый IP, он указывает на адрес, находящийся после инструкции, ну и потом прибавляется i_tmp к reg_ip. Поскольку reg_ip это uint16_t, то переполнение происходит корректно. То есть, другими словами, если i_tmp = FFFFh, то reg_ip уменьшается на 1, а не увеличивается на FFFFh. Также можно вызвать процедуру через modrm-операнд, используя i_reg=2 в групповой инструкции FFh:
1case 2: // CALL rm (i_reg = 2)
2
3    push(reg_ip);
4    reg_ip = get_rm(1);
5    break;

§ CALL far

Дальний вызов процедуры нужен для того, чтобы перейти к конкретному сегменту и смещению, причем они задаются абсолютно, а не относительно, как в случае с CALL.
1case 0x9A: {
2
3    i_tmp  = fetch(2);
4    i_tmp2 = fetch(2);
5
6    push(regs16[REG_CS]);
7    push(reg_ip);
8
9    reg_ip = i_tmp;
10    regs16[REG_CS] = i_tmp2;
11    break;
12}
Читаются 4 байта - первые 2 байта это новый IP, а второй - это новый CS. В стек помещается сначала текущий CS и IP, находящийся сразу же за инструкцией CALL far. И соответственно, передается управление к новому CS:IP.
Существует реализация CALL far для групповой инструкции FFh с операндом байта modrm. Стоит заметить, что call far вряд ли будет работать, если в качестве операнда указать не адрес в памяти, а регистр.
1case 3: // CALL far rm (i_reg = 3)
2
3    i_tmp = SEGREG(segment_id, i_ea);
4
5    i_op1 = rd(i_tmp, 2);
6    i_op2 = rd(i_tmp + 2, 2);
7
8    push(regs16[REG_CS]);
9    push(reg_ip);
10
11    reg_ip = i_op1;
12    regs16[REG_CS] = i_op2;
13    break;
Перед тем, как вызвать процедуру, вычисляется эффективный адрес, основанный на segment_id + i_ea, потом оттуда считываются 4 байта (новый ip, cs), и далее все так же, как и в обычном вызове call far.

§ RET near

Инструкция RET возвращает управление программе, делается выход из процедуры. Информация о том, куда возвращать указатель IP, записана в стеке. Подменяя значение в стеке, можно указывать куда угодно.
1// RET, RET imm8
2case 0xC2: case 0xC3:
3
4    i_tmp  = pop();
5    if (opcode_id == 0xC2) regs16[REG_SP] += fetch(2);
6    reg_ip = i_tmp;
7    break;
При вызове инструкции 0xC3 будет просто присвоено IP = извлеченное значение из стека, а при вызове инструкции 0xC2 дополнительно будет прочитано 2 байта после 0xC2, который прибавится к SP после того, как оттуда будет получен адрес возврата. Это иногда необходимо для того, чтобы аннулировать параметры, которые передаются через PUSH перед вызовом процедуры. Обычно такие параметры передают языки высокого уровня, такие как C++, Delphi и другие компилируемые языки программирования.

§ RET far

Ну и осталось только разобрать возврат из дальней процедуры.
1// RETF [imm]
2case 0xCA:
3case 0xCB:
4
5    i_op1 = pop();
6    i_op2 = pop();
7
8    if (opcode_id == 0xCA) regs16[REG_SP] += fetch(2);
9
10    reg_ip = i_op1;
11    regs16[REG_CS] = i_op2;
12
13    break;
Из стека извлекаются сначала будущий ip, потом cs. Если команда имеет непосредственный операнд (RETF imm8), то после извлечения из стека этот байт добавляется к SP. И в конце операции происходит переход на новый CS:IP.
Новый код можно найти по ссылке.
Следующий материал