Как создать ядро программы

Содержание

Обложка: Руководство по созданию ядра для x86-системы. Часть 1. Просто ядро

Давайте напишем простое ядро, которое можно загрузить при помощи бутлоадера GRUB x86-системы. Это ядро будет отображать сообщение на экране и ждать.

One does simply write a kernel

Как загружается x86-система?

Прежде чем мы начнём писать ядро, давайте разберёмся, как система загружается и передаёт управление ядру.

В большей части регистров процессора при запуске уже находятся определённые значения. Регистр, указывающий на адрес инструкций (Instruction Pointer, EIP), хранит в себе адрес памяти, по которому лежит исполняемая процессором инструкция. EIP по умолчанию равен 0xFFFFFFF0. Таким образом, x86-процессоры на аппаратном уровне начинают работу с адреса 0xFFFFFFF0.

Как сделать семантическое ядро для сайта за 5 минут?

На самом деле это — последние 16 байт 32-битного адресного пространства. Этот адрес называется вектором перезагрузки (reset vector).

Теперь карта памяти чипсета гарантирует, что 0xFFFFFFF0 принадлежит определённой части BIOS, не RAM. В это время BIOS копирует себя в RAM для более быстрого доступа. Адрес 0xFFFFFFF0 будет содержать лишь инструкцию перехода на адрес в памяти, где хранится копия BIOS.

Так начинается исполнение кода BIOS. Сперва BIOS ищет устройство, с которого можно загрузиться, в предустановленном порядке. Ищется магическое число, определяющее, является ли устройство загрузочным (511-ый и 512-ый байты первого сектора должны равняться 0xAA55).

Когда BIOS находит загрузочное устройство, она копирует содержимое первого сектора устройства в RAM, начиная с физического адреса 0x7c00; затем переходит на адрес и исполняет загруженный код. Этот код называется бутлоадером.

Бутлоадер загружает ядро по физическому адресу 0x100000. Этот адрес используется как стартовый во всех больших ядрах на x86-системах.

Все x86-процессоры начинают работу в простом 16-битном режиме, называющимся реальным режимом. Бутлоадер GRUB переключает режим в 32-битный защищённый режим, устанавливая нижний бит регистра CR0 в 1. Таким образом, ядро загружается в 32-битном защищённом режиме.

Заметьте, что в случае с ядром Linux GRUB видит протоколы загрузки Linux и загружает ядро в реальном режиме. Ядро самостоятельно переключается в защищённый режим.

Что нам нужно?

  • x86-компьютер;
  • Linux;
  • ассемблер NASM;
  • gcc;
  • ld (GNU Linker);
  • grub;

Исходники можно найти на GitHub.

Задаём точку входа на ассемблере

Как бы не хотелось ограничиться одним Си, что-то придётся писать на ассемблере. Мы напишем на нём небольшой файл, который будет служить исходной точкой для нашего ядра. Всё, что он будет делать — вызывать внешнюю функцию, написанную на Си, и останавливать поток программы.

Что такое ядро операционной системы? Назначение и виды ядер

Как же нам сделать так, чтобы этот код обязательно был именно исходной точкой?

Пишем макет 16-битного ядра на C/C++

В первой и второй статьях я лишь коротко представил процесс написания загрузчика на ассемблере и C. Для меня это было хоть и непросто, но в то же время интересно, так что я остался доволен. Однако создания загрузчика мне показалось мало, и я увлекся идеей его расширения дополнительной функциональностью. Но так как в итоге размер готовой программы превысил 512 байт, то при попытке запуска системы с несущего ее загрузочного диска я столкнулся с проблемой “This is not a bootable disk”.

О чем эта статья?

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

Нужен ли для этого опыт?

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

Как и ранее, процесс изложения построен по принципу вопрос-ответ, что должно упростить восприятие информации.

