Лучший способ объяснить — это самому сделать!
Л. Кэролл.
Часть 1. Платформа
Часть 2. Архитектура и библиотека ядра
Часть 3. Ключевые подсистемы ядра
Надо сказать, что разработка ОС - идея фикс любого программиста-системщика, потому на просторах интернета мы часто наблюдаем сообщения как о новых интересных разработках ОС, таких как Phantom OS, так и пшики типа BolgenOS :) Также есть куча информации по этому вопросу: http://wiki.osdev.org/ , но и велика трудность реализации: она связана с большой сложностью современного железа: тут тебе и режимы x86-процессора и драйвера для PCI и ATA. Ну в общем разработка ОС под x86-платформу - дело непростое и в конце концов я уперся в какие-то загадочные проблемы с настройкой IDT. Вторую попытку я совершил, уже учась в магистратуре.
Так как направление моего обучения было связано с встроенными системами - платформа тоже оказалась с микроконтроллером Cortex-M3 (в те времена его активно пиарили, и потому железка обошлась сравнительно дешево: 1000 рублей за плату LPCXpresso от NXP (подразделение Philips) с LPC1768 и 100 евро за Base Board с различными и нтерфейсами ввода-вывода, в том числе USB-2-Serial, OLED-дисплеем, динамиком и Ethernet. В отличие от x86 - Cortex-M3 оказался очень простым для программирования процессором: например обработчики прерывания в нем - сишные функции.
В контроллере всего 64 Кб оперативной памяти - это оказалось некоторой трудностью, поэтому требовалось экономить память на каждом шагу. Сейчас ядро ОС занимает 12 Кб в памяти (с учетом различного рода отладочных буферов), большая часть из которых конфигурируется. К тому же в нем отсутствовал MMU (Memory Management Unit), а значит не было виртуальной памяти (однако есть MPU, позволяющий организовывать защиту адресного пространства) - это привело к тому, что затруднена динамическая компоновка. В итоге я использовал статическую компоновку проекта, которая управляется скриптом ld - я писал о ней чуть ранее: Создание скриптов компоновщика ld.
Также процессоры Cortex-M3 имеют общее 32-х битное физическое адресное пространство, которое включает в себя Flash, содержащий код, оперативную память и порты устройств, а значит - отпадает необходимость в загрузчике. Не смотря на слово ARM в имени процессора и архитектуры (она называется ARM7-M), система комманд ARM не поддерживается - только Thumb-2. Что интересно, из-за этого флаг системы команд в регистре состояния процессора (его можно переключать), должен всегда быть установлен в 1 (режим Thumb-2). Когда я забыл про это в коде порождения и инициализации регистров потока, это стоило мне дня отладки.
Итак, мне потребовалось много терпения чтобы припаять ножки для подключения LPCXpresso от NXP к BaseBoard (т.к. работа паяльником - не мой конек) - и вуаля:

Отладочная плата LPC1768 + Baseboard (кликабельно)

Мое рабочее место (кликабельно)
Разработка под эту платформу ведется с помощью среды LPCXpresso. Для купивших плату она бесплатна (однако с ограничением на максимальный размер прошивки: до 128 Кб, при том что в моем микроконтроллере 512Кб флеш памяти). Она построена на базе Eclipse и надо сказать наследует все черты современных IDE: графический отладчик позволяет читать память контроллера, пошагово исполнять программу и строить стеки вызовов.

Среда LPCXpresso (кликабельно)
Первое что я сделал - скачал примеры и скомпилировал их. Мне помигал RGB-светодиод, значит что-то я таки спаял правильно. Однако теперь требовалось досконально изучить организацию микроконтроллера, и в этом мне помогла книга "The definitive guide to ARM Cortex-M3", а также ряд спецификаций. Нужно было также изучить ассемблерные команды Thumb-2, и ряд интересных инструкций.
Cortex-M3 содержит 16 регистров общего назначения, однако общедоступно только 13 из них. Регистр r13 является текущим указателем стека (при этом, реально таких регистра два, также как и режима работы: MSP - указатель стека ядра, а PSP - указатель стека пользовательского потока), LR - Link Register (указывает на инструкцию, на которую должен быть произведен при выходе из функции) и PC - программный счетчик. Остальные регистры, в частности регистр состояния процессора носят статус специальных, и для их чтения и записи нужно использовать инструкции MRS и MSR.
Набор команд у Cortex-M3 - RISC'овый, но есть и ряд интересных инструкций. Например инструкция ITE (if-then-else), позволяет выполнять до 3х инструкций за собой по условию или его инверсии, что уменьшает количество условных переходов:
tst r0, #0
itte eq
or r1, r2, r3 /*Выполнится, если r0 == 0*/
ldr r3, [r7] /*Выполнится, если r0 == 0*/
ldr r1, [r7] /*Выполнится, если r0 != 0*/
mov r2, r4 /*Выполняется всегда*/
Есть и ряд специфичных инструкций, таких как WFI - Wait-for-interrupt, необходимый для создания idle-потока и операций эксклюзивного доступа к памяти, позволяющих реализовать операции test-and-set (реализация test-and-set, атомарных операций и спин-блокировок находится в файле kernel/src/platform/microops.c).
Еще один важный аспект при разработке ОС - обработка прерываний и исключительных ситуаций. В Cortex-M3 она представлена NVIC (Nested Vector Interrupt Controller). Во-первых в файле kernel/src/init.c содержится вектор прерываний (таблица указателей на соответствующие обработчики):
-
Исключения NMI и Hard Fault. Первое сигнализирует о фатальном сбое системы, второе же обычно случается, если не был обработан другая исключительная ситуация (например ошибка доступа к шине).
-
Выход за границы памяти, ограниченной MMU (Memmanage fault), или обращение по недопустимому адресу (Bus fault)
-
Внутрипрограммная ошибка (usage fault), например деление на ноль
-
Специальные исключения, предназначенные для реализации системных вызовов - SVCall и PendSV
-
Прерывание от системного таймера SYSTICK
-
Внешние прерывания (в частности прерывание UART, используемого для отладки)
Общий код для обработчиков прерываний расположен в kernel/include/platform/irq.h При прерывании (равно как и при вызове функции) процессор сам сохраняет регистры r0-r3, r12, LR, PC и xPSR. Все остальные сохраняются вручную в структуру описания потока (tcb_t). Это позволяет легко написать переключение контекста: достаточно просто при выходе из прерывания загрузить чужие регистры и указатель стека.
Собственно обертка над обработчиком прерывания выглядит так (current - указатель на такущий исполняемый поток):
#define IRQ_HANDLER(name, sub) \
void name() __NAKED; \
void name() { \
irq_save(¤t->ctx); \
sub(); \
schedule(); \
irq_return(¤t->ctx); \
}
И наконец, код инициализации системы kernel/src/start.c. Он содержит в себе копирование сегментов данных из Flash в оперативную память и обнуление сегментов BSS, а также вызовы инициализации различных подсистем ядра, в том числе - UART'а, используемого для отладки:
dbg_uart_init(115200);
dbg_puts("\n\n---------------------------------------"
"\nL4Xpresso hello!\n");
Такой вот "Hello, world". На этом пока все, в следующих статьях я расскажу про библиотеку ядра L4Xpresso и диспетчеры потоков и памяти.
Исходники проекта располагаются на github: https://github.com/myaut/l4xpresso