В этой части пришла пора положить теорию на реальный код. Рассмотрим, как всё сказанное раньше записывается на языке С++ (именно он является основным для разработки программ под ОСРВ МАКС). Здесь мы поговорим только о минимально необходимых вещах, без которых невозможна ни одна программа.
Содержание (опубликованные и неопубликованные статьи):
Часть 1. Общие сведения
Часть 2. Ядро ОСРВ МАКС
Часть 3. Структура простейшей программы (настоящая статья)
Часть 4. Настройка ОС для работы
Часть 5. Первое приложение
Часть 5. Средства синхронизации потоков
Часть 6. Средства обмена данными между задачами
Часть 7. Работа с прерываниями
Код
Так как у ОСРВ МАКС объектно-ориентированная модель, то и программа должна содержать классы. При этом базовые классы уже имеются в составе ОС, прикладной программист должен лишь создать от них наследников и дописать требуемую функциональность.
Для реализации приложения следует сделать наследника от класса Application (обязательно перекрыв в нём виртуальную функцию Initialize()) и один или несколько наследников класса Task (обязательно перекрыв в нём виртуальную функцию Execute()). И всем этим будет управлять планировщик, реализованный в классе Scheduler.
Рис. 1. Минимально необходимые для работы классы (серые — уже имеются, белые — следует дописать)
Класс Application
На первый взгляд класс кажется совершенно ненужной прослойкой. В нём необходимо перекрыть метод Initialize(), в котором инициализируется приложение. Именно внутри данной функции удобно создавать задачи (хотя, это не догма, задачи можно создавать где угодно, просто внутри данной функции — удобнее всего).
void VPortUSBApp::Initialize()
{
Task::Add(vport = new VPortUSBTask, Task::PriorityNormal, Task::ModeUnprivileged, 400);
Task::Add(new HelloTask, Task::PriorityNormal, Task::ModeUnprivileged, 400);
}
Казалось бы, почему нельзя инициализировать приложение в функции main(), а данный класс — выкинуть, как лишний? Но не будем торопиться. Во-первых, эта функция всегда вызывается в привилегированном режиме, поэтому в ней можно настраивать аппаратуру, включая программирование NVIC, чего нельзя сделать в обычном режиме. Кроме того, этот класс выполняет намного больше функций, чем просто инициализация приложения.
В первую очередь, именно через объект этого класса ОС находит приложение. Я хотел было написать, что «находит приложение без глобальных переменных», но если заняться крючкотворством, то статическая переменная-член класса Application
static Application * m_app;
всё-таки является глобальной. Но как там в классике: «Самоса — сукин сын, но это — наш сукин сын». Переменная — глобальная, но она хорошо структурирована и принадлежит классу приложения. Соответственно, её имя изолировано ото всех остальных классов. Для обращения к ней имеется функция
inline Application & App() { return * Application::m_app; }
которую можно перекрыть, чтобы возвращать не исходный, а унаследованный тип класса. Например, один из тестов описывает класс со следующим перекрытием:
class AlarmMngApp : public Application
{
...
public:
...
static inline AlarmMngApp & App() { return * (AlarmMngApp *) m_app; }
Таким образом, данный класс содержит функциональность, благодаря которой приложение всегда может найти ту ниточку, потянув за которую оно придёт к нужной своей части. Это может пригодиться, например, в обработчиках прерываний.
Следующая неочевидная вещь при использовании класса Application — его конструктор. В конструкторе передаётся тип многозадачности.
/// @brief Конструктор приложения
/// @param use_preemption Режим многозадачности
/// (true - вытесняющая, false - кооперативная).
Application(bool use_preemption = true);
Класс Application содержит виртуальную функцию OnAlarm(). Она будет вызываться для информирования об исключительных ситуациях. Их перечень достаточно велик:
AR_NMI_RAISED, ///< Произошло немаскируемое прерывание (Non Maskable Interrupt, NMI)
AR_HARD_FAULT, ///< Аппаратная проблема (произошло прерывание Hard Fault)
AR_MEMORY_FAULT, ///< Произошла ошибка доступа к памяти (MemManage interrupt)
AR_NOT_IN_PRIVILEGED, ///< Произошла попытка выполнить привилегированную операцию в непривилегированном режиме...
AR_NMI_RAISED, ///< Произошло немаскируемое прерывание (Non Maskable Interrupt, NMI)
AR_HARD_FAULT, ///< Аппаратная проблема (произошло прерывание Hard Fault)
AR_MEMORY_FAULT, ///< Произошла ошибка доступа к памяти (MemManage interrupt)
AR_NOT_IN_PRIVILEGED, ///< Произошла попытка выполнить привилегированную операцию в непривилегированном режиме
AR_BAD_SVC_NUMBER, ///< Произошла попытка использовать SVC с некорректным номером сервиса
AR_COUNTER_OVERFLOW, ///< Произошло переполнение счетчика
AR_STACK_CORRUPTED, ///< Затерт маркер на верхней границе стека
AR_STACK_OVERFLOW, ///< Произошло переполнение стека задачи
AR_STACK_UNDERFLOW, ///< Произошел выход за нижнюю границу стека
AR_SCHED_NOT_ON_PAUSE, ///< Произошла попытка продолжить выполнение планировщика не в состоянии паузы
AR_MEM_LOCKED, ///< Менеджер памяти заблокирован
AR_USER_REQUEST, ///< Вызвано пользователем
AR_ASSERT_FAILED, ///< Условие проверки ASSERT не выполнилось
AR_STACK_ENLARGED, ///< Возникла необходимость в увеличении стека задачи
AR_OUT_OF_MEMORY, ///< Память "кучи" исчерпана
AR_SPRINTF_TRUNC, ///< Вывод функции sprintf был урезан из-за нехватки места в буфере
AR_DOUBLE_PRN_FMT, ///< Более одного последовательного вызова PrnFmt в одной и той же задаче
AR_NESTED_MUTEX_LOCK, ///< Произошла попытка повторно заблокировать нерекурсивный мьютекс одной и той же задачей
AR_OWNED_MUTEX_DESTR, ///< Мьютекс, захваченный одной из задач, удалён
AR_BLOCKING_MUTEX_DESTR, ///< Мьютекс, блокировавший одну или несколько задач, удалён
AR_NO_GRAPH_GUARD, ///< Попытка выполнить операцию рисования без использования GraphGuard
AR_UNKNOWN ///< Неизвестная ошибка
Перекрыв функцию, можно обеспечить обработку ошибок (либо аварийное выключение аппаратуры для того, чтобы она не вышла из строя). Для результата функции определены следующие значения:
AA_CONTINUE, ///< Продолжить выполнение задачи
AA_RESTART_TASK, ///< Перезапустить вызвавшую ошибку задачу
AA_KILL_TASK, ///< Снять с выполнения задачу, вызвавшую ошибку
AA_CRASH ///< Останов системы
Далее рассмотрим метод Run(). Именно его следует вызвать для того, чтобы ОС начала работу приложения. Собственно, типовая функция main() должна выглядеть следующим образом:
#include "DefaultApp.h"
int main()
{
MaksInit();
static DefaultApp app;
app.Run();
return 0;
}
ОСРВ МАКС поддерживает только одно приложение. Поэтому следует объявлять только один экземпляр класса, унаследованного от Application.
Класс Task
Непосредственно код задачи. Класс в чистом виде никогда не используется, для работы следует создавать наследника от него (либо пользоваться готовыми наследниками, о которых будет сказано в конце раздела).
Функция Execute()
Самая-самая главная функция в задаче — это, разумеется, виртуальная функция Execute().В классических процедурно-ориентированных ОС, программист должен реализовать функцию потока, а затем передать её в качестве аргумента для функции CreateThread(). При объектно-ориентированном подходе, алгоритм проще:
- Создать наследника от класса Task,
- Функция потока будет иметь имя Execute(). Достаточно перекрыть её. Ничего больше никуда передавать не требуется.
Те, кто привык работать с классическими ОС, заметят, что у функции Execute() нет аргументов, а в потоковую функцию традиционно принято передавать один аргумент. Чаще всего это указатель на целую структуру, содержащую те или иные параметры. Объектно-ориентированный подход избавляет от всех сложностей, связанных с подобным механизмом. Для задачи проектируется класс. Он может содержать неограниченное (в рамках системных ресурсов) число переменных-членов. В них можно размещать совершенно произвольные параметры любыми доступными способами, например, сделать публичные переменные и заполнять их, пока класс не поставлен на исполнение, или передать параметры в конструктор, а он сам заполнит поля, или сделать функции, которые заполняют параметры (сделать функцию инициализации или функции-сеттеры).
Таким образом, вместо одного указателя, который обрабатывается строго в потоковой функции, получаем широчайшие возможности инициализации данных, отделив их от непосредственно рабочего кода. Сама функция Execute(), соответственно, осталась без параметров.
Итак. Первое правило разработки любого класса задачи: Следует создать класс-наследник от Task и перекрыть в нём функцию Execute().
При выходе из функции Execute() задача удаляется из планировщика, но не удаляется из памяти, так как она может быть как на куче, так и на стеке, а оператор delete, применённый к стековому объекту, вызовет ошибку. Таким образом, удаление объекта задачи по окончании работы с ним — прикладного программиста.
Конструктор класса
Теперь поговорим про конструктор класса. Все конструкторы класса Task находятся в секции protected, поэтому их нельзя вызвать напрямую. Для этого следует в классе-наследнике реализовать свой конструктор, который вызовет тот или иной конструктор класса Task.
Примеры таких конструкторов-наследников:
class TaskYieldBeforeTestTask_1: public Task
{
public:
000000explicit TaskYieldBeforeTestTask_1():
000000000000Task()
000000{
000000}
Вариант немного посложнее:
000000explicit MessageQueuePeekTestTask_1(const char* name):
000000000000Task(name)
000000{
000000}
Стоит обратить внимание на такой параметр, как «имя задачи». Этот параметр является необяза-тельным, но иногда весьма полезным. Более того, существует два альтернативных метода его хранения. Самый простой метод — объявить в файле maksconfig.h константу
#define MAKS_TASK_NAME_LENGTH 0
и игнорировать имя (по умолчанию, используется указатель nullptr). Очень часто этот вариант является самым удобным.
Второй вариант: не переопределять константу MAKS_TASK_NAME_LENGTH, в этом случае, при создании задачи память под хранение имени задачи будет выделяться в куче. Само же имя может использоваться, например, для занесения в журнал событий. Чем грозит использование динамической памяти — описано в одном из следующих разделов (правда, это не относится к случаю добавления задач на этапе инициализации).
Наконец, третий вариант: переопределить константу MAKS_TASK_NAME_LENGTH положительным числом. В этом случае имя задачи будет храниться в переменной-члене класса Task. Это избавляет от работы с кучей, но если ради одной задачи зарезервировано приблизительно 20 символов, то все задачи будут тратить столько же, пусть их имена и будут короче. Для «больших» машин безумное утверждение, там разработчики мыслят мегабайтами (имея в наличии гигабайты или даже десятки гигабайт). Но для слабых контроллеров экономия каждого байта до сих пор актуальна.
Теперь пора разобраться, какие конструкторы есть в классе Task. Их всего два. Первый выглядит следующим образом:
Task(const char * name = nullptr)
Задача, созданная через этот конструктор, получит стек, выделенный операционной системой из кучи.
Однако не всегда стек задачи следует выделять из основной кучи. Дело в том, что микроконтроллер может работать с двумя и более физическими устройствами ОЗУ. Простейший случай — внутреннее статическое ОЗУ контроллера на десятки или сотни килобайт и внешнее динамическое ОЗУ на единицы или десятки мегабайт. Внутреннее ОЗУ будет работать быстрее, чем внешнее. Однако, в зависимости от ситуации, программист может разместить кучу во внешней или внутренней памяти, ведь это же замечательно, когда куча имеет размер в несколько мегабайт! Стек лучше поместить во внутренней памяти контроллера. Соответственно, иногда лучше не доверяться выделению в куче, а указать расположение стека задачи самостоятельно, будучи уверенным, что он расположен в быстром ОЗУ. И в этом поможет конструктор задачи второго вида:
Task(size_t stack_len, uint32_t * stack_mem, const char * name = nullptr)
По его аргументам ясно, что в него кроме имени задачи также передаётся указатель на ОЗУ, где будет размещён стек задачи, а также явно указан размер стека в 32-битных словах (не в байтах)
Пример использования:
Class MyTask : public Task
{
Private:
uint32_t m_stack[100 * sizeof(uint32_t)];
public:
MyTask() : Task(m_stack) {}
};
Указать компилятору, в какой памяти размещать переменные, объявленные в той или иной функции, довольно просто, но описание этого займёт несколько листов, и сильно запутает читателя. Поэтому вынесем эту информацию на уровень видео-урока/вебинара.
Таким образом, второе, что следует реализовать в классе-наследнике от Task — это конструктор. Можно даже конструктор-пустышку, который просто вызывает конструктор класса-предка.
Функция Add()
Минимально необходимая часть кода класса, реализующего задачу, написана. Можно добавлять его в планировщик. Для этого используется семейство функций Add(). Рассмотрим их более детально.
Вот вариант с наименьшим числом аргументов, где программист доверяет операционной системе разобраться со всеми параметрами самостоятельно:
static Result Add(Task * task, size_t stack_size = Task::ENOUGH_STACK_SIZE)
Добавляет задачу с возможностью указать необходимый ей размер стека (в 32-битных словах).
Пример вызова:
Task::Add(new MessageQueueDebugTestTask_1("MessageQueueDebugTestTask_1"));
Если требуется явно задать режим работы задачи (привилегированный или непривилегированный), можно воспользоваться следующим вариантом функции Add:
static Result Add(Task * task, Task::Mode mode, size_t stack_size = Task::ENOUGH_STACK_SIZE)
Пример вызова:
Task::Add(new RF_SendTask, Task::ModePrivileged);
Для примера ещё один вариант с явным указанием размера стека:
Task::Add(new MutexIsLockedTestTask_1("MutexIsLockedTestTask "), Task::ModePrivileged, 0x200);
Существует также вариант добавления задачи с указанием приоритета:
static Result Add(Task * task, Task::Mode mode, Task::Priority priority, size_t stack_size = Task::ENOUGH_STACK_SIZE)
Пример вызова:
Task::Add(new EventBasicTestManagementTask(), Task::PriorityRealtime);
Самый полный вариант: и с указанием приоритета, и с указанием режима работы:
static Result Add(Task * task, Task::Priority priority, Task::Mode mode, size_t stack_size = Task::ENOUGH_STACK_SIZE);
Пример вызова:
Task::Add(new NeigbourDetectionService(), Task::PriorityAboveNormal, Task::ModePrivileged);
Напомним, что самым удобным местом для вызова функции Add() в типовом случае, является функция Initialize() класса задачи.
void RFApplication::Initialize()
{
button.rise(&button_pressed);
button.fall(&button_released);
Task::Add(new SenderTask(), Task::PriorityNormal, Task::ModePrivileged, 0x100);
Task::Add(new ReceiverTask(), Task::PriorityNormal, Task::ModePrivileged, 0x100);
}
Однако эта функция может быть вызвана в произвольном месте кода. В классах тестирования ОС можно встретить подобные конструкции:
int EventIntTestMain::RunStep(int step_num)
{
switch ( step_num ) {
default :
_ASSERT(false);
case 1 :
Task::Add(new EventBasicTestManagementTask(), Task::PriorityRealtime);
return 1;
case 2 :
Task::Add(new EventUnblockOrderTestManagementTask(), Task::PriorityRealtime);
return 1;
case 3 :
Task::Add(new EventTypeTestManagementTask(), Task::PriorityRealtime);
return 1;
case 4 :
Task::Add(new EventProcessingTestManagementTask(), Task::PriorityRealtime);
return 1;
}
}
И они вполне допустимы.
Таким образом, после того, как класс-наследник от класса Task создан, в нём переопределены конструктор (можно конструктор-пустышка, вызывающий конструктор класса Task) и функция Execute, этот класс следует подключить к планировщику при помощи функции Add(). Если планировщик работает, задача начнёт исполняться. Либо она начнёт исполняться с момента запуска планировщика.
Функции, которые удобно вызывать из класса задачи
Существует ряд функций, которые класс-наследник от класса Task может вызывать для обеспечения собственного функционирования в рамках ОС. Рассмотрим кратко их перечень:
Delay() | Блокирует задачу на заданное время, заданное в миллисекундах. |
CpuDelay() | Выполняет задержку в миллисекундах, не блокируя задачу. Соответственно, управление другим задачам на время задержки принудительно не передаётся (у задачи может быть забрано управление при переключении по системному таймеру). Но при кооперативной многозадачности возможна только эта функция. |
Yield() | Принудительно отдаёт управление планировщику, чтобы он начал исполнение следующей задачи. При кооперативной многозадачности переключение задач осуществляется именно этой функцией. При вытесняющей — функция может быть вызвана, если задача видит, что ей больше нечего делать и можно отдать остаток кванта времени другим задачам. |
GetPriority() | Возвращает текущий приоритет задачи |
SetPriority() | Устанавливает текущий приоритет задачи. Если задача понижает свой приоритет, то при вытесняющей многозадачности она вполне может быть вытеснена, не дожидаясь завершения кванта времени. |
Функции, обычно вызываемые извне
Некоторые функции, наоборот, предназначены для вызова извне. Например, функция, позволяющая узнать состояние задачи: если задача будет вызывать её, то всегда будет получать «Активно». Узнавать состояние задачи имеет смысл откуда-то извне. Аналогично и остальные функции данной группы.
GetState() | Возвращает состояние задачи (активна, заблокирована и т. п. |
GetName() | Возвращает имя задачи. |
Remove() | Удаляет задачу. Может вызываться и из самой задачи, тогда она принудительно инициирует переключение контекста. Объект задачи остаётся в памяти. |
Delete() | То же, что и Remove(), но с удалением объекта. Соответственно, объект должен быть созданным при помощи оператора new, а не на стеке. |
GetCurrent() | Возвращает указатель на текущую задачу. |
Защита стека задачи от переполнения
При создании задачи, определяется размер стека для неё. После этого, размер не может быть динамически изменён. Если он был выбран неудачно (по ходу работы образовалась большая вложенность вызовов, либо число локальных переменных оказалось высоко, что могло произойти уже при сопровождении программы), данные могут выскочить за выделенные пределы, повредив данные в стеках других задач, в куче, либо иные данные и производя прочие непредсказуемые действия. Такую ситуацию желательно выявить и сообщить разработчику, что она требует устранения.
Идеальным методом предотвращения такой ситуации была бы проверка на уровне компилятора, без участия ОС, но к сожалению, такой механизм как минимум, создаёт большие накладные расходы. Основная задача для контроллеров — не проверять программиста, а производить управление. При тактовой частоте в районе ста-двухсот мегагерц (а иногда — и десятков мегагерц), такой метод контроля уже неприемлем.
На уровне ОС также можно производить контроль стека на переполнение. В ОСРВ МАКС используются следующие методы защиты:
- Проверка текущего положения указателя стека при переключении задач. Почти не влияет на производительность, но обладает низкой надежностью. Во-первых, разрушение стека уже произошло, а во-вторых – за время системного такта программа могла не только войти в функцию, вызвавшую переполнение, но и выйти из неё, а значит — указатель мог успеть вернуться обратно в разрешенный диапазон.
- Если установлен размер стека больше минимального, то к нему автоматически добавляется одно слово на вершине, куда записывается «magic number» — 32 разрядное число случайного вида, которое вряд ли встретится при работе программы. При переполнении стека это число будет затерто данными приложения, что почти наверняка позволит зафиксировать факт переполнения стека даже после возвращения указателя в рабочую область.
- В том случае, когда процессор содержит блок MPU (Memory Protection Unit), сразу за границей стека помещается область памяти минимально допустимого размера с защитой от доступа. Это самый совершенный способ контроля, так как при любом обращении к защищённой области, произойдет аппаратное прерывание. Следует, однако, помнить, что в некоторых случаях, защитная зона может оказаться не тронутой. Например, если часть локальных переменных, которые попали именно в эту зону, зарезервированы, но не используются. Защита сделана для самоконтроля и не должна идти в ущерб основным задачам.
Детали для работы с защитой стека можно найти среди констант, заданы в классе Task (в файле MaksTask.h). Изучая комментарии к этим константам, можно понять конкретные величины параметров «минимальный стек», «защищаемая область» и т.п. При желании, этим параметры можно и изменить. Следует только помнить, что размер защищаемой области должен быть кратен степени двойки.
Класс Scheduler
Раз этот класс упомянут, как минимально необходимый компонент, рассмотрим и его интерфейсные функции, хотя он просто выполняет свою работу, скрытую от прикладного программиста. Но тем не менее, несколько полезных функций он всё-таки содержит.
GetInstance() | Статическая функция, при помощи которой можно получить ссылку на объект планировщика для того, чтобы в дальнейшем обращаться к нему. |
GetTickCount() | Возвращает число системных тиков, прошедших с момента старта планировщика. |
Pause() | Приостанавливает переключение задач планировщиком, либо включает работу заново (конкретное действие передаётся в аргументе функции). |
ProceedIrq | Функция будет рассмотрена в разделе про прерывания. |
Задавайте вопросы и оставляйте комментарии — это то, что вдохновляет на написание и публикацию статей.
Следующая часть будет посвящена настройке ОС для работы.
Комментарии (16)
lzb_j77
09.09.2017 18:08Это, скорее всего, не ОС в общем её виде, а некая вспомогательная среда для программиста для оживления железяки.
Коряво сказал, но, надеюсь, кто-нибудь поймёт :)EasyLy Автор
09.09.2017 19:56Как я уже говорил, философия — не мой конёк. Но тем не менее. Ядро — имеется. Управление потоками (тут они называются задачами) — имеется. Средства синхронизации задач — имеются. Драйверы — имеются.
Железку вполне можно оживить и без этого набора. Причём не будем доходить до фанатизма. Простые железки лично я без ядра оживляю, хотя драйверы — мне так нравятся, что я только через них работаю. Ядро нужно, когда система взаимодействий становится сложной.
Если посмотреть на большинство RTOS для низших контроллеров — у них у всех есть этот минимальный джентельменский набор, после которого у нас уже не набор библиотек, а ОС.
А что у нас круче, чем у других — будет ближе к концу цикла. Ну, и объектный подход. Хотя, в комментариях к первой части на эту тему много философии было, а философия — не мой конёк. Я считаю, что это — преимущество…Comdiv
09.09.2017 22:35философия — не мой конёк. Я считаю, что это — преимущество
Это зря. Ядро философии — логика. А я вижу в ваших рассуждениях об ОСРВ МАКС некоторые логически слабые места, ложные предпосылки, но спорить о них не видел смысла, потому что видно, что Вы их крепко держитесь. Но победить их — значит улучшить систему, которая не будет уводится не в лучшем направлении. Для данной системы уже поздно, но будут же и новые проекты.
В общем, рассуждения о ненужности философии — это приблизительно то же, что рассуждения о ненужности физики от человека, не желающего знать физику. И физика и философия будут в любом случае, но они либо будут отрицаемы и замутнены, либо признаваемы, очищаемы от шелухи и лучше служить делу.
И да, МАКС — это операционная система.EasyLy Автор
09.09.2017 22:43В общем, рассуждения о ненужности философии — это приблизительно то же, что рассуждения о ненужности физики от человека, не желающего знать физику.
Я просто хотел сказать, что меня в философских вопросах «завалить» — проще простого. Например, рассуждая о плюсах и минусах ООП применительно к ОС. В дебри же так просто сорваться.
Относительно этой ветки дискуссии — что такое ОС, а что — просто набор библиотек. Там можно долго спорить. И как людей в таком споре «уделывают» — я знаю. Вот и сразу сдаюсь :-).
Так что тут не нужность и ненужность философии, а просто философия — не мой конёк. Технические вопросы — ко мне. Философские — сразу сдаюсь, просто высказываю свои аргументы и всё. А жонглировать понятиями — не в состоянии.
EasyLy Автор
09.09.2017 22:58Аааа! Я понял, что Вы имели в виду. Надо читать сразу три предложения. «Философствовать про преимущества ООП и процедурного подхода можно долго, лично я считаю, что ООП — это преимущество, но философия — не мой конёк, поэтому я так считаю, а спорить не буду.». Имелось в виду это в тех трёх предложениях. «Преимущество» относилось к слову «ООП», а не «не мой конёк».
lzb_j77
10.09.2017 04:52Я тоже не очень философичен :)
Тем не менее — я категорически ЗА отечественное ПО, пусть даже с дырами в философии :) Со временем доведёте до ума.GarryC
11.09.2017 09:55А я категорически ПРОТИВ любого ПО (хоть импортного, хоть отечественного), которое не доведено до ума — при этом я совсем не имею в виду описываемую систему.
EasyLy Автор
11.09.2017 13:27Чисто на всякий случай. Вдруг кто-то не прочтёт, что Вы не про нашу ОС. Поэтому я «прикрою» Ваш комментарий своим.
Мы несколько лет доводили ОС до минимально возможного ума. Мы прошли через ядро, которое работает, но делать на нём что-то страшно, так как то тут то там видны всякие мелкие утечки, а на имеющихся объёмах памяти это критично. Мы прошли через чистку этого ядра (полный рефакторинг внутренних механизмов с сохранением интерфейсов). Затем — пробная эксплуатация для внутренних задач. И уже потом — начали это дело как-то продвигать.
Не то, чтобы там уже больше нечего править. Команда всё время чем-то активно занята. Но продвигать мы начали уже не совершенно сырой продукт. Так редко, но бывает. И тут было именно так.
Comdiv
EasyLy Автор
В целом — Вы правы. К счастью, у нас сейчас не могут запускаться сторонние приложения. Приложения собираются вместе с ОС, так что зловредный код, который пытается «просочиться» — отсутствует. Стек растёт в сторону меньших адресов, так что вылет за пределы массива — тоже выскочит за защищаемую область только если нечаянно произошло отрицательное смещение.
Итого: Задача защиты стека от случайного типового случая переполнения — всё равно выполняется. Случайное грубое переполнение (вылет за пределы массива в отрицательную область) и зловредные действия — Вы верно заметили, отслежены не будут. Пожалуй, я подниму этот вопрос на обсуждение с архитекторами и разработчиками в плане, не будет ли вопросов при сертификации. Возможно, это просто придётся отразить в документах, ведь такое возможно и на PC под Windows. По крайней мере, было возможно — точно, указателю всё равно, указывает он на стек, на кучу или вообще на проецируемые на память регистры аппаратуры.
Но чисто формально — всё равно, метод — самый совершенный. Так как все остальные методы — хуже.
Comdiv
EasyLy Автор
Для систем, обрабатывающих важные данные, используются аппаратные системы другого уровня с другими типами ОС. ОСРВ МАКС в текущем исполнении должна обеспечивать работу оборудования (станков, бытовых приборов, иных аппаратов) на микроконтроллерах с не самым мощным набором встроенной аппаратуры. В частности, в большинстве из них, всё исполняется во флэшке. Поэтому там уровень защиты определяется скорее производителем контроллера.
В частности, у контроллеров Cypress FX2LP (51-е ядро, не поддерживается нашей ОС, но уж больно пример показательный) есть Vendor команда USB, которую нельзя перекрыть. Через неё память пишется и читается. Но это уже разработчику аппаратуры виднее, если он захочет поставить контроллер, у которого есть такая то ли особенность, то ли дыра… Зависит от случая.
В общем, сейчас считаем, что собирается ОС и приложение, это дело как-то «шьётся», после чего — старается работать с максимальной производительностью, обеспечивая работу «железа». Защититься от проникновения средствами контроллера, типа как я описал для FX2LP — ОС бессильна. И её задача — обеспечить хорошую, производительную работу системы.
Если появится Заказчик с иными задачами — будем их решать.
EasyLy Автор
Прочитал по Вашей ссылке про защиту от этого возвратно-ориентированного программирования. По-моему, тут ядро не должно никак участвовать. Скорее протоколы взаимодействия должны всё учитывать, вот их пусть на сертификации и проверяют.
А если у злодея есть личный доступ к аппаратуре, то он и через JTAG вломится. Наверняка же программист за собой не подчистит, и этот порт не заблокирует. Опять же, блокировка JTAG, взведение битов защиты и прочее — это уже не задача ядра (о котором речь в этой статье), это уже задачи прикладника, так как после отключения будет невозможна отладка. То есть, делаться всё должно на финальной стадии, если это вообще требуется.
А защита стека — она чисто от случайного переполнения. Пришёл новый сотрудник, добавил локальных переменных, стек и переполнился. Вот такие вещи отследить. Они побольше бед, чем злодеи, натворить могут. Вспомним хотя бы «муху цэцэ» у накопителей Seagate. Она возникала от переполнения файла с логом событий. А сколько дисков в кирпичи превращалось. Хорошо, что нашли выход…
EasyLy Автор
Решил добавить: От переполнения, так как произошло большое количество вложенных вызовов функций. Либо в функциях было слишком много локальных переменных. Вот от таких вещей защита в первую очередь работает.
Как прикидывать, не случится ли такое, «на глазок» — будет описано в следующей части, она отрезана от этой части, так как получалось слишком много PageDown-ов.
EasyLy Автор
А текст я переформулирую в понедельник. Немного пообщаюсь с товарищем, который писал соответствующий код, и изменю формулировки глобально. Слово «любом» уберу, но лучше сразу добавлю подробностей, как можно настраивать защиту под ситуацию. А для этого — лучше сначала запытаю автора кода, чтобы отразить не только, что сам вижу в коде, но и все его задумки. Большое спасибо за то, что обратили внимание!