План статьи

  • Ограничения загрузчика
  • Вызов из загрузчика других файлов диска
  • Файловая система FAT
  • Принцип работы FAT
  • Среда разработки
  • Написание загрузчика для FAT
  • Мини-проект: написание 16-битного ядра
  • Тестирование ядра

Ограничения загрузчика

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

В итоге передо мной стоит две задачи:

  • Расширить загрузчик кодом, реализующим дополнительную функциональность.
  • Сохранить при этом размер загрузчика в 512 байт.
Читайте также:
Какие камеры используют в программе опасные связи

Как я буду это делать?

  • Напишу программу kernel.c на C, внедрив в нее всю необходимую функциональность.
  • Скомпилирую и сохраню исполняемый файл как kernel.bin.
  • Скопирую этот файл во второй сектор загрузочного диска.

В загрузчике мы можем просто загрузить второй сектор, содержащий kernel.bin, в RAM по адресу, к примеру, 0x1000 , а затем перейти к этому адресу из 0x7с00 и запустить kernel.bin.

Вот схема для лучшего понимания идеи:

Запуск из загрузчика других файлов диска

Как мы теперь знаем, у нас есть возможность передачи управления от загрузчика ( 0x7c00 ) в другую область памяти, где размещается, например, наш kernel.bin, после чего продолжить выполнение. Но здесь у меня я хочу кое-что уточнить.

Как узнать сколько секторов kernel.bin займет на диске?

Ну это простой вопрос. Для ответа на него нам достаточно выполнить несложную арифметику, а именно разделить размер kernel.bin на размер сектора, который составляет 512 байт. Например, если kernel.bin будет равен 1024 байта, то и займет он 2 сектора.

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

Можно ли добавить помимо kernel.bin другие файлы, например office.bin, entertainment.bin, drivers.bin?

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

Откуда мы знаем, что после загрузочного сектора выполняются именно желаемые файлы?

Хороший вопрос, так как мы, действительно, только загружаем соответствующий сектор в память и начинаем его выполнение. Это не идеальный вариант, и в нем кое-чего не хватает.

Чего не хватает?

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

Что произойдет, если по ошибке загрузить во второй сектор не тот файл, обновить загрузчик и начать выполнение?

В этом случае система просто дает сбой. Поэтому на диске должна присутствовать фиксированная область, где подобно индексу в книге будут прописаны все имена файлов.
Тогда загрузчик будет запрашивать индекс этого файла на диске, и в случае обнаружения таковой будет обнаружен в списке – продолжать загрузку этого файла в память.

Мне такой вариант очень нравится, так как он избавляет от лишних действий.

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

Как это решается?

Для этого нужно просто организовать информацию на диске, после чего перепрограммировать загрузчик, повысив тем самым его эффективность при загрузке файлов.

Такой вид организации информации называется файловой системой. Существует много типов файловых систем, среди которых есть как коммерческие, так и бесплатные. Вот некоторые из них:

FAT

Для лучшего понимания этой файловой системы вам потребуется знать некоторые технические нюансы.

Минимальной единицей измерения пространства FAT является кластер, который занимает 1 сектор накопителя. Иначе говоря, на дискете, отформатированной в FAT, 1 кластер эквивалентен 1 сектору и, соответственно, равен 512 байт.

Для удобства использования файловая система дополнительно разделяется на четыре основные области:

  • загрузочный сектор (boot sector);
  • таблицу размещения файлов (file allocation table);
  • корневой каталог (root directory);
  • область данных (data area).

Рассмотрим каждую часть подробнее.

Загрузочный сектор

Загрузочный сектор содержит служебную информацию, на основе которой ОС распознает тип файловой системы диска, после чего уже переходит к чтению его содержимого.

Информация о файловой системе FAT, содержащаяся в загрузочном секторе, называется блоком параметров BIOS.

Блок параметров BIOS

Ниже я привел пример значений из этого блока:

Таблица размещения файлов

