Пятница, 20.06.2025, 06:41

Приветствую Вас Гость | RSS
Мой сайт
ГлавнаяРегистрацияВход
Меню сайта

Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

Главная » 2015 » Декабрь » 9 » Vinxru : Winapi аналог Getch()
17:41
Vinxru : Winapi аналог Getch()

За прошлую неделю мне не один человек рассказал про MemSQL. Это новая база данных, которая работает исключительно в оперативной памяти. И со слов разработчиковмаркетологов это ноу-хау которое позволяет повысить производительность на несколько порядков. Маркетологи молодцы, все в курсе.

Да, здравый смысл в этом есть. Сейчас 16 Гб оперативной памяти может поставить любой, а этого объема хватит большинству баз данных. Причем, надо понимать, что файлы баз данных на диске занимают в 10-100 раз больше места, чем полезные данные. Если мы будем данные хранить в ОЗУ, то места потребуется меньше.

Почему? Диск позволяет "быстро"читать данные только блоками, притом расположенными подряд. И пытаясь выжать максимум скорости, объем приносится в жертву. И никто его не жалеет. А ОЗУ работает одинаково быстро во всем объеме. Такие жертвы не нужны и даже вредны. ОЗУ все таки мало.

А ненадежность хранения в ОЗУ решается тем, что данные в MemSQL так же записываются на диск :)

Это всё лишь маркетинг. То что они говорят о работе в памяти как о главном факторе - это маркетинг. Потому что в памяти работают многие БД. Из самых популярных: MySQL умеет создавать таблицы в ОЗУ, SQLite то же умеет всю базу держать в ОЗУ (:memory:). SQL-подобная технология LINQ в C# то же работает с данными в памяти.

Более того, печальный факт. Как только размер базы у обычной СУБД превышает размер ОЗУ, производительность начинает сильно падать. Так что уже давно оперативная память наращивается под размер БД.

Что же действительно позволяет обогнать конкурентов, так это грамотное программирование. Видимо парни пораскинули мозгами и написали более оптимальный код. И там есть куда оптимизировать. Многие вещи SQL-сервера делают очень медленно. Например вставку данных в таблицу. Разархивация БД (выполненная на основе INSERT INTO) размером в 50 Мб может длиться несколько часов.

Вот и я подумал, пора бы то же сделать свой маленький быстрый SQL с преферансом и дамами.

JIT



Делаю я его по своей инициативе по выходым, так как это потребует много времени. Неделю или две. А больше двух дней на задачу на работе мне не дают. Но даже если я не сделаю SQL, собственный JIT компилятор мне очень даже пригодится.

До этого момента я писал все компиляторы в байт-код. Интерпретатор байт-кода можно написать на чистом Си, что позволяет компилировать его под любую архитектуру, избежать множества ошибок и легально работать с любым антивирусом. Антивирусы не очень хорошо относятся к модификации исполняемого кода. На идею байт-кода меня натолкнули исходные коды эмулятора процессора Z80 в далеком прошлом :) Я когда то писал свой эмулятор Спектрума, подглядывая в чужие эмуляторы.

Причем, мой интерпретатор байт кода работал не медленнее PHP (язык программирования). И на этом я успокоился. Сейчас же нужна максимальная скорость, поэтому я перейду на уровень машинного кода. И первым моим экспериментом была простейшая программа

// Выделяем память из которой можно выполнять программы. Это основной момент auto code = (unsigned char*)VirtualAlloc(0, 256, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE); VirtualLock(code, 256); // Записываем программу code[0] = 0xC3; // код команды RET // Выполняем ((void(*)())code)();

И она запустилась :) Далее начинаем писать грамотно, точнее пишем компилятор ассемблера. Это пока для 32-х битной версии, а для 64-х битной придется вносить небольшие изменения.

// Все условия перехода enum Cc { JA=7, JAE=3, JB=2, JBE=6, JC=2, JE=4, JG=0xF, JGE=0xD, JL=0xC, JLE=0xE, JNA=6, JNAE=2, JNB=3, JNBE=7, JNC=3, JNE=5, JNG=0xE, JNGE=0xC, JNL=0xD, JNLE=0xF, JNO=1, JNP=0xB, JNS=9, JNZ=5, JO=0, JP=0xA, JPE=0xA, JPO=0xB, JS=8, JZ=4 }; // Используемые регистры enum Reg { EAX=0, ECX=1, EDX=2, EBX=3 }; // Команды ALU enum Alu { ADD=0, OR=1, ADC=2, SBB=3, AND=4, SUB=5, XOR=6, CMP=7 }; class Compiler { protected: unsigned char *codeStart; // Начало кода unsigned char *code; // Конец кода unsigned char *prev; int cmd, cmdA, cmdB; // Для оптимизации enum { CMD_mov_reg_imm=1, CMD_mov_reg_EBX_rel, CMD_mov_reg_reg, CMD_SET_A, CMD_set };// Для оптимизации void s(char opcode) { cmd = 0; prev = code; *code++ = opcode; } void s(char opcode1, char opcode2) { s(opcode1); *code++ = opcode2; } void i(int imm) { *(int*)code = imm; code+=4; } public: Compiler() { prev = code = codeStart = (unsigned char*)VirtualAlloc(0, 256, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE); VirtualLock(code, 256); } void execute() { ((void(*)())codeStart)(); } // Команды без оптимизации void mov_reg_reg(Reg reg1, Reg reg2) { s(0x8B, 0xC0|(reg1<<3)|reg2); cmd=CMD_mov_reg_reg; cmdA=reg1; cmdB=reg2; } void mov_reg_imm(Reg reg, int imm) { s(0xB8|reg); i(imm); cmd=CMD_mov_reg_imm; cmdA=reg; cmdB=imm; } void mov_reg_EBX_rel(int reg, int rel) { s(0x8B, 0x83|(reg<<3)); i(rel); cmd=CMD_mov_reg_EBX_rel; cmdA=reg; cmdB=rel; } void mov_EBX_rel_reg(int rel, Reg reg) { s(0x89, 0x83|(reg<<3)); i(rel); } void push_reg(Reg reg) { s(0x50|reg); } void pop_reg(Reg reg) { s(0x58|reg); } void jcc(Cc cc, int label) { s(0x0F, 0x80|cc); fixup(code, label); i(-4); } void jmp(int label) { s(0xE9); fixup(code, label); i(-4); } void ret() { s(0xC3); } void set(Cc cc, Reg reg) { auto p=code; mov_reg_imm(reg,0); s(0x0F, 0x90|cc); *code++ = 0xC0|reg; prev=p; cmd=CMD_set; cmdA=cc; } // Команды с оптимизацией void needFlags_jcc(Cc cc, int label) { if(cc==JZ && cmd==CMD_set) { code = prev; jcc((Cc)cmdA, label); return; } alu_reg_reg(OR, EAX, EAX); jcc(cc, label); } void alu_reg_imm(Alu alu, Reg reg, int imm) { if(reg==EAX) { /* alu eax, imm */ s(0x05+(alu<<3)); i(imm); return; } /* alu reg1, imm */ s(0x81, 0xC0+(alu<<3)+reg); i(imm); } void alu_reg_reg(Alu alu, Reg reg1, Reg reg2) { /* alu reg1, imm */ if(cmd==CMD_mov_reg_imm && cmdA==reg2 ) { code=prev; alu_reg_imm(alu, reg1, cmdB); return; } /* alu reg1, [ebx+rel] */if(cmd==CMD_mov_reg_EBX_rel && cmdA==reg2 ) { code=prev; s(0x03+(alu<<3), 0x83+(reg1<<3)); i(cmdB); return; } s(0x03|(alu<<3)); *code++ = 0xC0|(reg1<<3)|reg2; } void idiv_reg(Reg reg) { /* idiv [ebx+rel] */ if(cmd == CMD_mov_reg_EBX_rel) { code = prev; s(0xF7, 0xBB); i(cmdA); return; } /* idiv reg */ s(0xF7, 0xF8|reg); } void imul_reg(Reg reg) { /* imul [ebx+rel] */ if(cmd==CMD_mov_reg_EBX_rel && cmdA==reg) { code = prev; s(0xF7); *code++ = 0xAB; i(cmdB); return; } /* imul reg */ s(0xF7); *code++ = 0xE8|reg; } // Метки struct Fixup { unsigned char* code; int label; Fixup() {}; Fixup(unsigned char* _code, int _label) { code=_code; label=_label; } }; std::vector<Fixup> fixups; std::vector<unsigned char*> labels; void fixup(unsigned char* code, int label) { fixups.push_back(Fixup(code, label)); } void label(int label) { labels[label] = code; } int allocLabel() { labels.push_back(code); return labels.size()-1; } void prepare() { for(auto f=fixups.begin(), fe=fixups.end(); f!=fe; f++) *(int*)f->code += labels[f->label] - f->code; } };

