Данный пост является продолжением Ещё одной архитектуры операционной системы.
Определившись с базовыми идеями, я начал размышлять о том, с чего начать разработку, да, притом, так, чтобы, столкнувшись с трудностями, не потерять интерес. Справедливости ради, замечу, что эта попытка у меня далеко не первая. Например, в прошлый раз я по простоте душевной начал с написания загрузчика. Вдоволь наигравшись с реальным и защищённым режимами, я закончил на работающем прототипе, незаметно растеряв весь свой интерес. Текущая попытка началась с осознанного понимания того, что начинать стоит с API, причем для этого совсем не нужно вступать в интимные отношения с сегментными дескрипторами.
Нативный API (С/С++) не подходил по нескольким причинам. Во-первых, он требует разделённые адресные пространства, что влечёт за собой приличные накладные расходы на IPC и взаимодействие с ядром. Вдохновлённый современными веяниями, я хотел ОС одного адресного пространства. Во-вторых, нативный API не обеспечит бинарной совместимости кода между разными архитектурами. И, наконец, такой API будет препятствовать прозрачности удалённых вызовов. Итак, требовалась виртуальная машина. С неё я и решил начать.
Вот основные требования, которые нарисовались сами собой:
1. Защита памяти (код одного модуля не может разрушить память другого модуля);
2. Прямая поддержка прозрачности вызовов;
3. Использование бритвы Оккама (KISS);
4. Эффективность;
Поэтому, стоит отказаться от «жирных» абстракций, таких как классы, mark and sweep GC, и др. (два последних пункта). Кроме того, виртуальная машина должна поддерживать только 64-битные логические и арифметические операции (третий пункт). Действительно, учитывая тот факт, что система будет целиком располагаться в виртуальном адресном пространстве, не имеет смысла ориентироваться на 32 бита (4 гигабайта сегодня мало даже для смартфона). Далее, архитектура виртуальной машины должна быть регистровой, а не стековой (последний пункт).
1. Когда поток исполняет программу, он читает и модифицирует переменные;
2. Переменная это непрерывная область памяти, организованная в виде массива элементов одного размера;
3. Тип переменной определяет структуру элемента переменной, а описатель переменной – число её элементов и флаги (дополнительные свойства);
4. Тип переменной и содержит поля:
a. bytes – число байт элемента, доступных для арифметических и логических операций;
b. vrefs – массив описателей переменных. Соответствует ссылкам элемента на другие переменные;
c. prefs – массив идентификаторов типа процедуры. Соответствует ссылкам элемента на процедуры;
Причина разделения структуры элемента и числа элементов в переменной на разные сущности (тип и описатель переменной) неочевидна. На практике оказалось, что тип переменной является более универсальным понятием, использующихся в разных операциях, тогда как число элементов привязано лишь к конкретной переменной.
На C++ имеем следующее определение типа переменной (vm/vmdefs.h):
Как видно из вышеприведённых определений, ссылка на переменную определяет и число её элементов. На самом деле есть переменные с переменным числом элементов, но пока это опустим. Тоже касательно ссылок на процедуры (пока опустим). Отмечу лишь то, что переменная, по сути, является аналогом массива структур, каждая из которых имеет простые поля, указатели на другие переменные и указатели на другие функции. Разница лишь в том, что здесь поля структуры семантически чётко поделены на три класса. Каждому классу соответствуют разные инструкции. Кроме того, поля каждого класса для простоты сгруппированы в непрерывные секции памяти (массив байт, массив ссылок на переменные, массив ссылок на процедуры). Типы переменных статически задаются в теле модуля во время его создания и не могут меняться.
Инструкции виртуальной машины не оперируют переменными непосредственно. Они это делают с использованием регистров. Регистр – число, которое в процессе исполнения кода ассоциируется к переменной, что позволяет её читать и модифицировать посредством инструкций. Регистру соответствует описатель переменной. Регистры, как и типы переменных, статически задаются в теле модуля во время его создания, и не могут меняться.
Регистры “привязываются “ и “отвязываются” от переменных посредством инструкций PUSH, PUSHR, POP. Инструкции PUSH и PUSHR берут в качестве аргумента регистр. Первая выделяет в стеке новую переменную и ассоциирует её с данным регистром. Вторая выделяет в стеке ссылку на переменную и ассоциирует её с данным регистром. Во втором случае регистр нельзя будет использовать для чтения/модификации до выделения переменной в куче специальной инструкцией (детальное описание особенностей переменных, выделяемых в куче, пока опустим). Инструкция POP отменяет действие последнего PUSH или PUSHR. При этом удаляется переменная (или ссылка на переменную) и соответствующий регистр возвращается к предыдущему назначению переменной.
В заключение приведу код, создающий модуль с одной внешней процедурой (функцией), вычисляющей факториал. Обсуждения деталей оставим до следующего поста, а пока просто составим впечатление (vm/test/modules.cpp):
Определившись с базовыми идеями, я начал размышлять о том, с чего начать разработку, да, притом, так, чтобы, столкнувшись с трудностями, не потерять интерес. Справедливости ради, замечу, что эта попытка у меня далеко не первая. Например, в прошлый раз я по простоте душевной начал с написания загрузчика. Вдоволь наигравшись с реальным и защищённым режимами, я закончил на работающем прототипе, незаметно растеряв весь свой интерес. Текущая попытка началась с осознанного понимания того, что начинать стоит с API, причем для этого совсем не нужно вступать в интимные отношения с сегментными дескрипторами.
Нативный API (С/С++) не подходил по нескольким причинам. Во-первых, он требует разделённые адресные пространства, что влечёт за собой приличные накладные расходы на IPC и взаимодействие с ядром. Вдохновлённый современными веяниями, я хотел ОС одного адресного пространства. Во-вторых, нативный API не обеспечит бинарной совместимости кода между разными архитектурами. И, наконец, такой API будет препятствовать прозрачности удалённых вызовов. Итак, требовалась виртуальная машина. С неё я и решил начать.
Вот основные требования, которые нарисовались сами собой:
1. Защита памяти (код одного модуля не может разрушить память другого модуля);
2. Прямая поддержка прозрачности вызовов;
3. Использование бритвы Оккама (KISS);
4. Эффективность;
Поэтому, стоит отказаться от «жирных» абстракций, таких как классы, mark and sweep GC, и др. (два последних пункта). Кроме того, виртуальная машина должна поддерживать только 64-битные логические и арифметические операции (третий пункт). Действительно, учитывая тот факт, что система будет целиком располагаться в виртуальном адресном пространстве, не имеет смысла ориентироваться на 32 бита (4 гигабайта сегодня мало даже для смартфона). Далее, архитектура виртуальной машины должна быть регистровой, а не стековой (последний пункт).
Переменные
1. Когда поток исполняет программу, он читает и модифицирует переменные;
2. Переменная это непрерывная область памяти, организованная в виде массива элементов одного размера;
3. Тип переменной определяет структуру элемента переменной, а описатель переменной – число её элементов и флаги (дополнительные свойства);
4. Тип переменной и содержит поля:
a. bytes – число байт элемента, доступных для арифметических и логических операций;
b. vrefs – массив описателей переменных. Соответствует ссылкам элемента на другие переменные;
c. prefs – массив идентификаторов типа процедуры. Соответствует ссылкам элемента на процедуры;
Причина разделения структуры элемента и числа элементов в переменной на разные сущности (тип и описатель переменной) неочевидна. На практике оказалось, что тип переменной является более универсальным понятием, использующихся в разных операциях, тогда как число элементов привязано лишь к конкретной переменной.
На C++ имеем следующее определение типа переменной (vm/vmdefs.h):
typedef uint32_t VarTypeId;
typedef uint32_t ProcTypeId;
struct VarSpec {
uint32_t flags;
VarTypeId vtype;
size_t count;
};
struct VarType {
size_t bytes;
std::vector<VarSpec> vrefs;
std::vector<ProcTypeId> prefs;
};
Как видно из вышеприведённых определений, ссылка на переменную определяет и число её элементов. На самом деле есть переменные с переменным числом элементов, но пока это опустим. Тоже касательно ссылок на процедуры (пока опустим). Отмечу лишь то, что переменная, по сути, является аналогом массива структур, каждая из которых имеет простые поля, указатели на другие переменные и указатели на другие функции. Разница лишь в том, что здесь поля структуры семантически чётко поделены на три класса. Каждому классу соответствуют разные инструкции. Кроме того, поля каждого класса для простоты сгруппированы в непрерывные секции памяти (массив байт, массив ссылок на переменные, массив ссылок на процедуры). Типы переменных статически задаются в теле модуля во время его создания и не могут меняться.
Регистры
Инструкции виртуальной машины не оперируют переменными непосредственно. Они это делают с использованием регистров. Регистр – число, которое в процессе исполнения кода ассоциируется к переменной, что позволяет её читать и модифицировать посредством инструкций. Регистру соответствует описатель переменной. Регистры, как и типы переменных, статически задаются в теле модуля во время его создания, и не могут меняться.
Регистры “привязываются “ и “отвязываются” от переменных посредством инструкций PUSH, PUSHR, POP. Инструкции PUSH и PUSHR берут в качестве аргумента регистр. Первая выделяет в стеке новую переменную и ассоциирует её с данным регистром. Вторая выделяет в стеке ссылку на переменную и ассоциирует её с данным регистром. Во втором случае регистр нельзя будет использовать для чтения/модификации до выделения переменной в куче специальной инструкцией (детальное описание особенностей переменных, выделяемых в куче, пока опустим). Инструкция POP отменяет действие последнего PUSH или PUSHR. При этом удаляется переменная (или ссылка на переменную) и соответствующий регистр возвращается к предыдущему назначению переменной.
В заключение приведу код, создающий модуль с одной внешней процедурой (функцией), вычисляющей факториал. Обсуждения деталей оставим до следующего поста, а пока просто составим впечатление (vm/test/modules.cpp):
void createFactorialModule(Module &module) {
ModuleBuilder builder;
// void fact(unsigned int *io) {
// if(*io)
// goto l1;
// *io = 1;
// return;
// l1:
// {
// unsigned int pr = 1;
// l2:
// pr = *io * pr;
// if(--*io)
// goto l2;
// *io = pr;
// }
// }
VarTypeId vtype = builder.addVarType(8);
RegId io = builder.addReg(0, vtype);
ProcTypeId ptype = builder.addProcType(0, io);
ProcId proc = builder.addProc(PFLAG_EXTERNAL, ptype);
builder.addProcInstr(proc, JNZInstr(io, 3));
builder.addProcInstr(proc, CPI8Instr(1, io));
builder.addProcInstr(proc, RETInstr());
RegId pr = builder.addReg(0, vtype);
builder.addProcInstr(proc, PUSHInstr(pr));
builder.addProcInstr(proc, CPI8Instr(1, pr));
builder.addProcInstr(proc, MULInstr(io, pr, pr));
builder.addProcInstr(proc, DECInstr(io));
builder.addProcInstr(proc, JNZInstr(io, -2));
builder.addProcInstr(proc, CPBInstr(pr, io));
builder.addProcInstr(proc, POPInstr());
builder.addProcInstr(proc, RETInstr());
builder.createModule(module);
}