Эта таблица представляет своего рода связанный список, в котором каждая ячейка, соответствующая кластеру файла, содержит значение следующего кластера, формируя таким образом связанную цепочку всех кластеров этого файла.

Это значение кластера служит для:

  • определения окончания файла. Если оно находится между 0x0ff8 и 0x0fff , значит файл не содержит данных в других секторах, т.е. достигнут его конец.
  • определения следующего кластера с данными этого файла.

Корневой каталог

Корневой каталог выступает в роли индекса всех находящихся на диске файлов. Именно здесь загрузчик ищет имя нужного файла и в случае обнаружения обращается к его первому кластеру для начала загрузки.

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

Область данных

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

Принцип работы FAT

Продолжим наш пример с kernel.bin, который загрузчик помещает в память для выполнения. Теперь в этом сценарии нужно прописать для загрузчика следующую функциональность:

  • Сравнить первые 11 байт данных с kernel.bin, начиная со смещения 0 в таблице корневого каталога.
  • В случае совпадения этой строки – извлечь первый кластер kernel.bin из смещения 26 корневого каталога.
  • Далее преобразовать этот кластер в соответствующий сектор и загрузить его данные в память.
  • После загрузки первого сектора в память перейти к поиску в FAT следующего кластера файла и определить, является он последним, или есть еще данные в других кластерах.
Читайте также:
Как изменить картинку программы

Среда разработки

Для успешной реализации этой задачи нам нужно иметь представление о следующем. Более подробную информацию по этим пунктам можете найти в двух предыдущих статьях.

  • Операционная система (GNU Linux).
  • Ассемблер (GNU Assembler).
  • Набор инструкций (x86).
  • Написание инструкций для микропроцессора x86 на GNU Assembler.
  • Компилятор (GNU C компилятор GCC).
  • Компоновщик (GNU linker ld)
  • Эмулятор, например bochs, используемый для тестирования.

Написание загрузчика FAT

Ниже я привожу фрагмент кода для выполнения файла kernel.bin на FAT-диске.

Файл: stage0.S

/********************************************************************************* * * * * * Name : stage0.S * * Date : 23-Feb-2014 * * Version : 0.0.1 * * Source : assembly language * * Author : Ashakiran Bhatter * * * * Описание: основная логика подразумевает сканирование файла kernel.bin * * на дискете fat12 и передачу этому файлу права * * выполнения. * * Использование: подробности в файле readme.txt * * * * * *********************************************************************************/ .code16 .text .globl _start; _start: jmp _boot nop /*блок параметров BIOS описание каждой сущности */ /*——————— ————————— */ .byte 0x6b,0x69,0x72,0x55,0x58,0x30,0x2e,0x31 /* метка OEM */ .byte 0x00,0x02 /* байтов в секторе */ .byte 0x01 /* секторов в кластере */ .byte 0x01,0x00 /* зарезервированных секторов */ .byte 0x02 /* таблиц fat */ .byte 0xe0,0x00 /* записей в каталоге */ .byte 0x40,0x0b /* всего секторов */ .byte 0xf0 /* описание среды передачи */ .byte 0x09,0x00 /* размер в каждой таблице fat */ .byte 0x02,0x01 /* секторов в дорожке */ .byte 0x02,0x00 /* головок на цилиндр */ .byte 0x00,0x00, 0x00, 0x00 /* скрытых секторов */ .byte 0x00,0x00, 0x00, 0x00 /* больших секторов */ .byte 0x00 /* идентификатор загрузочного диска*/ .byte 0x00 /* неиспользуемых секторов */ .byte 0x29 /* внешняя сигнатура загрузки */ .byte 0x22,0x62,0x79,0x20 /* серийный номер */ .byte 0x41,0x53,0x48,0x41,0x4b,0x49 /* метка тома 6 байт из 11 */ .byte 0x52,0x41,0x4e,0x20,0x42 /* метка тома 5 байт из 11 */ .byte 0x48,0x41,0x54,0x54,0x45,0x52,0x22 /* тип файловой системы */ /* включение макросов */ #include «macros.S» /* начало основного кода */ _boot: /* инициализация среды */ initEnvironment /* загрузка stage2 */ loadFile $fileStage2 /* бесконечный цикл */ _freeze: jmp _freeze /* непредвиденное завершение программы */ _abort: writeString $msgAbort jmp _freeze /* включение функций */ #include «routines.S» /* пользовательские переменные */ bootDrive : .byte 0x0000 msgAbort : .asciz «* * * F A T A L E R R O R * * *» #fileStage2: .ascii «STAGE2 BIN» fileStage2: .ascii «KERNEL BIN» clusterID : .word 0x0000 /* перемещение от начала к 510-му байту */ . = _start + 0x01fe /* добавление сигнатуры загрузки */ .word BOOT_SIGNATURE