Ради облегчения себе жизни в будущем я буду использовать самые простые варианты инструкций. Например команду умножения работающую только с регистрами. А уже компилятор ассемблера (код выше) будет объединять пару простых инструкций в более сложные. Например: MOV EDX, [EBX+?] + MUL EDX => MUL [EBX+?].

И во вторых, этот объект занимается компиляцией меток и переходов. Во время формирования кода, команд перехода, мы не знаем адрес перехода, если метка расположена дальше по коду. Для этого в команде перехода мы вызываем метод fixup(allocLabel()), он запоминает меcто куда надо будет записать адрес. А когда настает очередь метки, мы вызываем label, она запоминает адрес метки. А в конце вызывается метод prepare. Который расставляет все адреса.

Ну и ради теста пишем программу:

struct Item { int a, b; }; std::vector<Item> data; #define OFFSET(X) ((char*)&Item.X-(char*)&Item.a) void do() { Compiler c; c.mov_reg_imm(EBX, (int)data.begin()._Ptr); int l0 = c.allocLabel(); c.mov_reg_EBX_rel(EAX, OFFSET(a)); c.alu_reg_imm(CMP, EAX, 16); int l = c.allocLabel(); c.jcc(JA, l); c.mov_EBX_rel_imm(OFFSET(b), 1); c.label(l); c.alu_reg_imm(ADD, EBX, sizeof(item)); c.alu_reg_imm(CMP, EBX, (int)data.end()._Ptr); c.jcc(JNZ, l0); c.ret(); c.prepare(); c.execute(); }

Эта программа выполняет действие "if(a > 16) b = 1". Выполняется быстро, но пока это совсем не похоже на SQL.

Теперь нужен компилятор c SQL. Пока что компилятор простейшего выражения. Причем, компилятор мог быть маленьким. Но я ради оптимизации решил разделить его на две части. На разбор кода и компиляцию кода собственно.

Заброшенный компилятор Си для 8080 был написан одним куском, что насколько усложнило код, что я год к нему не хотел возращаться. С новой архитектурой всё проще и опять захотелось :)

