Как я писал ядро ОС. Платформа

    Лучший способ объяснить — это самому сделать!
    Л. Кэролл.

Часть 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(&current->ctx);   \
            sub();                     \
            schedule();                \
            irq_return(&current->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

К списку статей

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

Основная сфера моей деятельности связана с поддержкой Solaris и оборудования Sun/Oracle, хотя в последнее время к ним прибавились технологии виртуализации (линейка Citrix Xen) и всякое разное от IBM - от xSeries до Power. Учусь на кафедре Вычислительной Техники НИУ ИТМО.

See you...out there!

http://www.facebook.com/profile.php?id=100001947776045
https://twitter.com/AnnoyingBugs