В этом основном файле загрузки происходит:

  • Инициализация всех регистров и настройка стека вызовом макроса initEnvironment .
  • Вызов макроса loadFile для загрузки kernel.bin в память по адресу 0x1000:0000 и последующей передачи ему права выполнения.

Файл: macros.S

Этот файл содержит все предопределенные макросы и функции.

/********************************************************************************* * * * * * Name : macros.S * * Date : 23-Feb-2014 * * Version : 0.0.1 * * Source : assembly language * * Author : Ashakiran Bhatter * * * * * *********************************************************************************/ /* предопределенный макрос: загрузчик */ #define BOOT_LOADER_CODE_AREA_ADDRESS 0x7c00 #define BOOT_LOADER_CODE_AREA_ADDRESS_OFFSET 0x0000 /* предопределенный макрос: сегмент стека */ #define BOOT_LOADER_STACK_SEGMENT 0x7c00 #define BOOT_LOADER_ROOT_OFFSET 0x0200 #define BOOT_LOADER_FAT_OFFSET 0x0200 #define BOOT_LOADER_STAGE2_ADDRESS 0x1000 #define BOOT_LOADER_STAGE2_OFFSET 0x0000 /* предопределенный макрос: разметка дискеты */ #define BOOT_DISK_SECTORS_PER_TRACK 0x0012 #define BOOT_DISK_HEADS_PER_CYLINDER 0x0002 #define BOOT_DISK_BYTES_PER_SECTOR 0x0200 #define BOOT_DISK_SECTORS_PER_CLUSTER 0x0001 /* предопределенный макрос: разметка файловой системы */ #define FAT12_FAT_POSITION 0x0001 #define FAT12_FAT_SIZE 0x0009 #define FAT12_ROOT_POSITION 0x0013 #define FAT12_ROOT_SIZE 0x000e #define FAT12_ROOT_ENTRIES 0x00e0 #define FAT12_END_OF_FILE 0x0ff8 /* предопределенный макрос: загрузчик */ #define BOOT_SIGNATURE 0xaa55 /* пользовательские макросы */ /* макрос для установки среды */ .macro initEnvironment call _initEnvironment .endm /* макрос для отображения строки на экране. */ /* Для выполнения этой операции он вызывает функцию _writeString */ /* параметр: вводная строка */ .macro writeString message pushw message call _writeString .endm /* макрос для считывания сектора в памяти */ /* Вызывает функцию _readSector со следующими параметрами */ /* параметры: номер сектора */ /* адрес загрузки */ /* смещение адреса */ /* количество считываемых секторов */ .macro readSector sectorno, address, offset, totalsectors pushw sectorno pushw address pushw offset pushw totalsectors call _readSector addw $0x0008, %sp .endm /* макрос для поиска файла на FAT-диске. */ /* Для этого он вызывает макрос readSector */ /* параметры: адрес корневого каталога */ /* целевой адрес */ /* целевое смещение */ /* размер корневого каталога */ .macro findFile file /* считывание таблицы FAT в память */ readSector $FAT12_ROOT_POSITION, $BOOT_LOADER_CODE_AREA_ADDRESS, $BOOT_LOADER_ROOT_OFFSET, $FAT12_ROOT_SIZE pushw file call _findFile addw $0x0002, %sp .endm /* макрос для преобразования заданного кластера в номер сектора */ /* Для этого он вызывает _clusterToLinearBlockAddress */ /* параметр: номер кластера */ .macro clusterToLinearBlockAddress cluster pushw cluster call _clusterToLinearBlockAddress addw $0x0002, %sp .endm /* макрос для загрузки целевого файла в память. */ /* Он вызывает findFile и загружает данные соответствующего файла в память */ /* по адресу 0x1000:0x0000 */ /* параметр: имя целевого файла */ .macro loadFile file /* проверка наличия файла */ findFile file pushw %ax /* считывание таблицы FAT в память */ readSector $FAT12_FAT_POSITION, $BOOT_LOADER_CODE_AREA_ADDRESS, $BOOT_LOADER_FAT_OFFSET, $FAT12_FAT_SIZE popw %ax movw $BOOT_LOADER_STAGE2_OFFSET, %bx _loadCluster: pushw %bx pushw %ax clusterToLinearBlockAddress %ax readSector %ax, $BOOT_LOADER_STAGE2_ADDRESS, %bx, $BOOT_DISK_SECTORS_PER_CLUSTER popw %ax xorw %dx, %dx movw $0x0003, %bx mulw %bx movw $0x0002, %bx divw %bx movw $BOOT_LOADER_FAT_OFFSET, %bx addw %ax, %bx movw $BOOT_LOADER_CODE_AREA_ADDRESS, %ax movw %ax, %es movw %es:(%bx), %ax orw %dx, %dx jz _even_cluster _odd_cluster: shrw $0x0004, %ax jmp _done _even_cluster: and $0x0fff, %ax _done: popw %bx addw $BOOT_DISK_BYTES_PER_SECTOR, %bx cmpw $FAT12_END_OF_FILE, %ax jl _loadCluster /* выполнение ядра */ initKernel .endm /* параметры: имя целевого файла */ /* макрос для передачи права выполнения файлу, загруженному */ /* в память по адресу 0x1000:0x0000 */ /* параметры: none */ .macro initKernel /* инициализация ядра */ movw $(BOOT_LOADER_STAGE2_ADDRESS), %ax movw $(BOOT_LOADER_STAGE2_OFFSET) , %bx movw %ax, %es movw %ax, %ds jmp $(BOOT_LOADER_STAGE2_ADDRESS), $(BOOT_LOADER_STAGE2_OFFSET) .endm

