Рассылка закрыта
При закрытии подписчики были переданы в рассылку "Для бухгалтера: программы, новости, советы" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
← Февраль 2004 → | ||||||
1
|
||||||
---|---|---|---|---|---|---|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
10
|
11
|
12
|
13
|
14
|
15
|
|
17
|
18
|
19
|
20
|
21
|
22
|
|
23
|
24
|
25
|
26
|
27
|
28
|
29
|
Статистика
0 за неделю
Разработка операционных систем - для начинающих и не только!
Информационный Канал Subscribe.Ru |
Разработка операционных систем
Выпуск 25 от 2004-02-16
Возрадуйтесь, страждущие по практическим примерам, ибо сегодня жажда ваша утолится и изопьете до дна вы чашу низкоуровнего кода :)
А если серьезно, то вашему вниманию предлагается выпуск рассылки, который подготовил nvm ( http://math.nsc.ru/LBRT/i1/nedelko/ruswin.html), за что ему большой респект :)
Рассматривается пример кода, реализующего переключение задач.
Этот пример не минимальный, поскольку основан на материалах разработки
прототипа ОС
и реализует некоторые функции более общего назначения, чем необходимо для простого переключения задач.
В частности, достаточно детально реализована функция монтирования страниц памяти.
Этот код будет загружен в качестве отдельной задачи. Файл Defs.h Файл Map.h Распределение памяти ядра.
Данные и код (все, кроме кучи) находятся в пространстве, где физические адреса совпадают с линейными.
Приведенная карта памяти рассчитана на использование в прототипе ОС, и некоторые элементы в данном примере не используются.
В том числе не используется точка монтирования TSS_APP_LINK, вместо этого TSS задачи располагается по фиксированному адресу.
Данный модуль агрегирует компоненты и осуществляет передачу управления.
Предполагается, что он начинает работу уже в защищенном режиме, но с отключенным страничным преобразованием.
В начале кода помещается адрес точки входа в программу, поэтому загрузчик должен осуществлять косвенный переход.
В тело модуля непосредственно вставлен код задачи, на которую в конце передается управление.
Передача управления делается командой jmp с указанием селектора TSS, при этом последующий адрес (смещение) не используется, так как EIP загружается из TSS.
Модуль разбит на две функции. Их вполне можно объединить в одну, так как они вызываются последовательно.
Функция kernel_main() первым шагом проводит очистку экрана.
Далее вычисляется и выводится объем доступной памяти.
Метод вычисления основан на последовательном переборе адресов, с попыткой записи в начало каждой страницы и последующим чтением.
Далее создаются пустые каталог разделов и каталог страниц первого раздела, после чего второй подключается к первому.
В пределах первого мегабайта проводится монтирование страниц один-к-одному, т.е. каждая страница монтируется на физический адрес, равный линейному.
Для проверки содержимое начальных элементов каталогов выдается на экран.
Наконец, ассемблерная вставка загружает регистр CR3 и включает страничное преобразование (бит PE).
Последней вызывается функция обновления GDT, для подготовки к переключению задач.
Далее выполняется kernel_last().
В начале этой функции помещена демонстрация работы перемонтирования страниц:
видеобуфер монтируется на свободный адрес и уже по этому адресу выводится символ '!'.
Наконец, вызывается функция, подготавливающая сегмент статуса для новой задачи.
Файл Process.h
Определяем структуру сегмента статуса задачи (TSS).
Файл Process.c
Функция init_GDT() подготавливает и загружает глобальную таблицу дескрипторов.
Первые два - это дескрипторы сегментов кода и данных, такие же, что использовались для перехода в защищенный режим.
Следующий - дескриптор TSS для исходной задачи (той, откуда будем переключаться).
К соответствующему TSS обращение будет вестись только на запись (так как возвращаться мы не планируем),
поэтому выделим под него свободную область, не инициализируя ее.
И, наконец, дескриптор TSS целевой задачи. Под него также выделим свободную область (TSS_APP), которую проинициализируем позже.
Теперь ассемблерной вставкой загружаем GDT и TR.
Последнее необходимо для сохранения состояния текущей задачи.
Функция init_TSS() подготавливает TSS для новой задачи.
Все регистры необходимо загрузить корректными значениями, в частности
в EIP поместить 4 - адрес точки входа, в CR3 - адрес созданного нового каталога страниц.
В адресное пространство задачи по адресу 0 монтируется страница с кодом,
по адресу 0x1000 выделяется память под стек, а следом монтируется видеобуфер.
Также необходимо смонтировать GDT, причем по тому же адресу, что и в текущей задаче.
Файл Memory.h Файл Memory.c
Модуль памяти отвечает за выделение физических и монтирование логических страниц.
Освобождение памяти не предусмотрено, поэтому выделение физических страниц реализовано просто: через адрес начала области свободной памяти.
Функция монтирования страниц имеет три параметра:
Одна из трудностей реализации данной функции состоит в том, что каталог страниц, в который нужно внести изменения, может быть недоступен в адресном пространстве ядра,
и требуется провести его временное монтирование. Для этого функция вызывается рекурсивно.
Для того, чтобы изменение страничного назначения вступило в силу, используется команда invlpg.
Использован, с небольшими изменениями, модуль из 9-го выпуска. Файл Ktty.h Файл Ktty.c
Следующий код используется для загрузки программы в память (по адресу 0x25000),
перехода в защищенный режим и передачи управления по адресу, находящемуся в ячейке 0x25000.
Данный загрузчик имеет более широкие возможности, а для упомянутых действий можно было написать гораздо более простой код.
Однако, чтобы не делать лишней работы, можно использовать предлагаемый вариант.
Следует только следить, чтобы число загружаемых секторов (старший байт слова по адресу control:) соответствовало размеру файла kernel.bin.
Проект собирался под Windows средствами nasmw и djgcc.
Тестирование проведено в эмуляторе Bochs.
Нужно заметить, что для Windows нет нормально работающих GNU инструментов.
Так, при сборке средствами djgcc линковщик не находит глобальных символов, определенных в ассемблерных модулях.
Однако данная ошибка не мешает запускать данный проект.
Intro от Lonesome'a
Переключение задач
Содержание:
Введение
Файл app.asm - код задачи.
Физически код будет находиться внутри основного модуля (по адресу 0x25000),
при этом в пространстве задачи он будет спроецирован с адреса 4 (первые 4 байта достанутся от "охватывающего" файла - см. далее).
С адреса 0x2000 к задаче будет подключена видеопамять.
Задача будет в бесконечном цикле выводить символы в заданную позицию.
[BITS 32]
[ORG 4]
start:
mov eax, [count]
inc eax
mov [count], eax
and eax, 0x3f
add eax, 0x20
mov [0x2080], al ; put a symbol
jmp start
count: dd 0
Распределение памяти и определения
#define bool int
#define true 1
#define false 0
#define NULL 0
#define const
#define pointer void*
#define address unsigned long
#define DWORD unsigned long
#include "Defs.h"
#define KERNEL_STACK_BOTTOM 0x1000
// нижняя граница стека для ядра, стек имеет фиксированный размер
#define KERNEL_STACK_ORG (0x10000-4)
// начало (верхняя граница) стека для ядра
#define GDT_MAIN 0x10000
// физический адрес главной (общей для всех и единственной) GDT
#define IDT_MAIN 0x20000
// физический адрес IDT
#define TSS_KERNEL_MAIN 0x21000
// сегмент статуса для ядра
#define PAGE_DIRECTORY 0x23000
// каталог разделов для ядра
#define PAGE_TABLE 0x24000
// каталог страниц для первого раздела
#define KERNEL_START 0x25000
// начало кода ядра
#define VIRTUAL_START 0x100000
// начало действия страничного преобразования (для ядра)
// с этого момента все адреса линейные и не совпадают с физическими
#define PAGE_DIR_LINK 0x100000
// точка монтирования для каталогов разделов задач
// ядро не имеет постоянного доступа к данным процессов,
// поэтому нужны точки монтирования для временного подключения
#define PAGE_TAB_LINK 0x101000
// точка монтирования для каталогов страниц задач
#define TSS_APP_LINK 0x102000
// точка монтирования для сегмента статуса процессов
#define IPC_LINK 0x104000
// точка монтирования для данных
#define KERNEL_HEAP 0x200000
// начало кучи ядра
#define GDT_VIRTUAL 0xFFF00000
// дополнительный линейный адрес GDT (ссылка на тот же объект)
// нужен для обращения из процессов (которым недоступен первичный адрес)
// линейное пространство процессов ограничено этим адресом (0xFFEFFFFF)
#define IDT_VIRTUAL 0xFFF10000
// дополнительный линейный адрес IDT
#define TSS_KERNEL_VIRTUAL 0xFFF11000
// дополнительный линейный адрес сегмента статуса ядра
#define TSS_APP_VIRTUAL 0xFFF13000
// сегмент статуса для процессов
Файл startup.asm - головной модуль.
[BITS 32]
[EXTERN kernel_main]
[EXTERN kernel_last]
[GLOBAL start]
start: dd begin ; loader jumps indirectly
incbin 'app.bin'
begin:
mov esp, 0x10000 ; init stack
mov ax, ds
mov es, ax
call kernel_main
call kernel_last
jmp 0x20: 0 ; to app task
Основной модуль kernel.c
#include "Process.h"
#include "ktty.h"
void kernel_main()
{
unsigned long addr;
int i;
unsigned long* p_dir=(unsigned long*) PAGE_DIRECTORY;
init_tty();
clear();
puts(" Kernel started..\n");
for (addr=0x100000; addr<0xFFF00000; addr+=0x1000){
*((unsigned long*)addr)=0xF0B0C;
if (*((unsigned long*)addr)!=0xF0B0C) break;
}
puts("We have "); put16(addr); puts(" bytes of memory\n");
g_PhysicalPagesNumber=addr/0x1000;
create_Directory(PAGE_DIRECTORY);
create_Directory(PAGE_TABLE);
p_dir[0]=(PAGE_TABLE&0xfffff000)|1; // link to first page dir
for (addr=0; addr<VIRTUAL_START; addr+=4096)
if (mount_page_kernel(addr,addr)==0) puts("##error -- mount_page()\n");
puts("\n");
for (i=0; i<9; i++) put16(p_dir[i]);
for (i=0; i<9; i++) put16(p_dir[1024+i]);
puts("Pages directory prepared.\n");
asm("cli\n mov $0x23000, %eax\n mov %eax, %cr3");
asm("mov %cr0, %eax\n or $0x80000000, %eax\n mov %eax, %cr0");
puts("Pagination made on!\n");
init_GDT();
puts("GDT reloaded.\n");
}
void kernel_last()
{
char* b=(char*) 0x70000;
puts("Kernel: last part..\n");
if (mount_page(0x70000,0xb8000,PAGE_DIRECTORY)==0) puts("##error -- mount_page() - new\n");
b[0]='!';
init_TSS();
puts("init_TSS - completed");
}
Инициализация GDT и TSS.
#include "Memory.h"
bool init_GDT();
bool init_TSS();
struct TSS
{
DWORD back_link;
DWORD ESP0;
DWORD SS0;
DWORD ESP1;
DWORD SS1;
DWORD ESP2;
DWORD SS2;
DWORD CR3;
DWORD EIP;
DWORD EFLAGS;
DWORD EAX;
DWORD ECX;
DWORD EDX;
DWORD EBX;
DWORD ESP;
DWORD EBP;
DWORD ESI;
DWORD EDI;
DWORD ES;
DWORD CS;
DWORD SS;
DWORD DS;
DWORD FS;
DWORD GS;
DWORD LDT;
DWORD offset_andT;
DWORD IOPB;
};
#include "Process.h"
unsigned long g_gdtr[2];
#define TSS_APP (TSS_KERNEL_MAIN+0x1000)
bool init_GDT()
{
unsigned long* gdt=(unsigned long*) GDT_MAIN;
gdt[0]=0; // not used
gdt[1]=0;
gdt[2]=0x0000FFFF; // code
gdt[3]=0x00CF9A00;
gdt[4]=0x0000FFFF; // data
gdt[5]=0x00CF9200;
gdt[6]=((TSS_KERNEL_MAIN<<16)&0xFFFF0000)|((sizeof(struct TSS))&0x0000FFFF);
gdt[7]=(TSS_KERNEL_MAIN&0xFF000000)|0x8900|((TSS_KERNEL_MAIN>>16)&0x000000FF);
// TSS kernel
gdt[8]=((TSS_APP<<16)&0xFFFF0000)|((sizeof(struct TSS))&0x0000FFFF);
gdt[9]=(TSS_APP&0xFF000000)|0x8900|((TSS_APP>>16)&0x000000FF);
// TSS app
g_gdtr[0]=(GDT_MAIN<<16)|0xFFFF;
g_gdtr[1]=(GDT_MAIN>>16)&0xFFFF;
asm("lgdt g_gdtr");
asm("mov $0x18, %eax\n ltr %ax");
return true;
}
bool init_TSS()
{
struct TSS* tss=(struct TSS*)TSS_APP;
address Dir;
tss->back_link=0;
tss->ESP0=0x2000;
tss->SS0=0x10;
tss->ESP1=0x2000;
tss->SS1=0x10;
tss->ESP2=0x2000;
tss->SS2=0x10;
tss->CR3=0;
tss->EIP=0x4;
tss->EFLAGS=0;
tss->EAX=0;
tss->ECX=0;
tss->EDX=0;
tss->EBX=0;
tss->ESP=0x2000;
tss->EBP=0;
tss->ESI=0;
tss->EDI=0;
tss->ES=0x10;
tss->CS=0x8;
tss->SS=0x10;
tss->DS=0x10;
tss->FS=0x10;
tss->GS=0x10;
tss->LDT=0;
tss->offset_andT=0;
tss->IOPB=0xFFFFFFFF;
Dir=alloc_page(0);
put16(Dir);
create_Directory(Dir);
mount_page(0,KERNEL_START,Dir);
put16(*((unsigned long*)Dir));
mount_page(GDT_MAIN,GDT_MAIN,Dir); // GDT
mount_page(0x1000,alloc_page(0),Dir); // for stack
mount_page(0x2000,0xB8000,Dir); // screen
tss->CR3=Dir;
return true;
}
Управление страницами памяти (memory.c)
#include "Map.h"
extern address g_FreePhysicalMemory;
extern unsigned long g_PhysicalPagesNumber;
address alloc_page(long owner);
// выделить свободную страницу для owner
bool mount_page(address logical, address physical, address Directory);
// подключает логическую страницу к физической (address=long)
// Directory - физический адрес каталога, в котором проводится подключение
// если это не каталог ядра, то производится временное монтирование на PAGE_DIR_LINK
#define mount_page_kernel(l,p) mount_page(l,p,PAGE_DIRECTORY)
void create_Directory(address add);
// инициализирует новый каталог
- линейный адрес, на который проводится монтирование;
- физический адрес монтируемой страницы;
- физический адрес каталога страниц, в котором ведется монтирование.
#include "Memory.h"
unsigned long g_PhysicalPagesNumber=0;
struct MemPage* g_PhysicalPages=NULL;
address g_FreePhysicalMemory=0x00100000;
address alloc_page(long owner)
{
address a=g_FreePhysicalMemory;
if ((a&0xfff) || (a/4096>=g_PhysicalPagesNumber)) return 0;
g_FreePhysicalMemory+=4096;
return a;
}
unsigned long g_inv_addr;
bool mount_page(address logical, address physical, address Directory)
// подключает логическую страницу к физической
// Directory - физический адрес каталога, в котором проводится подключение
// при необходимости производится временное монтирование на PAGE_DIR_LINK
{
address Page;
unsigned long* p_dir=(unsigned long*) Directory;
int i_dir=(logical>>22)&0x3ff;
int i_page=(logical>>12)&0x3ff;
unsigned long* p_page=NULL;
int i;
bool new_dir;
if (Directory>=VIRTUAL_START){
p_dir=(unsigned long*) PAGE_DIR_LINK;
mount_page_kernel(PAGE_DIR_LINK,Directory);
g_inv_addr=PAGE_DIR_LINK;
asm("mov g_inv_addr, %eax\n invlpg (%eax)");
}
new_dir=!((p_dir[i_dir])&1);
if (new_dir){ // directory does not exist => create
Page=alloc_page(0);
if (!Page) return false;
p_dir[i_dir]=(Page&0xfffff000) | 1;
}
Page=p_dir[i_dir]&0xfffff000;
if (Page>=VIRTUAL_START){
p_page=(unsigned long*) PAGE_TAB_LINK;
mount_page_kernel(PAGE_TAB_LINK,Page);
g_inv_addr=PAGE_TAB_LINK;
asm("mov g_inv_addr, %eax\n invlpg (%eax)");
}
else p_page=(unsigned long*) Page;
if (new_dir) for (i=0; i<1024; i++) p_page[i]=0;
p_page[i_page]=(physical&0xfffff000) | 1;
return true;
}
void create_Directory(address add)
// создать новый каталог
{
unsigned long* p=(unsigned long*) add;
int i;
if (add>=VIRTUAL_START){
p=(unsigned long*) PAGE_DIR_LINK;
mount_page_kernel(PAGE_DIR_LINK,add);
g_inv_addr=PAGE_DIR_LINK;
asm("mov g_inv_addr, %eax\n invlpg (%eax)");
}
for (i=0; i<1024; i++) p[i]=0;
}
Модуль вывода на экран.
void init_tty();
void clear();
void putchar(char c);
void puts(const char *s);
void put16(unsigned long addr);
extern int tty_cursor;
extern int tty_attribute;
#define VIDEO_WIDTH 80
#define VIDEO_HEIGHT 25
#define VIDEO_RAM 0xb8000
#include "ktty.h"
int tty_cursor;
int tty_attribute;
void init_tty()
{
tty_cursor = 0;
tty_attribute = 10;
}
void clear()
{
char *video = (char*) VIDEO_RAM;
int i;
for (i = 0; i < VIDEO_HEIGHT*VIDEO_WIDTH; i++) {
*(video + i*2) = ' ';
}
tty_cursor = 0;
}
void putchar(char c)
{
char *video = (char*) VIDEO_RAM;
int i;
switch (c) {
case '\n': //Если это символ новой строки
tty_cursor+=VIDEO_WIDTH;
tty_cursor-=tty_cursor%VIDEO_WIDTH;
break;
default:
*(video + tty_cursor*2) = c;
*(video + tty_cursor*2+1) = tty_attribute;
tty_cursor++;
break;
}
//Если курсор вышел за границу экрана, сдвинем экран вверх на одну строку
if(tty_cursor>VIDEO_WIDTH*VIDEO_HEIGHT){
for(i=VIDEO_WIDTH*2;i<=VIDEO_WIDTH*VIDEO_HEIGHT*2+VIDEO_WIDTH*2;i++){
*(video+i-VIDEO_WIDTH*2)=*(video+i);
}
tty_cursor-=VIDEO_WIDTH;
}
}
void puts(const char *s)
{
while(*s) {
putchar(*s);
s++;
}
}
static void conv16(char* s, unsigned long addr)
{
int i;
unsigned char c;
for (i=0; i<8; i++){
c=(unsigned char)((addr>>(4*(7-i)))&0xf);
if (c<10) s[i]=c+'0';
else s[i]=c-10+'A';
}
s[8]=0;
}
void put16(unsigned long addr)
{
char s[16];
conv16(s,addr);
puts(s);
if (tty_cursor%80!=0) puts(" ");
}
Начальный загрузчик (Boot.asm)
[BITS 16]
[ORG 0x7c00]
_start:
cli
mov ax, cs
mov ds, ax
mov ss, ax
mov sp, _start
lgdt [gd_reg]
in al, 0x92
or al, 2
out 0x92, al
sti
mov si, msg
call kputs
.loop:
mov al, [control+3]
test al,al
jz .cont
call read_record
mov si, msg_read
call kputs
mov [proc_addr], long move_record
call prot_call
jmp .loop
.cont:
mov si, msg_jump
call kputs
mov [proc_addr], long prot_jump
call prot_call
;; Завесим процессор
hlt
jmp short $
%define SIGNATURE 0xF0B0C
;; ---------------- device control -------------------
signature: dd SIGNATURE
control: dd 0x07000002
;;CL=$ CL - младшие шесть бит - номер сектора
;;CH=$+1 старшие два бита CL и CH - номер цилиндра (дорожки)
;;DH=$+2 DH - номер головки
;;AL=$+3 AL - количество секторов (в сумме не более одного цилиндра)
destination: dd 0x25000
;; address to place section
next_record: dd 0x0
;; ---------------------------------------------------
entry_point: dd 0x25000
device: dd 0
;;DL=$ DL - номер диска (носителя). Например 0 - дисковод A:
;;----------------------------------------------------
read_record:
mov [.errors_counter], byte 0
cmp [signature], long SIGNATURE
je .start
mov si, msg_giving_up_sig
call kputs
hlt
jmp short $
.start:
mov ax, 0x800
mov es, ax
mov bx, 0
mov cx, [control]
mov dh, [control+2]
mov al, [control+3]
mov ah, 0x02
int 0x13
jc .read_error
ret
%define MAX_READ_ERRORS 5
.errors_counter: db 0
.read_error:
inc byte [.errors_counter]
cmp byte [.errors_counter], MAX_READ_ERRORS
jl .start
mov si, msg_giving_up
call kputs
hlt
jmp short $
kputs:
.loop:
lodsb
test al, al
jz .quit
mov ah, 0x0E
int 0x10
jmp short .loop
.quit:
ret
msg: db "Startup..",0x0A,0x0D,0
msg_read: db "<=read<=",0x0A,0x0D,0
msg_jump: db "Jump..",0x0A,0x0D,0
msg_giving_up: db "Fatal: Too many errors",0x0A,0x0D, 0
msg_giving_up_sig: db "Fatal: Wrong record signature",0x0A,0x0D, 0
proc_addr: dd prot_jump
;; make a call in protected mode and return to real
prot_call:
pushf
cli
mov eax, cr0
or al, 1
mov cr0, eax
jmp 0x8: _protected
back:
mov ax, cs
mov ds, ax
mov ss, ax
popf
ret
[BITS 32]
_protected:
mov ax, 0x10
mov ds, ax
mov ss, ax
call [proc_addr]
;; Сброс бита PE регистра CR0
mov eax, cr0
and al, 11111110b
mov cr0, eax
jmp 0: back
prot_jump:
mov eax, [entry_point]
jmp [eax]
move_record:
; В DS - адрес исходного сегмента
; В ES - адрес целевого сегмента
mov ax, ds
mov es, ax
mov esi, 0x8000 ; source
mov edi, [destination]
; Копируем [control+3] секторов по 512 байт
xor eax, eax
mov al, [control+3]
shl eax, 7 ; *128
mov ecx, eax
cld ; choose direction
cmp esi, edi
jz .get_next
jg .copy
std
mov eax, ecx ; backward from the top
dec eax
shl eax, 2
add esi, eax
add edi, eax
.copy: rep movsd
.get_next:
mov esi,[next_record]
test esi, esi
jnz .load_record
mov [control+3], byte 0
ret
.load_record:
mov edi, signature
cld
movsd ; signature
movsd ; control
movsd ; destination
movsd ; next address
ret
gdt:
dw 0, 0, 0, 0 ; Нулевой дескриптор
db 0xFF ; Сегмент кода с DPL=0
db 0xFF ; Базой=0 и Лимитом=4 Гб
db 0x00
db 0x00
db 0x00
db 10011010b
db 0xCF
db 0x00
db 0xFF ; Сегмент данных с DPL=0
db 0xFF ; Базой=0 и Лимитом=4Гб
db 0x00
db 0x00
db 0x00
db 10010010b
db 0xCF
db 0x00
;; Значение, которое мы загрузим в GDTR:
gd_reg:
dw 8192
dd gdt
times 510-($-$$) db 0
db 0x55,0xAA
incbin 'kernel.bin'
Командный файл для компиляции.
gcc -fno-leading-underscore -ffreestanding -c -o ktty.o ktty.c
gcc -fno-leading-underscore -ffreestanding -c -o memory.o memory.c
gcc -fno-leading-underscore -ffreestanding -c -o process.o process.c
gcc -fno-leading-underscore -ffreestanding -c -o kernel.o kernel.c
nasmw -fbin -o app.bin app.asm
nasmw -fcoff -o startup.o startup.asm
ld -Ttext 0x25000 -o kernel.bin startup.o ktty.o memory.o process.o kernel.o
objcopy kernel.bin -O binary
nasmw -fbin -o image.bin boot.asm
http://www.lowlevel.ru - Сайт. Наш сайт :)
http://www.lowlevel.ru/articles/ - Архив всех выпусков рассылки (кроме того, его можно найти здесь)
http://www.lowlevel.ru/cgi-bin/yabb/YaBB.cgi - Форум
Посмотреть 10 последних сообщений форума
http://sf.net/projects/tyros/ - Открытая операционная система Tyros/Neutronix, разрабатываемая по материалам нашей рассылки
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||