enum Type { tAcc, tVar, tImm, tStack }; struct Var { Type type; int n; void set(Type _type, int _n) { type=_type; n=_n; } ~Var(); }; class Node { public: Var v; // Переменная или константа Operator o; // Оператор Cc cc; // Условие перехода Node *a, *b; // Аргументы оператора. Надо заменить на list args Node(Var& _v) { o=oNone; a=0; b=0; v=_v; } Node(int& n) { o=oNone; a=0; b=0; v.n=n; v.type=tImm; } Node(Operator _o, Cc _cc, Node* _a, Node* _b) { o=_o; cc=_cc; a=_a; b=_b; } }; Node* parseVar(int level=0) { Node* a; if(p.ifToken("(")) { a = parseVar(0); p.needToken(")"); } else if(p.ifToken(ttInteger)) { a = new Node(p.i); } else if(p.ifToken(varNames)) { a = new Node(vars[p.i]); } else p.syntaxError(); while(true) { Operator o; int l; Cc cc=JZ; if(level<1 && p.ifToken("Or")) l=1, o=oOr; else if(level<2 && p.ifToken("And")) l=2, o=oAnd; else if(level<3 && p.ifToken("<" )) l=3, o=oCmp, cc=JB; else if(level<3 && p.ifToken("<=")) l=3, o=oCmp, cc=JBE; else if(level<3 && p.ifToken(">" )) l=3, o=oCmp, cc=JA; else if(level<3 && p.ifToken(">=")) l=3, o=oCmp, cc=JAE; else if(level<3 && p.ifToken("==")) l=3, o=oCmp, cc=JE; else if(level<3 && p.ifToken("!=")) l=3, o=oCmp, cc=JNE; else if(level<4 && p.ifToken("+" )) l=4, o=oAdd; else if(level<4 && p.ifToken("-" )) l=4, o=oSub; else if(p.ifToken("*")) l=5, o=oMul; else if(p.ifToken("/")) l=5, o=oDiv; else return a; Node* b = parseVar(l); a = new Node(o, cc, a, b); } }

Эта функция строит дерево выражения. Некий аналог Expression Trees в .NET или еще его можно представить как программу на LISP. (это Abstract syntax tree (с) ex0_panet) Помимо разделения кода на две части, это древеро очень удобно оптимизировать. Но этот момент я пока опущу. Компилятор получился не очень большим:

void compileVar(Var* x, Node* n, int jmpIfFalse=-1) { // Это переменная или константа if(n->o==oNone) { *x = n->v; return; } // AND и OR обрабатываем по особому. if(n->o==oAnd || n->o==oOr) { if(x) jmpIfFalse = c.allocLabel(); if(n->o==oOr) { int jmpIfTrue = c.allocLabel(); int elseLabel = c.allocLabel(); compileVar(0, n->a, elseLabel); c.jmp(jmpIfTrue); c.label(elseLabel); compileVar(0, n->b, jmpIfFalse); c.label(jmpIfTrue); } else { compileVar(0, n->a, jmpIfFalse); compileVar(0, n->b, jmpIfFalse); } if(x) { c.mov_reg_imm(EAX, 1); int l = c.allocLabel(); c.jmp(l); c.label(jmpIfFalse); c.mov_reg_imm(EAX, 0); c.label(l); accUsed = x; x->type = tAcc; } return; } // Получаем аргументы оператора Var a; compileVar(&a, n->a); Var b; compileVar(&b, n->b); // Помещаем арументы в EAX, EDX if(b.type==tAcc) push(EDX, b); push(EAX, a); if(b.type!=tAcc) push(EDX, b); // Выполняем оператор switch(n->o) { case oCmp: c.alu_reg_reg(CMP, EAX, EDX); /* Требуется значение */ if(x) c.set(n->cc, EAX); break; case oAdd: c.alu_reg_reg(ADD, EAX, EDX); break; case oSub: c.alu_reg_reg(SUB, EAX, EDX); break; case oDiv: c.idiv_reg(EDX); break; case oMul: c.imul_reg(EDX); break; default: FatalAppExitA(1, ":)"); } if(x) { // Требуется значение accUsed = x; x->type = tAcc; } else { // Требуется условие c.jcc(n->cc, jmpIfFalse); } } // Поместить значение в регистр void push(Reg reg, Var& x, bool freeAcc=false) { switch(x.type) { case tVar: c.mov_reg_EBX_rel(reg, x.n); break; case tImm: c.mov_reg_imm(reg, x.n); break; case tAcc: if(reg!=EAX) c.mov_reg_reg(reg, EAX); break; case tStack: c.pop_reg(reg); break; } x.type = tAcc; if(freeAcc) accUsed=0; }

И его проверка. Здесь мы компилируем выражение (a > b OR a > 18) AND b > c и его результат записываем в Item::result.

// Начало цикла // for(EBX=data.begin(); EBX!=data.end(); EBX++) { c.mov_reg_imm(EBX, (int)data.begin()._Ptr); int l0 = c.allocLabel(); // Компиляция выражения p.load("(a > b OR a > 18) AND b > c"); Var out; compileVar(&out, parseVar()); p.needToken(ttEof); // Сохраняем выражение в EBX->resut push(EAX, out, true); c.mov_EBX_rel_reg(OFFSET(result), EAX); // Конец цикла for(EBX=data.begin(); EBX!=data.end(); EBX++) c.alu_reg_imm(ADD, EBX, sizeof(item)); c.alu_reg_imm(CMP, EBX, (int)data.end()._Ptr); c.jcc(JNZ, l0); c.ret(); // Запуск c.prepare(); c.execute();

Добавить несколько простых строк и этот код уже сможет выполнять запрос типа: SELECT выражение, выражение, выражение FROM таблица WHERE выражение.

Но надо еще сделать поддержку типов данных std::string, double, функций. Это не сложно.

Чуть сложнее будет с компиялцией слов GROUP BY, HAVING. Но я это уже делал, только транслировал не в машинный код в программу на Паскале.

SIMD



Что действительно заставляет задуматься, дак это применение SIMD инструкций (MMX/SSE/AVX/AVX-512). Это инструкции выполняющие одинаковую операцию для несколькими числами одновременно (за один такт). SSE инструкция обрабатывает за такт 128 бит данных, то есть 4 целых 32-х битных числа или 2 дробных 64-битных. Более поздние наборы инструкций AVX или AVX-512 позволяют обрабатывать 256 или 512 бит за раз.

В SQL-запросах одинаковые выражения применяются к миллионам разных строк, поэтому эти инструкции напрашиваются сами собой. Но для них необходимо данные распологать в определенном порядке. Сначала идут значения одного слобика таблицы, затем второго, затем третьего. Это надо, что бы процессор смог одним запросом вытащить из памяти значения сразу нескольких ячеек.

Как то мне один весьма умный человек, преподаватель по программирования в университете, сказал: SIMD тебе не нужен, даже обычные инструкции обрабатывают данные быстрее, чем работает ОЗУ. Прироста скорости не будет...

В прошлом я написал несколько компиляторов Пролога. И очень хотелось написать транслятор Пролога на Си, но это было невозможно, потому что в Прологе программа выполняется весьма необычным способом. Вызов функции может привести к многократному вызову функций по коду ниже. (Правильнее говорить предикат, а не функция). Например, в самом простом случае.

Совершеннолетние_клиенты(Имя) :- Клиенты(Имя, Возраст), Возраст > 18;

Первая строка выполнит многократный вызов второй строки. (Это зависит от компилятора, но предположим самый глупый компилятор). И на Си написать аналог этого примера было сложно. Конечно возможно, но машинный код был понятнее.

Вообще, в Прологе порядок строк может быть любой и на результат это не влияет. Каждая строка порождает множество вариантов и все они объединяются по И. Программа выше звучит как "Все что является клиентом"и "Всё что больше 18 лет".

Лямбда функции, появившиеся недавно в C++, позволяют красиво, то есть без хаков, представить на С++ программу на Прологе. Возьмем пример из Википеди (мне лень искать что то более интересное). Программа поиска общего делителя для массива чисел:

% Верно, что НОД (A, 0) = A gcd2(A, 0, A). % Верно, что НОД(A, B) = G, когда A>0, B>0 и НОД(B, A % B) = G (% - остаток от деления) gcd2(A, B, G) :- A>0, B>0, N is mod(A, B), gcd2(B, N, G). gcdn(A, [], A). gcdn(A, [B|Bs], G) :- gcd2(A, B, N), gcdn(N, Bs, G). gcdn([A|As], G) :- gcdn(A, As, G).

На C++ с Лямбдами это будет выглядеть так:

void gcd2(int a, int b, std::function<void(int)> result) { // Верно, что НОД (A, 0) = A if(b==0) result(a); //Верно, что НОД(A, B) = G, когда A>0, B>0 и НОД(B, A % B) = G (% - остаток от деления) if(a>0 && b>0) gcd2(b, a % b, result); } void gcdn(int a, int* bb, int* be, std::function<void(int)> result) { if(bb==be) result(a); else gcd2(a, *bb, [&](int n) { gcdn(n, bb+1, be, result); }); } void gcdn(int* xb, int* xe, std::function<void(int)> result) { gcdn(*xb, xb+1, xe, result); } void test() { vector<int> numbers; numbers.push_back(36); numbers.push_back(6); numbers.push_back(10); gcdn(numbers.begin(), numbers.end(), [&](int n) { cout << n; }); }

В принципе, можно компилировать с Пролога на Си красиво

А почему бы сразу не писать так? Компилятор пролога строит план выполнения, переставляет строки, объединяет предикаты, выбрасывает лишнее. А еще определяет, какая переменная будет входящей, а какая исходящей. В предикате A=B*C, входящей может быть любая переменная и даже все сразу. Вот за этим он и нужен.

А пригодится всё это для построения оптимизаторов.

ОБЖ мне нравилось. На нём мы проводили репетицию атомной войны. Нам рассказывали про атомные взрывы, что во время взрыва лучше всего лечь в ванную. Она защитит от проникающей радиации и от упавшего перекрытия. И рассказывали как выжить в постапокалиптическом мире :) Про время распада радиоактивного йода. Рассказывали, какие таблетки надо поедать. Каждые пол года в городе объявлялась учебная тревога и мы ровными рядами бежали из школы до ближайшего бомбоубежища. Тревога реально внушала страх - вой сирен по всему городу.

Помимо атомной угрозы, в городе были еще больше запасы хлора и аммиака. В случае аварии, облака ядовитого газа закроют город. От хлора надо бежать на крыши, а от аммиака наоборот лечь на пол. И нас учили как в домашних условиях изготовить простейшую защиту органов дыхания.

И это мне пригодилось! Однажды дома я открыл банку с раствором хлора (или жидким хлором), мне стало плохо и я тут же выбежал из комнаты. У меня чуть глаза и лёгкие не "лопнули". И вот комната закрыта, я стою снаружи и думаю, а как оттуда банку забрать?

Я взял тазик, налил воды, растворил соду. В этом растворе вымочил полотенце. На голову одел прозрачный пакет, так что бы он закрывал только глаза. А рот (нижнюю часть головы) обмотал этим пропитанным содой полотенцем. И вполне комфортно пошел в комнату, открыл окна и закрыл банку.

Но ОБЖ-шник был типичным воякой. И всё у нас было как в армии, даже дедовщина :)

У нас в классе самыми важными были второ- и тертьегодники. Во первых они были испорченными, а это привлекало. Например, они закидывали мощные петарды, коробки с казеином или газеты пропитанные селитрой в окна переполненных отъезжающих автобусов. Во вторых, они всем рассказывали ощущения от приёма наркотиков и где их купить. В частности ощущения от Тарена, украденного из аптечек АИ-2, которые нам показывали на ОБЖ. Вокруг них собирались целы толпы учеников. А в третьих, их крышевали старшие братья, поэтому их даже коллективно бить было страшно. Гопники гороче.

И вот в один из обычных школьных дней, прямо в коридоре учебного заведения эти гопники начали пинать одного из одноклассников. Заставляли его маршировать и пинали под зад. А если он отказывался, то били уже кулаками.

По коридору идёт ОБЖ-шник и спрашивает:
- Что тут происходит?

Я думаю, ура, их накажут! Но ОБЖ-шик то же присоединился к ним и стал объяснять:

- Мы учим его маршировать!
- Молодцы! Что ты двигаешься как Буратино! Ноги выше!

И пнул его. Ну и вы понимаете, какая кличка была у человека после этого. Я тогда офигел. ОБЖ-шник был своим мужиком для гопников, он никогда не стучал завучу на класс. Даже когда один из блатных учеников прямо на уроке сказал:

- Ты говнюк! И что бы мне сделаешь?
- Да как ты со мной разговариваешь, я капитан второго ранга!
- Сифон у тебя второго ранга. Что ты сделаешь старая гнида?

ОБЖ-шник отвернул глаза и продолжил урок.

В преддверии термоядерной войны я решил почитать, что нового изобрели ученые и политики. Ведь атомные бомбы изобрели в 1950-х годах. Пора изобретать новое оружие. Представьте технику 1960-х годов! И атомные бомбы прямиком оттуда. Нам нужны новые бомбы 21-ого века. Они будут еще мощнее, еще ядовитее, еще разрушительнее! При этом будут обладать интеллектом и никакая ПРО их не остановит. И это будущее уже не за горами:

Во первых, договор СНВ-III был подписан Россией и США в Праге в 2010г можно считать разорванным. В течении 10 лет стороны должны сократить ядерные боезаряды до 1550 единиц, а межконтинентальные баллистические ракеты подводных лодок и тяжелых бомбардировщиков - до 700 единиц. Поскольку США разорвало отношения между военными ведомствами США и России, нормальных регулярных двухсторонних контактов по соблюдению договоров быть не может и договор можно считать разорванным.

Во вторых, Россия и США давно уже проводят ядерные испытания. Только теперь они называются "неядерными ядерными испытаниями"или "подкритическими ядерными взрывами". Суть в том, что бомба содержит недостаточное для цепной реакции количество радиоактивного материала. После нажатия на красную кнопку, все так же происходит химический взрыв приводящий к обжатию плутония. Так же ускоряется расщепление плутония, но ускоряется недостаточно для моментального взрыва.

Ученые из такой модели могут получить гораздо больше полезных сведений, чем от полноценного взрыва. После взрыва от бомбы ничего не остается. А в случае подкритичного ядерного испытания, обжатый плутоний можно исследовать и использовать в следующей бомбе. Можно однозначно спрогнозировать силу ядерного взрыва.

Но и мирный атом не стоит на месте. Китай продает ядерные светящиеся брелки, их уже все видели. А вот ядерные батарейки, я увидел сегодня впервые. Такую бы в смартфон!

H-DOLBAQ1zE[1]

http://item.taobao.com/item.htm?spm=a1z10.1.4004-56656384.29.dysKyM&id=15659486189

Говорят, что батарейка дает 3 Вольта, 50.0 мкВт / 3 = 16 мкА в течении 20 лет. Для такой цены это мало. Но все когда-то начиналось с заоблачных цен и микроскопического кпд.

Переделал все на новый стандарт. Осталось командер отладить (глючит). Ну и потом добавить эмулятор магнитофона.

Работает на удивление шустро. Скорость можно глянуть тут:


UPD: Надо отключать автоматическую регулировку яркости при съёмке монитора :)

Сначала запускается SD STARTER, программа размером меньше 100 байт. Она всего лишь запускает файл SD BIOS (boot/sdbios.rks).

Идея в том, что SD STARTER потом будет загружаться через порт магнитофона (эмулятором магнитофона спрятанном внутри контроллера). И хранится на флешке в файле boot/boot.rks. Благодаря этому не требуется изменение стандартного ПЗУ компьютера. То есть не нужно дорабатывать компьютер. Скорость загрузки с магнитофона не очень большая, всего 150 байт/сек, поэтому эту программу я сделал максимально маленькой.

Но лично я засунул SD STARTER в ПЗУ. Так компьютер запускается быстрее и порт магнитофона остается свободным. Самое большое неудобство программ в ПЗУ, это невозможность исправлять ошибки. И тут маленький объем стартера (и минимум функций) как нельзя кстати. Меньше вероятность наделать ошибок. Новые функции в нем никогда не появятся. И я надеюсь, что мне его никогда не придется переделывать.

Что не скажешь о SD BIOS-е. Это набор подпрограмм для работы с контроллером.

Стартер запускает БИОС. БИОС получает версию контроллера, выводит её на экран. То есть на экране видно три строки:

SD STARTER 1.0
SD BIOS 1.0
SD CONTROLLER 1.0

БИОС состоит из двух частей. Резидентной, которая всегда должна находится в памяти. Это собственно подпрограммы для работы с контроллером. И часть, выполняющая запуск компьютера, которая потом уничтожается. Эта часть рисует на экране изображение флешки, что то пишет и запускает файл boot/shell.rks, который является командором.

Вместо любого файла (boot, sdbios, shell) можно подсунуть любую игру/программу. Она будет работать, только БИОСА в памяти не будет. А он может быть и не нужен.

После запуска БИОСА карта памяти выглядит так:

С самого начала памяти свободно 35 537 байт памяти (до 8AD1h). Дальше находятся переменные SELF_NAME, CMD_LINE. Если они не нужны программисту, то объем непрерывной свободной памяти составляет ~36 049 байта (до 8CD1h).

8AD1h SELF_NAME Собственное имя файла 256 байт (этот адрес содержится в DE после запуска) 8BD1h CMD_LINE Командная строка 256 байт (этот адрес содержится в HL после запуска) 8CD1h SD_BIOS_CODE Код SD BIOS 559 байт (возможно будет расти в начало сдвигая SELF_NAME и CMD_LINE) 8F00h - Не используется ~233 байта (возможно тут есть переменные монитора) 8FDFh MONITOR_VARS Переменные монитора 9000h VIDEO_MEM Видеопамять

Свободное место по адресу 8F00h оставлено на всякий случай. Во первых, что бы не конфликтовать с Монитором. Возможно, что будущие версии SD BIOS будут использовать эту память.

SD BIOS может быть в любом месте памяти. При запуске программы регистр A содержит версию набора команд (сейчас 1). BС содержит точку входа. HL командную строку. DE собственное имя.

Для вызова функции контроллера, надо поместить в регистр A код функции и вызвать подпрограмму по адресу переданному при старте программы в регистре BC.

Функции контроллера:

Reboot Теплая перезагрузка (A=0, HL="", DE="" / A=код ошибки) Exec Запустить программу (A=0, HL=имя файла, DE=командная строка / A=код ошибки) FindFirst Начать получение списка файлов (A=1, HL=путь, DE=максимум файлов для загрузки, BC=адрес / HL=сколько загрузили, A=код ошибки) FindNext Продолжить получение списка файлов (A=1, HL=":", DE=максимум файлов для загрузки, BC=адрес / HL=сколько загрузили, A=код ошибки) Open Открыть файл (A=2, D=0, HL=имя файла / A=код ошибки) Create Создать файл (A=2, D=1, HL=имя файла / A=код ошибки) MkDir Создать папку (A=2, D=2, HL=имя файла / A=код ошибки) Delete Удалить файл/папку (A=2, D=100, HL=имя файла / A=код ошибки) Seek Установить позицию чтения записи файла (A=3, B=режим, DE:HL=позиция / A=код ошибки, DE:HL=позиция) (С начала B=0, с текущего положения B=1, с конца B=2) GetSize Получить размер файла (A=3, B=100 / A=код ошибки, DE:HL= размер файла) Swap Работа с двумя открытыми файлами (A=3, B=200 / A=код ошибки) Read Прочитать из файла (A=4, HL=размер, DE=адрес / A=код ошибки, HL=сколько загрузили) Write Записать в файл (A=5, HL=размер, DE=адрес / A=код ошибки) WriteEOF Конец файла (A=5, HL=0 / A=код ошибки) Move Переместить файл/папку (A=6, HL=из, DE=в / A=код ошибки) GetFree Размер флешки и свободное место на флешке (A=7, HL=буфер / A=код ошибки)


Давно, давно, давно надо было сделать этот контроллер. Но в начале этого года отвлек меня SysCat, подарив плату Специалиста. :) И все же возвращаюсь. Первая версия платы выглядит так, схема в самом низу.



Все просто. Контроллер подключается к параллельному порту. Контакты расположены так, что бы подключаться к Апогею БК01. А внутри МК находится эмулятор ПЗУ и запустить программу оттуда можно с помощью стандартной директивы R0,100 G. Загруженная программа уже будет работать с драйвером файловой системы FAT32, которые так же находится внутри МК.

Протокол будет таким:

Сначала на шину адреса устанавливаем значения 19, 23, 87. Они отличаются одним битом, поэтому шума между ними быть не должно.

Далее, компьютер читает адреса 86,87,86,87,86,87... каждый байт 86 является тактовым импульсом, т.е. подтверждением приема/передачи байта.

Если синхронизация удалась, то МК выставляет на шину данных ERR_START, а после тактового импульса ERR_OK_WAIT. Код ERR_OK_WAIT может держаться неопределенное кол-во тактов. В это время происходит инициализация флешки.

Потом ERR_OK_WAIT меняется на ERR_OK_NEXT если все нормально. Или возвращается код ошибки.

Компьютер должен переключить порт на выход и передать команду. В примере ниже команда BOOT - единственный байт 0

На это МК отвечает ERR_OK_WAIT (много раз), ERR_OK_ADDR, адрес загрузки L, адрес загрузки H, ERR_OK_WAIT (много раз)

Потом выдает содержимое файла блоками: 0, длина L, длина H, данные (HL раз), ERR_OK_WAIT (много раз)

Если данных не осталось, выдает ERR_OK_READ или код ошибки

Любое значение на шине адреса кроме 86, 87 вызовет отключение SD контроллера и включение эмулятора ПЗУ. Поэтому начальный загрузчик всегда можно загрузить директивой R0,100

Код: .org 0h DATA_PORT = 0EE00h ADDR_PORT = 0EE01h CTL_PORT = 0EE03h SEND_MODE = 10001011b ; Настройка: 1 0 0 A СH 0 B CL 1=ввод 0=вывод RECV_MODE = 10011011b ERR_START = 040h ERR_WAIT = 041h ERR_OK_DISK = 042h ERR_OK = 043h ERR_OK_READ = 044h ERR_OK_ENTRY = 045h ERR_OK_WRITE = 046h ERR_OK_ADDR = 047h Entry: ; Первым этапом происходит синхронизация с контроллером ; 256 попыток. Для этого в регистр C заносится 0 ; А в стек заносится адрес перезагрузки 0C000h LXI B, 0F800h PUSH B JMP Boot NOP ;---------------------------------------------------------------------------- ; Отправка и прием байта Rst1: INX H ; HL = ADDR_PORT MVI M, 86 MVI M, 87 DCX H ; HL = DATA_PORT MOV A, M RET ;---------------------------------------------------------------------------- ; Ожидание готовности МК Rst2: WaitForReady: Rst 1 CPI ERR_WAIT JZ WaitForReady RET ;---------------------------------------------------------------------------- RetrySync: ; Попытки DCR C RZ ; Ошибка синхронизации, перезагрузка Boot: ; Режим передачи (освобождаем шину) и инициализируем HL MVI A, RECV_MODE CALL SetMode ; Начало любой команды MVI M, 19 MVI M, 23 MVI M, 87 ; Если есть синхронизация, то контроллер ответит ERR_START Rst 1 CPI ERR_START JNZ RetrySync ; Дальше будет ERR_OK_WAIT, ERR_OK_NEXT ; Инициализация флешки Rst 2 CPI ERR_OK_ADDR JNZ Rst1 ; Ошибка, освобождаем шину и перезагрузка ; ERR_OK_NEXT высталенный МК будет висеть на шине до следующего RST, ; только после него МК освободит шину и мы сможем включить режим передачи. Rst 1 ; Режим передачи MVI A, SEND_MODE CALL SetMode ; Код команды XRA A Rst 1 ; МК читает данные во время тактового импульса, т.е. он уже их прочитал. ; Включаем режим приема, т.е. освобождаем шину. ; Режим приема MVI A, RECV_MODE CALL SetMode ; МК захватит шину во время тактового импульса (первого RST) ; Дальше будет ERR_OK_WAIT, ERR_OK_RKS Rst 2 CPI ERR_OK_RKS JNZ Rst1 ; Ошибка, освобождаем шину и перезагрузка ; Удаляем из стека 0F800h POP B ; Адрес загрузки в BC Rst 1 MOV C, A Rst 1 MOV B, A ; Сохраняем в стек адрес запуска PUSH B ; Подождать, пока МК прочитает очередной блок денных RecvLoop: Rst 2 CPI ERR_OK_READ JZ Rst1 ; Всё загружено, освобождаем шину и запуск ORA A JNZ 0F800h ; Ошибка, перезагрузка (не отпускаем контроллер) ; Принять очередной блок Rst 1 MOV E, A Rst 1 MOV D, A RecvBlock: Rst 1 STAX B INX B DCX D MOV A, E ORA D JNZ RecvBlock JMP RecvLoop ; Прием/передача SetMode: LXI H, CTL_PORT MOV M, A DCX H DCX H ; HL = ADDR_PORT RET .End

108 байт.

Схема

(RESET забыл :)

https://github.com/vinxru/MXOS

Исправленные недоработки.

В файле DOS.SYS
● BIG_MEM - Поддержка ДОЗУ большего объема, чем 64 Кб
● ROM_64K - Размер ПЗУ у Специалиста MX2 всего 64 Кб, но без этой опции будет работать лишь 32 Кб
● LOAD_FONT - Загружать шрифт в ОЗУ (ускорение работы и возможность загрузки ОС с любого накопителя)
● DISABLE_COLOR_BUG - Включить инициализацию контроллера цвета при запуске

В файле NC.COM
● ENABLE_COLOR - Включить цвет
● BIG_MEM - Поддержка ДОЗУ большего объема, чем 64 Кб
● DISABLE_FREE_SPACE_BUG - Исправить ошибку определения свободного объема


О чем я вообще?


MXOS - это название операционной системе я дал сам, поскольку оригинального названия я не нашел. Народ её называл командер, а внутри оперционки содержатся лишь строки: BIOS 4.40, COMMANDER VERSION 1.4, (C) OMSK 1992.

MXOS это альтернативная операционная система для компьютера Специалист МХ. Оригинальна операционная система называлась RamFOS.

В отличии от RamFOS эта операционная система написана более грамотно. Для доступа к накопителям используются драйвера. Чтение и запись происходит 256 байтными блоками. В RamFOS доступ был побайтный и при том частый и хаотичный, что не позволяло подключить дисковд или SD карту в качестве накопителя.

Так же MXOS более шустрая и обладает более приятным интерфейсом напоминающим Norton Commander. Любое расширение файла можно привязать к любой программе. Список соответствий хранится в файле NC.EXT, максимальный размер которого 36 Кб.

Но при этом MXOS содержит меньше сервисных возможностей и не совместима с RamFOS и стандартным Специалистом. Основные отличия, это:

● Нет поддерживаемых системой верхней и нижней строки состояния
● Система не содержит форму для открытия файла
● Нет поддержки звука на основе ВИ53
● Нет поддержки цвета (хотя и RamFOS его полноценно не поддерживал и не использовал вообще)
● Поддерживается значительно меньше специальных кодов при выводе на экран
● Поддерживается лишь одна модель принтера
● Оригинальная ОС поддерживает лишь 64 Кб дополнительного ОЗУ (что в сумме дает 128 Кб), которое используется как RAM-диск. Запуск с большим объемом памяти приведет к зависанию, так как в порт выподра страницы записывается случаное число. Исправленная версия ОС поддерживает 448 Кб дополнительной памяти, т.е. 7 страниц как накопители B-H.

Многие возможности можно реализовать дополнительными модулями. Например, в комплекте идет драйвер ПЗУ подключаемого к порту расширения. Таким же образом могла быть выполнена поддержка дисковода, но драйвера у меня нет.

Поддерживаются две кодировки KOI-7 и KOI-8, переключаемые как с клавиатуры, так и ESC-последовательностями (ESC+'('и ESC+')'). Знакогенератор содержит 256 символов.

Используется раскладка клавиатуры Стандартного специалиста. У MX отличаются коды клавиш F1-F10, TAB, ESC.

MXOS поддерживает BAT-файлы и передачу аргументов запускаемым программам. При холодной перезагрузке запускается файл B:AUTOEX.BAT, затем A:FORMAT.COM B:, а затем A:NC.COM. Устройство A: - это ПЗУ, устройство B: - это оперативная память. Максимально поддерживается 8 устройств.

При создании собственного ПЗУ (загрузочного диска A:) вы можете разместить AUTOEX.BAT так же и на диске A:, а в нем разместить запуск драйверов.

Запуск MXOS


Если при запуске компьютера зажать клавишу ?, то MXOS сразу перейдет к загрузке программы с магнитофона. Если при запуске компьютера зажать клавишу ?, то MXOS пропустит запуск B:AUTOEX.BAT.

При перезагрузке инициируемой программами запускается лишь A:NC.COM.



Имя файла состоит из 6+3 символов. Ввод расширения при запуске файла из ком. строки обязателен. Папки самой операционной системой не поддерживаются, но это можно реализовать через драйверы. Максимальное кол-во файлов в папке - 48 шт, но оболочка поддерживает отображение лишь 36 файлов. Остальные файлы вы не увидите, не сможете выполнять над ними действия из оболочки, но сможете запустить их ком строки. Максимальный размер файла - 64 Кб, но оболочка может работать лишь с ~36 Кб. При попытке скопировать (и т.п.) файл большего размера произойдет переполнение буфера, уничтожение системных переменных, затем экрана, затем самой ОС в зависимости от размера файла. Структура хранения файлов напоминает FAT, только используются 8 битные номера кластеров.

В отличии от "монитора"стандартного Специалиста, MXOS не содержит режима работы с консоли и соответственно директив вводимых с клавиатуры. Вся работа происходит в диалоговом режиме.

Оболочка поддерживает две панели, как Norton Commander.



Клавиши выполняют следующие команды:

● ESC - Очистка ком строки
● TAB - Переход между панелями
● F1, F2 - Выбор накопителя для левой и правой панели
● F3 - Отображение на неактивной панели информации о накопителе активной панели (как на фото выше)
● F4 - Запуск внешнего редактора E.COM для выбранного файла
● F5 - Копирование файла
● F6 - Изменение имени файла / перемещение
● F7 - Загрузка файлов с ленты (магнитофона) на накопитель
● F8 - Удаление файла
● F9 - Сохранение файла с накопителя на ленту

Есть и другие клавиши, но я пока не разбирался

Раскрасил командер



Добавил ключ ENABLE_COLOR, который включает эту раскраску.




Ядро (DOS.SYS)



Представляет собой набор подпрограмм по адресу C800h. Это подпрограммы обслуживания
экрана, принтера, клавиатуры и накопителя на магнитой ленте и файловой системы.



Зеленым отмечены функции соврапающие со стандратным Специалистом. Но галвная проблема
невосместимости - Аппаратная. Адрес порта клавиатуры у MX изменен.



C800h reboot Запуск файла A:NC.COM C803h getch Ввод символа с клавиатуры; A-код C806h tapeRead Ввод байта с ленты; A-код; если ошибка, то происходит переход на адрес по адресу 8FE1h C809h printChar Вывод байта на экран; C-код C80Сh tapeWrite Вывод байта на ленту; C-байт C80Fh input Ввод строки с клавиатуры C812h keyScan Ввод кода нажатой клавиши; A-код; иначе-0FFh C815h printHexByte Вывести 16-ричное число на экран; A-число C818h printString Вывод строки символов на экран, до нулевого байта; HL-начало строки C81Bh keyScan Ввод кода нажатой клавиши; A-код; иначе-0FFh C81Eh getCursorPos Получить координаты курсора в HL (координаты в пикселях) C821h setCursorPos Установить координаты курсора из HL (координаты в пикселях) 0C824h tapeLoad Загрузить программу с ленты 0C827h tapeSave Сохранить программу на ленту 0C82Ah calcCrc Расчет контрольной суммы C82Dh printHexWord Вывести 16-ричное число на экран; HL-число C830h getMemTop Получить объем доступной памяти; HL-объем C833h setMemTop Установить объем доступной памяти; HL-объем C836h printer Вывод байта на принтер C838h - Переход на 0C800h C83Сh reboot2 Запустить A:NC.COM (стандартную точку С800h можно изменить) C83Fh fileList Получить список файлов C842h fileGetSetDrive Получить/установить активное устройство C845h fileCreate Создать файл C848h fileLoad Загрузить файл по адресу из заголовка этого файла C84Bh fileDelete Удалить файл C84Eh fileRename Переименовать файл C851h fileLoadInfo Загрузить информацию о файле C854h fileGetSetAddr Получить/установить адрес загрузки файла C857h fileGetSetAttr Получить/установить атрибуты файла C85Ah fileNamePrepare Преобразовать имя файла во внутренний формат C85Dh fileExec Запустить файл C860h installDriver Установить драйвер накопителя C863h diskDriver Драйвер выбранного диска C866h fileLoad2 Загрузить файл по адресу

Стандартные служебные ячейки



8FE1h 2 tapeError Адрес куда происходит переход при ошибке чтения с ленты 8FE3h 2 tapeAddr Адрес программы загруженной с ленты 8FE7h 2 charGen Адрес альтернативного знакогенератора; адрес необходимо разделить на 8 8FE9h 1 cursorCfg Внешний вид курсора (7 - бит видимость, 654 - положение, 3210 - высота) 8FEAh 1 koi8 0FFh=включен KOI-8, 0=включен KOI-7 8FEBh 1 escMode Обработка ESC-последовательности 8FECh 1 keyLocks Состояние клавиш CAPS LOCK и РУС/LAT 8FEFh 2 lastKey Две последние нажатые клавиши 8FF1h 2 beep Длительность и частота звукового сигнала 8FF4h 1 repeat Задержка повтора клавиш 8FFAh 2 inverse 0=нормальный текст, 0FFFFh=инверсный текст 8FFCh 1 cursorY Положение курсора по вертикали в пикселях 8FFDh 1 cursorX Положение курсора по горизонтали в пикселях / 2 8FFEh 1 writeDelay Скорость при записи на ленту 8FFFh 1 readDelay Скорость при чтении с ленты

Карта памяти



0000h - 8FDEh 36830 байт Свободная память 8FDFh - 8FFFh 33 байта Системные переменные 9000h - BFFFh 12 Кб Экран C000h - СFFFh 4 Кб DOS.SYS (после ~CE94h свободно) D000h - E1FFh 4608 байт NC.COM (после ~E11Bh свободно) / FORMAT.COM E200h - FAFFh 6 Кб Резерв под драйвера FB00h - FDFFh 768 байт Дисковый буфер FF00h - FF81h 130 байт Коммандная строка. Заполняется fileExec FF82h - FFC0h 62 байта Стек FFC0h - FFEFh 32 байта DOS.SYS FFD0h - FFFFh 32 байта Оборудование

Формат файловой системы FAT8



Оригинальная ОС поддерживает лишь 64 Кб ДОЗУ и при запуске с большим объемом зависнет. Это происходит из за того, что
в регистр страницы ДОЗУ записывается случайное значение.

Исправленная ОС (опция BIG_MEM) по умолчанию 7 первых страниц ДОЗУ отображает как накопители B-H.</p>

Операционная система адресует диски 256 байтными блоками. А так как последние 64 байта адресного пространства всегда занимают
основное ОЗУ и устройства, то целых блоков получается 255.

То есть операционная система не использует последние 192 байта памяти. Только NC.COM в конце нулевой страницы (FF00h-FF0Ah) хранит своё состояние.

Файловая система подобна FAT. Накопитель максимальным объемом 64 КБ разбит на 256 блоков по 256 Кб.

Первые 4 блока содержат служеюную информацию. Нулевой блок таблицу FAT, следующие 4 блока - каталог.

Таблица FAT содержит 256 чисел. Число 5 по адресу 8, значит что за 8 блоком следует читать 5-ый блок. Последний блок замыкается сам на себя, то есть у последнего блока в ячейке 7 должно быть число 7. Свободным блокам в таблице FAT соответствует число 0. Первые 4 числа в таблице не используются, как и не используются блоки с нмоерами 0-3 для хранения файлов.

Каталог находящийся в блоках 1-3 содержит список файлов. 48 файлов по 16 байт на каждый. Если первый байт имени файла FFh,
значит файл не существует.

Структура записи следующая:

6 байт - имя файла 3 байта - расширение файла 1 байт - атрибуты файла 2 байт - адрес загрузки файла 2 байт - длина файла - 1 1 байт - ? 1 байт - первый кластер файла

После включения компьютера процессор начинает выполнять программу с начала ПЗУ. Эта ПЗУ имеет такой же формат как и ДОЗУ и
представлена в системе диском A:

Первые 4 байта ПЗУ, то есть таблицы FAT содержат команду перехода. Сам же загрузчик может обычно размещаться в конце каталога. Если первый символ имени файла FF, то остальные 15 байт записи могут содержать произвольные данные.

Но загрузчик можно размещать в любом блоке, который может быть даже помечен как свободный. При использовании неизменного ПЗУ это никогда не приведет к ошибке.

В оригинальной системе ПЗУ по адресам 800h-FFFh олжно содержать знакогенератор. Перед выводом каждого символа на экран, этот
символ будет копировать из ПЗУ в ОЗУ. Что не только медленно, но и не позволяет отвязать систему от ПЗУ.

И еще хвала TASM :)



Здорово, что в tasm есть макросы из языка Си. Код получается на порядок понятнее.

#define G_WINDOW(X,Y,W,H) .db 2, Y, 90h+(X>>3), H-6, (W>>3)-2 #define G_HLINE(X,Y,W) .db 1, Y, 90h+(X>>3), (((X&7)+W+7)>>3)-2, 0FFh>>(X&7), (0FF00h>>((W+X)&7)) & 0FFh #define G_VLINE(X,Y,H) .db 3, Y, 90h+(X>>3), H, 80h>>(X&7) g_filePanel: G_WINDOW(0, 0, 192, 230) ; было 2, 0, 90h, 0E0h, 16h G_HLINE(4, 208, 184) ; было 1, 0D0h, 90h, 16h, 0Fh, 0F0h G_VLINE(96, 3, 205) ; было 3, ?, 9Ch, 0CDh, 80h .db 0 g_infoPanel: G_WINDOW(0, 0, 192, 230) ; было 2, 0, 90h, 0E0h, 16h G_HLINE(4, 31, 184) ; было 1, 1Fh, 90h, 16h, 0Fh, 0F0h
Просмотров: 345 | Добавил: supoinclus | Рейтинг: 0.0/0
Всего комментариев: 0
Вход на сайт

Поиск

Календарь
«  Декабрь 2015  »
Пн Вт Ср Чт Пт Сб Вс
 123456
78910111213
14151617181920
21222324252627
28293031

Архив записей

Друзья сайта
  • Официальный блог
  • Сообщество uCoz
  • FAQ по системе
  • Инструкции для uCoz


  • Copyright MyCorp © 2025Бесплатный хостинг uCoz