Читайте также:
Программа чтобы открыть мрт снимки

Общая сводка

initEnvironment:

  • Макрос для установки сегментных регистров.
  • Аргументов не требует.

writeString:

  • Макрос для отображения на экране строки с завершающим нулем.
  • В качестве аргумента передается строковая переменная с завершающим нулем.
  • Макрос для чтения с диска заданного сектора и его загрузки в целевой адрес памяти.
  • Количество аргументов: 4.
  • Макрос для проверки наличия файла.
  • Количество аргументов: 1.

clusterToLinearBlockAddress:

  • Макрос для преобразования заданного кластера в номер сектора.
  • Количество аргументов: 1.

Источник: habr.com

Ассемблер и ядро Операционной Системы

Ядро в данном случае будет загружаться по адресу со смещением в 1M относительно начала памяти компьютера. Описываются переменные code, data, bss, end. Задается выравнивание всех сегментов на границу страницы памяти (4096 байт). В качестве стартовой берётся функция с именем start. Функция взята из примера: http://www.osdever.net/bkerndev/Docs/basickernel.htm

; This is the kernel’s entry point. We could either call main here, ; or we can use this to setup the stack or other nice stuff, like ; perhaps setting up the GDT and segments. Please note that interrupts ; are disabled at this point: More on interrupts later! [BITS 32] global start start: mov esp, _sys_stack ; This points the stack to our new stack area jmp stublet ; This part MUST be 4byte aligned, so we solve that issue using ‘ALIGN 4’ ALIGN 4 mboot: ; Multiboot macros to make a few lines later more readable MULTIBOOT_PAGE_ALIGN equ 1

Приведенный пример удивляет адресом загрузки.

Как в real mode можно загрузить ядро по физическому адресу за пределами первого мегабайта? Ответ: в real mode никак. Пример этот рассчитан на использование GRUB, работающего в защищенном режиме. Подведу итоги про ld. Существует множество версий gcc и binutils.

Они поддерживают самые разные форматы объектных файлов. В том числе есть версии binutils скомпилированные под Windows. Это позволяет разрабатывать ядро Операционной системы с использованием любого подходящего для разработчика компилятора.

Главное, чтобы в пару к компилятору была соответствующая версия binutils, поддерживающая нужную версию объектных и выполнимых (тоже объектных) файлов. Дополнительные примеры линкерного скрипта можно посмотртеть в статье: Настройка виртуальных адресов внутри ядра UEFI P.S.

Есть ещё один аргумент против ассемблера… Как известно, загрузкой операционной системы после включения компьютера занимается BIOS. BIOS — Basic Input Output System. А в целом, API для работы с аппаратурой, представляет из себя набор прерываний, с передачей данные через регистры, а также набор портов ввода-вывода и memory mapped областей памяти.

Появилась такая система в момент разработки первых компьютеров IBM/PC еще в 80-е годы прошлого века. Архитектура IBM PC Compatible систем медленно эволюционировала. Появлялись новые системные шины и стандарты: ISA, EISA, PCI, и т.д. При этом BIOS оставался частной собственностью нескольких компаний.

BIOS предоставляет интерфейсы программам только в реальном режиме, интерфейсы BIOS устарели, несли бремя совместимости и ограничены в возможностях. Такая ситуация стала тормозить развитие фантазии разработчиков аппаратных средств. При разработке платформы Itanium, Intel выступил с инициативой EFI (Extendible Firmware Interface). Идея EFI заключалась в замене BIOS новой разработкой.

EFI, в отличии от BIOS больше не базируется на прерываниях. EFI больше не написан на ассемблере, и предоставляет для использования таблицы функций разного назначения написанных на Cи. Естественно процесс продвижения новой технологии долгий.

Прошло несколько лет и после версии EFI 1.10 Intel передал права на дальнейшее развитие нового стандарта ново созданной инициативной группе UEFI, включающей в себя представителей Intel, IBM, Apple, Microsoft и других. Теперь эта технология получила приставку Unified, означающую, что единые переносимые firmware получат как компьютеры на базе Intel PC, так и планшетники на базе ARM.

UEFI, по сравнению с BIOS несомненно прогрессивен. Он предоставляет графические драйвера, сетевые драйвера и т.д. Компьютер без операционной системы будет способен осуществлять в том числе сетевые подключения… Однако есть у этого всего и минусы. Одной из технологий, поддерживаемой UEFI является Safe boot. Запрет загрузки не подписанного софта.

Это означает, что возможно создание железа, на котором будет загружаться лишь одна разрешенная (подписанная) операционная система. Самым недавним ударом по BIOS стало требование Microsoft во всех системах, сертифицированных для работы с Windows 8 наличие UEFI safe boot. Что из вышесказанного следует?

Из вышесказанного следует, что платформе Intel PC в том её виде, в котором она существует сегодня, жить осталось недолго. Выход Windows 8 не за горами. Второй вывод — ассемблер перестает быть инструментом, даже при написании операционных систем. С появлением новых интерфейсов, необходимость в ассемблере, судя по всему уменьшится. Третий вывод — загрузка операционной системы, да и архитектура драйверов в ближайшее время переживет революционные изменения. Продолжение читайте здесь…

Источник: dev64.wordpress.com

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru